import test, { after, afterEach } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { BlockLightController } from "../block-light-controller.js"; import { buildBlockLight } from "../block-light.js"; import { loadLightProperties } from "../light-properties.js"; const properties = JSON.parse( await readFile(new URL("../light-properties.json", import.meta.url)), ); const savedWorker = globalThis.Worker, savedFetch = globalThis.fetch; const controllers = []; class FakeWorker { constructor() { this.packets = []; this.terminated = false; } postMessage(packet) { this.packets.push(structuredClone(packet)); } terminate() { this.terminated = true; } reply(index) { const packet = this.packets[index]; const { min, size, data, sourceCount, blockLevels, skyLevels } = buildBlockLight( new Map(packet.blocks), new Map(packet.materials), packet.bounds, ); this.onmessage({ data: { generation: packet.generation, field: { min, size, data, sourceCount, blockLevels, skyLevels }, milliseconds: 1, }, }); } } globalThis.Worker = FakeWorker; globalThis.fetch = async () => ({ ok: true, json: async () => properties }); afterEach(() => { for (const controller of controllers.splice(0)) { clearTimeout(controller.timer); controller.worker?.terminate(); } }); after(() => { if (savedWorker === undefined) delete globalThis.Worker; else globalThis.Worker = savedWorker; globalThis.fetch = savedFetch; }); const bounds = { min: [-2, 0, -2], max: [2, 3, 2] }; const materials = new Map([ [1, { state: "minecraft:torch", light: 14, render: [] }], [ 2, { state: "minecraft:sea_lantern", light: 15, render: [{ min: [0, 0, 0], max: [1, 1, 1] }], }, ], ]); function fixture() { const published = []; const controller = new BlockLightController((field) => published.push(field)); controllers.push(controller); return { controller, worker: controller.worker, published }; } test("requests awaiting metadata coalesce into the latest complete world", async () => { const { controller, worker, published } = fixture(); controller.request(new Map([["0,1,0", 1]]), materials, bounds); controller.request(new Map([["1,1,0", 2]]), materials, bounds); await loadLightProperties(); controller.start(); assert.equal(worker.packets.length, 1); assert.equal(worker.packets[0].generation, 2); assert.deepEqual(worker.packets[0].blocks, [["1,1,0", 2]]); worker.reply(0); assert.equal(published.length, 1); assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15); assert.equal(controller.status, "ready"); }); test("an old world's in-flight result is discarded before publishing the replacement world", async () => { const { controller, worker, published } = fixture(); await loadLightProperties(); const first = new Map([["0,1,0", 1]]); controller.request(first, materials, bounds); controller.start(); controller.request(new Map([["1,1,0", 2]]), materials, bounds); first.clear(); assert.equal(worker.packets.length, 1); worker.reply(0); assert.equal(published.length, 0); assert.equal(worker.packets.length, 2); worker.reply(1); assert.equal(published.length, 1); assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15); assert.equal(controller.pending, null); assert.equal(controller.busy, false); }); test("rapid edits during a build coalesce while the last valid field remains available", async () => { const { controller, worker, published } = fixture(); await loadLightProperties(); const blocks = new Map([["0,1,0", 1]]); controller.request(blocks, materials, bounds); controller.start(); worker.reply(0); const retained = published[0]; blocks.set("0,1,0", 2); controller.request(blocks, materials, bounds); controller.start(); blocks.set("1,1,0", 1); controller.request(blocks, materials, bounds); blocks.clear(); controller.request(blocks, materials, bounds); assert.equal(published.length, 1); assert.equal(retained.sample([0, 1, 0]).blockLevel, 14); worker.reply(1); assert.equal(worker.packets.length, 3); assert.equal(worker.packets[2].generation, 4); assert.deepEqual(worker.packets[2].blocks, []); assert.equal(published.length, 1); worker.reply(2); assert.equal(published.length, 2); assert.equal(published[1].sourceCount, 0); assert.equal(published[1].sample([0, 1, 0]).blockLevel, 0); }); test("worker failure recovers using the newest queued world and the same solver", async () => { const { controller, worker, published } = fixture(); await loadLightProperties(); controller.request(new Map([["0,1,0", 1]]), materials, bounds); controller.start(); controller.request(new Map([["1,1,0", 2]]), materials, bounds); let prevented = false; worker.onerror({ preventDefault() { prevented = true; }, }); assert.equal(prevented, true); assert.equal(worker.terminated, true); assert.equal(controller.worker, null); // Resolve through publish rather than a timing assumption about the fallback. await new Promise((resolve, reject) => { const timeout = setTimeout( () => reject(Error("Fallback lighting did not finish")), 1000, ); const publish = controller.publish; controller.publish = (field) => { clearTimeout(timeout); publish(field); resolve(); }; }); assert.equal(published.length, 1); assert.equal(published[0].sample([1, 1, 0]).blockLevel, 15); assert.equal(controller.status, "ready"); }); test("a duplicate stale reply cannot mark a newer build idle or start a concurrent job", async () => { const { controller, worker, published } = fixture(); await loadLightProperties(); controller.request(new Map([["0,1,0", 1]]), materials, bounds); controller.start(); controller.request(new Map([["1,1,0", 2]]), materials, bounds); worker.reply(0); assert.equal(worker.packets.length, 2); assert.equal(controller.busy, true); worker.reply(0); assert.equal(controller.busy, true); controller.request(new Map([["-1,1,0", 1]]), materials, bounds); controller.start(); assert.equal(worker.packets.length, 2); assert.equal(published.length, 0); worker.reply(1); assert.equal(worker.packets.length, 3); worker.reply(2); assert.equal(published.length, 1); assert.equal(published[0].sample([-1, 1, 0]).blockLevel, 14); worker.reply(2); assert.equal(published.length, 1); }); test("synchronous Worker construction failure still publishes light from the fallback solver", async () => { const previousWorker = globalThis.Worker; globalThis.Worker = class { constructor() { throw new Error("Worker blocked by browser policy"); } }; let context; try { context = fixture(); } finally { globalThis.Worker = previousWorker; } const { controller, published } = context; assert.equal(controller.worker, null); const completed = new Promise((resolve, reject) => { const timeout = setTimeout( () => reject(Error("Fallback lighting did not finish")), 1000, ); const publish = controller.publish; controller.publish = (field) => { clearTimeout(timeout); publish(field); resolve(); }; }); controller.request(new Map([["0,1,0", 1]]), materials, bounds); await loadLightProperties(); controller.start(); await completed; assert.equal(published.length, 1); assert.equal(published[0].sample([0, 1, 0]).blockLevel, 14); assert.equal(controller.status, "ready"); });