import { capturePatchTransactionSnapshot, generatePatch, generatePatchAsync, restorePatchTransactionSnapshot } from "./mapPatch.js"; import { collectTransferableBuffers } from "./transferUtils.js"; import { applyCommittedWorldDelta, hashCommittedWorld } from "./committedWorldDelta.js"; export { applyCommittedWorldDelta as applyCommittedMirrorDelta, hashCommittedWorld } from "./committedWorldDelta.js"; let persistentCommittedMirror = null; let persistentCommittedRevision = -1; let pendingMirrorBootstrap = null; const pendingApplyDeltas = new Map(); const rawCandidateWorkerSlots = []; const rawCandidateWorkerTerminations = []; let rawCandidateTaskSerial = 0; function normalizeWorkerTermination(value) { return Promise.resolve(value).catch(() => undefined); } function destroyRawCandidateWorkerSlot(index, reason = "Raw patch candidate worker was terminated.") { const slot = rawCandidateWorkerSlots[index]; if (!slot) return rawCandidateWorkerTerminations[index] || Promise.resolve(); rawCandidateWorkerSlots[index] = null; const active = slot.active; slot.active = null; if (active) { const error = new Error(reason); error.code = "raw-candidate-worker-terminated"; active.reject(error); } let termination; try { termination = normalizeWorkerTermination(slot.worker?.terminate?.()); } catch { termination = Promise.resolve(); } const trackedTermination = termination.finally(() => { if (rawCandidateWorkerTerminations[index] === trackedTermination) rawCandidateWorkerTerminations[index] = null; }); rawCandidateWorkerTerminations[index] = trackedTermination; return trackedTermination; } async function ensureRawCandidateWorkerSlot(index) { if (rawCandidateWorkerTerminations[index]) await rawCandidateWorkerTerminations[index]; const existing = rawCandidateWorkerSlots[index]; if (existing?.worker) return existing; if (typeof Worker !== "function") { const error = new Error("Nested module workers are not available in this environment."); error.code = "raw-candidate-worker-unavailable"; throw error; } const worker = new Worker(new URL("./patchCandidateWorker.js", import.meta.url), { type: "module" }); const slot = { worker, active: null, index }; worker.onmessage = (event) => { const message = event.data || {}; const active = slot.active; if (!active || Number(message.id) !== active.id) return; if (message.type === "raw-patch-candidate-progress") { active.onProgress?.(active.request, message.progress || {}); return; } if (message.type !== "raw-patch-candidate-result") return; slot.active = null; if (!message.ok) { const error = new Error(message.error || "Raw patch candidate generation failed."); error.code = message.code || "raw-patch-candidate-error"; error.stack = message.stack || error.stack; active.reject(error); return; } active.resolve(message.candidate); }; worker.onerror = (event) => { const active = slot.active; slot.active = null; const error = new Error(event?.message || "Raw patch candidate worker failed."); error.code = "raw-candidate-worker-error"; if (active) active.reject(error); // Do not reuse a worker that raised an uncaught error. The next task in // this lane waits for termination before constructing a replacement. destroyRawCandidateWorkerSlot(index, error.message); }; rawCandidateWorkerSlots[index] = slot; return slot; } async function runRawCandidateTask(slotIndex, request, onProgress) { const slot = await ensureRawCandidateWorkerSlot(slotIndex); if (slot.active) { const error = new Error(`Raw candidate worker slot ${slotIndex} is unexpectedly busy.`); error.code = "raw-candidate-worker-busy"; throw error; } const id = ++rawCandidateTaskSerial; return new Promise((resolve, reject) => { slot.active = { id, request, onProgress, resolve, reject }; try { slot.worker.postMessage({ type: "generate-raw-patch-candidate", id, taskId: request.taskId || `raw-candidate-${id}`, seed: request.seed >>> 0, mapOptions: request.mapOptions || {}, }); } catch (error) { slot.active = null; reject(error); } }); } async function precomputeRawCandidateBatch(requests, onProgress) { if (!Array.isArray(requests) || !requests.length) return []; if (requests.length > 2) throw new Error(`Raw candidate batch exceeds the two-worker limit (${requests.length}).`); try { return await Promise.all(requests.map((request, index) => runRawCandidateTask(index, request, onProgress))); } catch (error) { // A failed child may have left its module worker in an unknown state. Reset // the bounded pool before the coordinator falls back to serial generation. await Promise.all(rawCandidateWorkerSlots.map((slot, index) => slot?.active ? destroyRawCandidateWorkerSlot(index) : null)); throw error; } } // Continuously feed at most two production helpers instead of waiting for // fixed pairs to finish. Merge order remains strictly deterministic in // mapPatch.js. Callers choose a bounded sliding window; production uses one // resident result per lane so transferred candidate graphs cannot accumulate. export function scheduleRawCandidateSequence(requests, onProgress, options = {}) { const list = Array.isArray(requests) ? requests : []; const count = list.length; const laneCount = Math.max(1, Math.min(2, Math.floor(options.parallelism || 2), Math.max(1, count))); const windowSize = Math.max(laneCount, Math.min(count || laneCount, Math.floor(options.windowSize || (laneCount + 1)))); const recycleWorkers = options.recycleWorkers === true; // Keep each helper for a small bounded run. Reusing it forever lets // generateMap's module-local/cache state accumulate; recycling after every // tile creates excessive isolate churn on max-range selections. Three // complete candidates per lane is the bounded middle ground. const recycleEvery = recycleWorkers ? Math.max(1, Math.min(4, Math.floor(options.recycleEvery || 3))) : Infinity; const controls = list.map(() => { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); // The coordinator can stop after an earlier failure. Register a rejection // observer now so later cancelled items cannot become unhandled rejections. promise.catch(() => undefined); return { promise, resolve, reject, settled: false }; }); const released = new Uint8Array(count); let releasedPrefix = 0; let nextToSchedule = 0; let cancelled = false; let cancelError = null; const gateWaiters = new Set(); const wake = () => { for (const resolve of gateWaiters) resolve(); gateWaiters.clear(); }; const waitForGate = () => new Promise((resolve) => gateWaiters.add(resolve)); const allowedEnd = () => Math.min(count, releasedPrefix + windowSize); const takeNext = () => { if (cancelled || nextToSchedule >= count) return -1; if (nextToSchedule >= allowedEnd()) return -2; return nextToSchedule++; }; const failAll = (error) => { if (cancelled) return; cancelled = true; cancelError = error instanceof Error ? error : new Error(String(error || "Raw candidate sequence cancelled.")); for (const control of controls) { if (!control?.settled) { control.settled = true; const reject = control.reject; // Promise capability functions keep their Promise reachable. Clear // both callbacks as soon as the public Promise has settled so a // transferred raw candidate cannot remain retained through scheduler // bookkeeping after the coordinator drops publicPromises[index]. control.resolve = null; control.reject = null; reject?.(cancelError); } } for (let index = 0; index < laneCount; index++) { if (rawCandidateWorkerSlots[index]?.active) destroyRawCandidateWorkerSlot(index, cancelError.message); } wake(); }; const lane = async (slotIndex) => { let tasksSinceRecycle = 0; while (!cancelled) { const index = takeNext(); if (index === -1) return; if (index === -2) { await waitForGate(); continue; } try { let candidate = await runRawCandidateTask(slotIndex, list[index], onProgress); if (cancelled) return; const control = controls[index]; if (!control.settled) { control.settled = true; const resolve = control.resolve; control.resolve = null; control.reject = null; resolve?.(candidate); } // The Promise result owns the transferred graph now. Do not keep an // additional lane-local reference while waiting for the next window. candidate = null; // Reclaim helper-local generator state before it can grow without // paying module-worker startup/teardown cost on every single tile. tasksSinceRecycle++; if (recycleWorkers && tasksSinceRecycle >= recycleEvery) { await destroyRawCandidateWorkerSlot(slotIndex, `Raw candidate helper recycled after ${tasksSinceRecycle} completed tiles.`); tasksSinceRecycle = 0; } } catch (error) { failAll(error); return; } } }; // Keep the fulfilled Promise graph only in the public array. Once the // coordinator nulls an entry after merge, scheduler bookkeeping must not keep // the transferred candidate reachable for the rest of the sequence. const publicPromises = controls.map((control) => control.promise); for (const control of controls) control.promise = null; const lanes = count ? Array.from({ length: laneCount }, (_, index) => lane(index)) : []; const done = Promise.allSettled(lanes).then(() => undefined); return { promises: publicPromises, release(index) { const i = Math.floor(Number(index)); if (i < 0 || i >= count || released[i]) return; released[i] = 1; while (releasedPrefix < count && released[releasedPrefix]) releasedPrefix++; wake(); }, cancel(reason = "Raw candidate sequence cancelled.") { const error = reason instanceof Error ? reason : new Error(String(reason)); if (!error.code) error.code = "raw-candidate-sequence-cancelled"; failAll(error); return done; }, done, async dispose(reason = "Raw candidate sequence disposed.") { await done; await Promise.allSettled(Array.from({ length: laneCount }, (_, index) => rawCandidateWorkerSlots[index] ? destroyRawCandidateWorkerSlot(index, reason) : rawCandidateWorkerTerminations[index] ).filter(Boolean)); }, get cancelled() { return cancelled; }, get error() { return cancelError; }, laneCount, windowSize, }; } export async function shutdownRawCandidateWorkers() { const pending = []; for (let index = 0; index < rawCandidateWorkerSlots.length; index++) { if (rawCandidateWorkerSlots[index]) pending.push(destroyRawCandidateWorkerSlot(index, "Raw candidate operation completed.")); else if (rawCandidateWorkerTerminations[index]) pending.push(rawCandidateWorkerTerminations[index]); } if (pending.length) await Promise.allSettled(pending); } function isMirrorSyncMessage(type) { return typeof type === "string" && type.startsWith("patch-mirror-sync-"); } function safeMirrorKey(key, label = "key") { const normalized = String(key ?? ""); if (!normalized || normalized === "__proto__" || normalized === "prototype" || normalized === "constructor") { throw new Error(`Invalid mirror ${label}: ${normalized || ""}.`); } return normalized; } function defineMirrorEntry(target, key, value) { Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true, }); } function sameMirrorKeys(actual, expected) { const actualKeys = Object.keys(actual || {}); if (!Array.isArray(expected) || actualKeys.length !== expected.length) return false; const actualSet = new Set(actualKeys); return expected.every((key) => actualSet.has(key)); } function validateCompletedMirrorBootstrap(bootstrap) { if (!bootstrap?.world || !bootstrap?.manifest) throw new Error("Mirror bootstrap is incomplete."); const { world, manifest } = bootstrap; if (!sameMirrorKeys(world.fields, manifest.fieldKeys)) throw new Error("Mirror field manifest is incomplete."); if (!sameMirrorKeys(world.sourceMap, manifest.sourceKeys)) throw new Error("Mirror sourceMap manifest is incomplete."); const rootActual = Object.keys(world).filter((key) => key !== "fields" && key !== "sourceMap" && key !== "generatedMask"); const rootExpected = Array.isArray(manifest.rootKeys) ? manifest.rootKeys : []; if (rootActual.length !== rootExpected.length || !rootExpected.every((key) => rootActual.includes(key))) { throw new Error("Mirror root manifest is incomplete."); } if (!!manifest.hasGeneratedMask !== !!world.generatedMask) throw new Error("Mirror generatedMask manifest is incomplete."); const width = Number(world.width); const height = Number(world.height); const area = width * height; if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || !Number.isSafeInteger(area) || area <= 0) { throw new Error("Mirror dimensions are invalid."); } for (const key of manifest.fieldKeys || []) { const field = world.fields[key]; if (!ArrayBuffer.isView(field) || field.length !== area) { throw new Error(`Mirror field ${key} has invalid storage (${field?.length ?? "missing"} != ${area}).`); } } if (manifest.hasGeneratedMask && (!ArrayBuffer.isView(world.generatedMask) || world.generatedMask.length !== area)) { throw new Error(`Mirror generatedMask has invalid storage (${world.generatedMask?.length ?? "missing"} != ${area}).`); } for (const [key, childKeys] of Object.entries(manifest.expandedSourceObjects || {})) { if (!world.sourceMap[key] || !sameMirrorKeys(world.sourceMap[key], childKeys)) { throw new Error(`Mirror sourceMap.${key} manifest is incomplete.`); } } } function handleMirrorSyncMessage(incoming) { if (!isMirrorSyncMessage(incoming?.type)) return false; const type = incoming.type; const syncId = String(incoming.syncId || ""); const sequence = Number(incoming.sequence || 0); const acknowledge = (ok, error = null, extra = {}) => { self.postMessage({ id: incoming.id, type: "patch-mirror-sync-ack", syncId, sequence, stage: type, ok, error, ...extra, }); }; try { if (!syncId || !Number.isSafeInteger(sequence) || sequence <= 0) throw new Error("Invalid mirror synchronization envelope."); if (type === "patch-mirror-sync-start") { const manifest = incoming.manifest || {}; if (!Array.isArray(manifest.rootKeys) || !Array.isArray(manifest.fieldKeys) || !Array.isArray(manifest.sourceKeys)) { throw new Error("Mirror synchronization manifest is invalid."); } pendingMirrorBootstrap = { id: incoming.id, syncId, committedRevision: Number(incoming.committedRevision ?? -1), lastSequence: sequence, manifest, world: { fields: {}, sourceMap: {} }, }; acknowledge(true); return true; } const bootstrap = pendingMirrorBootstrap; if (!bootstrap || bootstrap.syncId !== syncId || bootstrap.id !== incoming.id) throw new Error("Mirror synchronization session is unavailable or stale."); if (sequence <= bootstrap.lastSequence) throw new Error(`Mirror synchronization sequence did not advance (${sequence} <= ${bootstrap.lastSequence}).`); bootstrap.lastSequence = sequence; if (type === "patch-mirror-sync-root") { const key = safeMirrorKey(incoming.key, "root key"); if (!bootstrap.manifest.rootKeys.includes(key)) throw new Error(`Unexpected mirror root key ${key}.`); defineMirrorEntry(bootstrap.world, key, incoming.value); } else if (type === "patch-mirror-sync-field") { const key = safeMirrorKey(incoming.key, "field key"); if (!bootstrap.manifest.fieldKeys.includes(key)) throw new Error(`Unexpected mirror field ${key}.`); defineMirrorEntry(bootstrap.world.fields, key, incoming.value); } else if (type === "patch-mirror-sync-generated-mask") { if (!bootstrap.manifest.hasGeneratedMask) throw new Error("Unexpected mirror generatedMask."); bootstrap.world.generatedMask = incoming.value; } else if (type === "patch-mirror-sync-source") { const key = safeMirrorKey(incoming.key, "source key"); if (!bootstrap.manifest.sourceKeys.includes(key)) throw new Error(`Unexpected mirror source key ${key}.`); if (Object.prototype.hasOwnProperty.call(bootstrap.manifest.expandedSourceObjects || {}, key)) { throw new Error(`Mirror source key ${key} must be synchronized by child entries.`); } defineMirrorEntry(bootstrap.world.sourceMap, key, incoming.value); } else if (type === "patch-mirror-sync-source-object-start") { const key = safeMirrorKey(incoming.key, "source object key"); if (!Object.prototype.hasOwnProperty.call(bootstrap.manifest.expandedSourceObjects || {}, key)) { throw new Error(`Unexpected expanded mirror source key ${key}.`); } defineMirrorEntry(bootstrap.world.sourceMap, key, {}); } else if (type === "patch-mirror-sync-source-object-entry") { const key = safeMirrorKey(incoming.key, "source object key"); const childKey = safeMirrorKey(incoming.childKey, "source child key"); const expectedChildren = bootstrap.manifest.expandedSourceObjects?.[key]; if (!Array.isArray(expectedChildren) || !expectedChildren.includes(childKey) || !bootstrap.world.sourceMap[key]) { throw new Error(`Unexpected mirror source child ${key}.${childKey}.`); } defineMirrorEntry(bootstrap.world.sourceMap[key], childKey, incoming.value); } else if (type === "patch-mirror-sync-finish") { validateCompletedMirrorBootstrap(bootstrap); persistentCommittedMirror = bootstrap.world; persistentCommittedRevision = bootstrap.committedRevision; pendingMirrorBootstrap = null; pendingApplyDeltas.clear(); acknowledge(true, null, { mirrorCommittedRevision: persistentCommittedRevision }); return true; } else if (type === "patch-mirror-sync-abort") { pendingMirrorBootstrap = null; acknowledge(true); return true; } else { throw new Error(`Unknown mirror synchronization stage ${type}.`); } acknowledge(true); } catch (error) { if (type === "patch-mirror-sync-start" || pendingMirrorBootstrap?.syncId === syncId) pendingMirrorBootstrap = null; acknowledge(false, error?.message || String(error)); } return true; } function valuesEqual(a, b) { return a === b || (Number.isNaN(a) && Number.isNaN(b)); } function exactStructuredEqual(a, b) { if (valuesEqual(a, b)) return true; if (a == null || b == null || typeof a !== "object" || typeof b !== "object") return false; if (ArrayBuffer.isView(a) || ArrayBuffer.isView(b)) { if (!ArrayBuffer.isView(a) || !ArrayBuffer.isView(b) || a.constructor !== b.constructor || a.length !== b.length) return false; for (let index = 0; index < a.length; index++) if (!valuesEqual(a[index], b[index])) return false; return true; } if (Array.isArray(a) || Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let index = 0; index < a.length; index++) if (!exactStructuredEqual(a[index], b[index])) return false; // Generated paths use one enumerable array annotation. Compare it directly // instead of allocating Object.keys arrays for every coordinate tuple. return a.patchGenerated === b.patchGenerated; } if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime(); if (a instanceof Map || b instanceof Map) { if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size) return false; for (const [key, value] of a) if (!b.has(key) || !exactStructuredEqual(value, b.get(key))) return false; return true; } if (a instanceof Set || b instanceof Set) { if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size) return false; for (const value of a) if (!b.has(value)) return false; return true; } const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; for (let index = 0; index < aKeys.length; index++) { const key = aKeys[index]; if (key !== bKeys[index] || !exactStructuredEqual(a[key], b[key])) return false; } return true; } function isDensePlainArray(value) { if (!Array.isArray(value)) return false; const keys = Object.keys(value); if (keys.length !== value.length) return false; for (let index = 0; index < keys.length; index++) if (keys[index] !== String(index)) return false; return true; } function buildExactArraySplice(base, next, { cloneValues = true } = {}) { if (!isDensePlainArray(base) || !isDensePlainArray(next)) return null; const commonLimit = Math.min(base.length, next.length); let start = 0; while (start < commonLimit && exactStructuredEqual(base[start], next[start])) start++; if (start === base.length && start === next.length) return { unchanged: true }; let suffix = 0; while (suffix < commonLimit - start && exactStructuredEqual(base[base.length - 1 - suffix], next[next.length - 1 - suffix])) suffix++; const items = next.slice(start, next.length - suffix); return { start, deleteCount: base.length - start - suffix, items: cloneValues ? structuredClone(items) : items, }; } function buildExactObjectDelta(base = {}, next = {}, { cloneValues = true } = {}) { const set = {}; const arraySplices = {}; for (const [key, value] of Object.entries(next || {})) { if (!Object.prototype.hasOwnProperty.call(base || {}, key)) { set[key] = cloneValues ? structuredClone(value) : value; continue; } if (isDensePlainArray(base[key]) && isDensePlainArray(value)) { const splice = buildExactArraySplice(base[key], value, { cloneValues }); if (splice && !splice.unchanged) arraySplices[key] = splice; continue; } if (!exactStructuredEqual(base[key], value)) set[key] = cloneValues ? structuredClone(value) : value; } const removed = Object.keys(base || {}).filter((key) => !Object.prototype.hasOwnProperty.call(next || {}, key)); return { set, arraySplices, removed }; } function buildTypedRowDelta(base, next, width) { if (!ArrayBuffer.isView(next)) return null; if (!ArrayBuffer.isView(base) || base.constructor?.name !== next.constructor?.name || base.length !== next.length) { return { constructorName: next.constructor.name, length: next.length, replace: new next.constructor(next) }; } const rows = []; const rowWidth = Math.max(1, Number(width || next.length)); for (let rowStart = 0; rowStart < next.length; rowStart += rowWidth) { const rowEnd = Math.min(next.length, rowStart + rowWidth); let first = -1; let last = -1; for (let index = rowStart; index < rowEnd; index++) { if (valuesEqual(base[index], next[index])) continue; if (first < 0) first = index; last = index; } if (first >= 0) rows.push({ start: first, values: new next.constructor(next.subarray(first, last + 1)) }); } return rows.length ? { constructorName: next.constructor.name, length: next.length, rows } : null; } export function buildCommittedMirrorDelta(baseWorld, nextWorld) { if (!baseWorld || !nextWorld) throw new Error("Cannot build a committed mirror delta without both worlds."); const fieldNames = new Set([...Object.keys(baseWorld.fields || {}), ...Object.keys(nextWorld.fields || {})]); const fields = {}; for (const name of fieldNames) { const next = nextWorld.fields?.[name]; if (!next) { fields[name] = { remove: true }; continue; } const delta = buildTypedRowDelta(baseWorld.fields?.[name], next, nextWorld.width); if (delta) fields[name] = delta; } const generatedMask = buildTypedRowDelta(baseWorld.generatedMask, nextWorld.generatedMask, nextWorld.width); const baseMeta = {}; const nextMeta = {}; for (const [key, value] of Object.entries(baseWorld)) { if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") baseMeta[key] = value; } for (const [key, value] of Object.entries(nextWorld)) { if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") nextMeta[key] = value; } const exactDeltas = structuredClone({ sourceMapDelta: buildExactObjectDelta(baseWorld.sourceMap || {}, nextWorld.sourceMap || {}, { cloneValues: false }), metaDelta: buildExactObjectDelta(baseMeta, nextMeta, { cloneValues: false }), }); return { width: nextWorld.width, height: nextWorld.height, fields, generatedMask, sourceMapDelta: exactDeltas.sourceMapDelta, metaDelta: exactDeltas.metaDelta, }; } export function buildMainThreadTransferDelta(delta) { if (!delta) return null; // Raster row buffers are transferred to the main thread and therefore need // a Worker-side copy so the retained Apply-ACK delta is not detached. Plain // metadata is already cloned once by postMessage; cloning that graph here as // well only doubled paths, points, histories and diagnostics at peak memory. const raster = structuredClone({ fields: delta.fields || {}, generatedMask: delta.generatedMask || null, }); return { ...delta, fields: raster.fields, generatedMask: raster.generatedMask, }; } function markPreviewChange(changeTracker, worldIndex, category = 0) { if (!changeTracker?.mask) return; const width = changeTracker.worldWidth; const x = worldIndex % width; const y = Math.floor(worldIndex / width); const rect = changeTracker.rect; if (x < rect.x0 || y < rect.y0 || x >= rect.x1 || y >= rect.y1) return; const localIndex = (y - rect.y0) * changeTracker.width + (x - rect.x0); changeTracker.mask[localIndex] |= 1 | category; } function markAllPreviewChanges(changeTracker, category = 0) { if (!changeTracker?.mask) return; const flags = 1 | category; changeTracker.mask.fill(flags); } function buildTypedRowDeltaFromTransaction(snapshot, name, next, changeTracker = null) { if (!ArrayBuffer.isView(next)) return null; const baseRef = snapshot?.fieldRefs?.get(name); if (!ArrayBuffer.isView(baseRef) || baseRef.constructor?.name !== next.constructor?.name || baseRef.length !== next.length) { const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0; markAllPreviewChanges(changeTracker, category); return { constructorName: next.constructor.name, length: next.length, replace: new next.constructor(next) }; } const localEntry = snapshot?.fields?.get(name) || null; const rect = snapshot?.fieldRect || null; const width = Math.max(1, Number(snapshot?.width || next.length)); const rows = []; const scanLocalRect = localEntry && rect; const startY = scanLocalRect ? Math.max(0, rect.y0) : 0; const endY = scanLocalRect ? Math.min(snapshot.height, rect.y1) : Math.ceil(next.length / width); for (let y = startY; y < endY; y++) { const rowStart = y * width; const rowEnd = Math.min(next.length, rowStart + width); const scanStart = scanLocalRect ? Math.max(rowStart, rowStart + rect.x0) : rowStart; const scanEnd = scanLocalRect ? Math.min(rowEnd, rowStart + rect.x1) : rowEnd; let first = -1; let last = -1; for (let index = scanStart; index < scanEnd; index++) { const x = index - rowStart; let oldValue = baseRef[index]; if (localEntry && rect && x >= rect.x0 && x < rect.x1 && y >= rect.y0 && y < rect.y1) { oldValue = localEntry.data[(y - rect.y0) * snapshot.fieldWidth + (x - rect.x0)]; } if (valuesEqual(oldValue, next[index])) continue; const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0; markPreviewChange(changeTracker, index, category); if (first < 0) first = index; last = index; } if (first >= 0) rows.push({ start: first, values: new next.constructor(next.subarray(first, last + 1)) }); } return rows.length ? { constructorName: next.constructor.name, length: next.length, rows } : null; } export function buildCommittedMirrorDeltaFromTransaction(snapshot, nextWorld) { if (!snapshot || snapshot.lightweight || !nextWorld) throw new Error("A full patch transaction snapshot is required."); const fieldNames = new Set([...(snapshot.fieldNames || []), ...Object.keys(nextWorld.fields || {})]); const fields = {}; const snapshotRect = snapshot.fieldRect || { x0: 0, y0: 0, x1: nextWorld.width, y1: nextWorld.height }; const changeRect = { x0: Math.max(0, Math.floor(snapshotRect.x0 || 0)), y0: Math.max(0, Math.floor(snapshotRect.y0 || 0)), x1: Math.min(nextWorld.width, Math.ceil(snapshotRect.x1 || 0)), y1: Math.min(nextWorld.height, Math.ceil(snapshotRect.y1 || 0)), }; const changeTracker = { rect: changeRect, width: Math.max(0, changeRect.x1 - changeRect.x0), worldWidth: nextWorld.width, mask: null, }; changeTracker.mask = new Uint8Array(changeTracker.width * Math.max(0, changeRect.y1 - changeRect.y0)); for (const name of fieldNames) { const next = nextWorld.fields?.[name]; if (!next) { if (snapshot.fieldNames?.has(name)) { fields[name] = { remove: true }; const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0; markAllPreviewChanges(changeTracker, category); } continue; } const delta = buildTypedRowDeltaFromTransaction(snapshot, name, next, changeTracker); if (delta) fields[name] = delta; } const baseMeta = {}; const nextMeta = {}; for (const [key, value] of Object.entries(nextWorld)) { if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") { baseMeta[key] = value; nextMeta[key] = value; } } Object.assign(baseMeta, { width: snapshot.width, height: snapshot.height, originX: snapshot.originX, originY: snapshot.originY, sourceWidth: snapshot.sourceWidth, sourceHeight: snapshot.sourceHeight, generatedRects: snapshot.generatedRects, lastPatchResult: snapshot.lastPatchResult, patchGenerationSerial: snapshot.patchGenerationSerial, seaLevel: snapshot.seaLevel, }); const rawSourceMapDelta = buildExactObjectDelta(snapshot.sourceMap || {}, nextWorld.sourceMap || {}, { cloneValues: false }); const rawMetaDelta = buildExactObjectDelta(baseMeta, nextMeta, { cloneValues: false }); // Transaction restore replaces world/sourceMap roots; it does not mutate the // accepted arrays and diagnostic objects referenced by these deltas. Adopt // that completed graph directly. Cloning it here duplicated all changed // metadata immediately before rollback and the later postMessage clone. const exactDeltas = { sourceMapDelta: rawSourceMapDelta, metaDelta: rawMetaDelta }; let changedCells = 0; let terrainChangedCells = 0; let adminChangedCells = 0; for (const flags of changeTracker.mask) { if (flags & 1) changedCells++; if (flags & 2) terrainChangedCells++; if (flags & 4) adminChangedCells++; } const changedSourceKeys = new Set([ ...Object.keys(rawSourceMapDelta.set || {}), ...Object.keys(rawSourceMapDelta.arraySplices || {}), ...(rawSourceMapDelta.removed || []), ]); let featureLayersChanged = 0; for (const key of PREVIEW_FEATURE_KEYS) if (changedSourceKeys.has(key)) featureLayersChanged++; return { width: nextWorld.width, height: nextWorld.height, fields, generatedMask: buildTypedRowDelta( snapshot.generatedMask || snapshot.generatedMaskRef, nextWorld.generatedMask, nextWorld.width ), sourceMapDelta: exactDeltas.sourceMapDelta, metaDelta: exactDeltas.metaDelta, previewDelta: { changedCells, terrainChangedCells, adminChangedCells, featureLayersChanged, identical: changedCells === 0 && featureLayersChanged === 0, }, }; } function nowMs() { return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now(); } function compactQuality(quality) { if (!quality || typeof quality !== "object") return null; return { hardPass: quality.hardPass !== false, score: Number(quality.score || 0), selectedVariant: Number.isFinite(quality.selectedVariant) ? quality.selectedVariant >>> 0 : null, terrainType: quality.terrainType || quality.terrain?.terrainType || null, terrain: quality.terrain ? { hardPass: quality.terrain.hardPass !== false, landRatio: Number(quality.terrain.landRatio || 0), developableRatio: Number(quality.terrain.developableRatio || 0), largestComponentRatio: Number(quality.terrain.largestComponentRatio || 0), } : null, human: quality.human ? { hardPass: quality.human.hardPass !== false, settlementCount: Number(quality.human.settlementCount || 0), labelCount: Number(quality.human.labelCount || 0), roadPaths: Number(quality.human.roadPaths || 0), railPaths: Number(quality.human.railPaths || 0), } : null, finalMerge: quality.finalMerge ? { hardPass: quality.finalMerge.hardPass !== false, ownedCells: Number(quality.finalMerge.ownedCells || 0), ownedLandCells: Number(quality.finalMerge.ownedLandCells || 0), ownedLandRatio: Number(quality.finalMerge.ownedLandRatio || 0), landFloor: Number(quality.finalMerge.landFloor || 0), settlementCount: Number(quality.finalMerge.settlementCount || 0), minFinalSettlements: Number(quality.finalMerge.minFinalSettlements || 0), preMergeSettlementCount: Number.isFinite(quality.finalMerge.preMergeSettlementCount) ? Number(quality.finalMerge.preMergeSettlementCount) : null, candidateMinimumSettlements: Number(quality.finalMerge.candidateMinimumSettlements || 0), labelCount: Number(quality.finalMerge.labelCount || 0), minFinalLabels: Number(quality.finalMerge.minFinalLabels || 0), preMergeLabelCount: Number.isFinite(quality.finalMerge.preMergeLabelCount) ? Number(quality.finalMerge.preMergeLabelCount) : null, candidateMinimumLabels: Number(quality.finalMerge.candidateMinimumLabels || 0), adminCenters: Number(quality.finalMerge.counts?.adminCenters || 0), minFinalAdminCenters: Number(quality.finalMerge.minFinalAdminCenters || 0), transportRequired: quality.finalMerge.transportRequired === true, roadPaths: Number(quality.finalMerge.roadPaths || 0), railPaths: Number(quality.finalMerge.railPaths || 0), rectangularCoastHardPass: quality.finalMerge.rectangularCoastHardPass !== false, rectangularCoastRun: Number(quality.finalMerge.rectangularCoastCut?.maxAxisAlignedRun || 0), rectangularCoastLongestRun: quality.finalMerge.rectangularCoastCut?.longestRun ? { ...quality.finalMerge.rectangularCoastCut.longestRun } : null, rectangularCoastCutScope: quality.finalMerge.rectangularCoastCutScope || null, score: Number(quality.finalMerge.score || 0), } : null, }; } function compactSeam(diagnostics) { if (!diagnostics || typeof diagnostics !== "object") return null; return { hardPass: diagnostics.hardPass !== false, status: diagnostics.status || null, gateReasons: [...(diagnostics.gateReasons || [])], roadPortalsBroken: Number(diagnostics.roadPortalsBroken || 0), railPortalsBroken: Number(diagnostics.railPortalsBroken || 0), prefectureSeamBreakEdges: Number(diagnostics.prefectureSeamBreakEdges || 0), adminSeamBreakEdges: Number(diagnostics.adminSeamBreakEdges || 0), landToSeaCells: Number(diagnostics.landToSeaCells || 0), transportLandToSeaConflicts: Number(diagnostics.transportLandToSeaConflicts || 0), duplicateBoundaryPairs: Number(diagnostics.duplicateBoundaryPairs || 0), expansionFootprintEscapedCells: Number(diagnostics.expansionFootprintEscapedCells || 0), maxEstablishedFrontierElevationJump: Number(diagnostics.maxEstablishedFrontierElevationJump || 0), humanTerrainReconciliation: diagnostics.humanTerrainReconciliation ? { checked: Number(diagnostics.humanTerrainReconciliation.checked || 0), alreadyValid: Number(diagnostics.humanTerrainReconciliation.alreadyValid || 0), relocated: Number(diagnostics.humanTerrainReconciliation.relocated || 0), dropped: Number(diagnostics.humanTerrainReconciliation.dropped || 0), stagedOutsideOwnership: Number(diagnostics.humanTerrainReconciliation.stagedOutsideOwnership || 0), byLayer: diagnostics.humanTerrainReconciliation.byLayer ? Object.fromEntries(Object.entries(diagnostics.humanTerrainReconciliation.byLayer).map(([key, value]) => [key, { checked: Number(value?.checked || 0), alreadyValid: Number(value?.alreadyValid || 0), relocated: Number(value?.relocated || 0), dropped: Number(value?.dropped || 0), stagedOutsideOwnership: Number(value?.stagedOutsideOwnership || 0), }])) : {}, } : null, }; } function isInvariantFailure(result) { if (result?.tileResult && isInvariantFailure(result.tileResult)) return true; const code = String(result?.code || ""); const reasons = result?.seamDiagnostics?.gateReasons || []; return code === "patch-candidate-coverage-incomplete" || code === "patch-generated-footprint-write-escape" || code === "patch-invariant-breach" || reasons.includes("generated-footprint-write-escape") || Number(result?.candidateUnmappedActiveCells || 0) > 0; } function isContentRejection(result) { const code = String(result?.code || ""); if (isInvariantFailure(result)) return false; if ((code === "patch-large-tile-failed" || code === "patch-large-regeneration-tile-failed") && result?.tileResult) { return isContentRejection(result.tileResult); } return code === "patch-quality-gate-failed" || code === "patch-seam-gate-failed" || code === "patch-large-final-seam-failed"; } function normalizeCandidateResult(result) { if (!result?.ok || result?.seamDiagnostics?.hardPass !== false) return result; return { ...result, ok: false, code: "patch-seam-gate-failed", reason: `Candidate failed final seam continuity (${(result.seamDiagnostics.gateReasons || []).join(", ") || "unknown seam failure"}).`, rolledBack: true, }; } function summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt = 1) { return { candidateId: candidate.candidateId || `${candidate.variant >>> 0}:${candidate.seed >>> 0}`, candidateOrdinal, executionAttempt: Math.max(1, Number(executionAttempt || 1)), variant: candidate.variant >>> 0, seed: candidate.seed >>> 0, status, ok: result?.ok === true, code: result?.code || null, reason: result?.reason || null, patchMode: result?.patchMode || null, tileCount: Number(result?.tileCount || 0), wallMs: Math.round(wallMs * 10) / 10, candidateQuality: compactQuality(result?.candidateQuality), seamDiagnostics: compactSeam(result?.seamDiagnostics), patchTimings: (result?.patchTimings || []).map((entry) => ({ key: entry.key || null, label: entry.label || entry.key || "Stage", ms: Number(entry.ms || 0), })), }; } const PREVIEW_TERRAIN_FIELDS = new Set(["elevation", "slope", "sea", "landMask", "plain", "landuse", "populationDensity"]); const PREVIEW_ADMIN_FIELDS = new Set(["adminId", "municipalityId", "prefectureRegionId", "prefectureMask", "humanRegionMask"]); const PREVIEW_FEATURE_KEYS = [ "villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations", "interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters", "externalGateways", "prefectureRegions", "premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads", "railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways", "mainRivers", "tributaryRivers", "smallStreams", "prefectureBorder", "regionalPrefectureBorders", "adminBorders", ]; function computePreviewDelta(baseWorld, previewWorld, rectLike, onProgress = () => {}) { const rect = rectLike?.transportReachRect || rectLike?.repairRect || rectLike?.writeRect || rectLike || null; if (!baseWorld || !previewWorld || !rect) return null; const x0 = Math.max(0, Math.floor(rect.x0 || 0)); const y0 = Math.max(0, Math.floor(rect.y0 || 0)); const x1 = Math.min(Math.max(baseWorld.width || 0, previewWorld.width || 0), Math.ceil(rect.x1 || 0)); const y1 = Math.min(Math.max(baseWorld.height || 0, previewWorld.height || 0), Math.ceil(rect.y1 || 0)); const fieldNames = new Set([...Object.keys(baseWorld.fields || {}), ...Object.keys(previewWorld.fields || {})]); let changedCells = 0; let terrainChangedCells = 0; let adminChangedCells = 0; const totalRows = Math.max(0, y1 - y0) + PREVIEW_FEATURE_KEYS.length; for (let y = y0; y < y1; y++) { for (let x = x0; x < x1; x++) { const bi = x >= 0 && y >= 0 && x < baseWorld.width && y < baseWorld.height ? y * baseWorld.width + x : -1; const pi = x >= 0 && y >= 0 && x < previewWorld.width && y < previewWorld.height ? y * previewWorld.width + x : -1; if (bi < 0 || pi < 0) continue; let terrainChanged = false; let adminChanged = false; let cellChanged = false; for (const name of fieldNames) { const a = baseWorld.fields?.[name]?.[bi]; const b = previewWorld.fields?.[name]?.[pi]; if (a === b || (Number.isNaN(a) && Number.isNaN(b))) continue; cellChanged = true; if (PREVIEW_TERRAIN_FIELDS.has(name)) terrainChanged = true; if (PREVIEW_ADMIN_FIELDS.has(name)) adminChanged = true; } if (cellChanged) changedCells++; if (terrainChanged) terrainChangedCells++; if (adminChanged) adminChangedCells++; } if ((y - y0) % 16 === 15 || y + 1 === y1) onProgress(y - y0 + 1, totalRows, "raster"); } let featureLayersChanged = 0; for (let index = 0; index < PREVIEW_FEATURE_KEYS.length; index++) { const key = PREVIEW_FEATURE_KEYS[index]; if (!exactStructuredEqual(baseWorld.sourceMap?.[key] || [], previewWorld.sourceMap?.[key] || [])) featureLayersChanged++; onProgress(Math.max(0, y1 - y0) + index + 1, totalRows, "features"); } return { changedCells, terrainChangedCells, adminChangedCells, featureLayersChanged, identical: changedCells === 0 && featureLayersChanged === 0, }; } export function runPatchCandidateSearch(message, dependencies = {}) { const { id, world, rect, options, search } = message || {}; const cloneWorld = dependencies.cloneWorld || ((value) => structuredClone(value)); const generateCandidate = dependencies.generateCandidate || generatePatch; const clock = dependencies.now || nowMs; const publishProgress = dependencies.onProgress || (() => {}); const transactional = dependencies.transactional === true; const candidatePlan = Array.isArray(search?.candidatePlan) && search.candidatePlan.length ? search.candidatePlan : [{ variant: options?.variant || 0, seed: options?.seed || world?.seed || 0 }]; const candidateCount = Math.max(candidatePlan.length, Number(search?.totalCandidateCount || candidatePlan.length)); const searchId = search?.searchId || `patch-${id}`; const workerEpoch = Number(search?.workerEpoch || 0); const executionAttempt = Math.max(1, Number(search?.executionAttempt || 1)); let eventSeq = 0; let phaseOrdinal = 0; let currentPhase = null; const workUnits = new Map(); const emitProgress = (progress = {}, candidateOrdinal = 0) => { const phase = String(progress.phase || progress.key || "patch"); if (phase !== currentPhase) { currentPhase = phase; phaseOrdinal++; } const hasCompleted = progress.completed != null; const hasTotal = progress.total != null; const completed = Number(progress.completed); const total = Number(progress.total); const boundedWork = hasCompleted || hasTotal; const explicitWorkUnitId = progress.workUnitId != null && String(progress.workUnitId).length > 0; const workUnitId = String(explicitWorkUnitId ? progress.workUnitId : (progress.key || phase)); if (boundedWork) { if (!hasCompleted || !hasTotal || !Number.isFinite(completed) || !Number.isFinite(total)) { const error = new Error(`Incomplete bounded progress for ${phase}/${workUnitId}: ${String(progress.completed)}/${String(progress.total)}`); error.code = "worker-progress-invariant"; throw error; } if (!explicitWorkUnitId) { const error = new Error(`Bounded progress is missing an explicit workUnitId for ${phase}/${workUnitId}.`); error.code = "worker-progress-invariant"; throw error; } if (completed < 0 || total < 0 || completed > total) { const error = new Error(`Invalid progress bounds for ${phase}/${workUnitId}: ${completed}/${total}`); error.code = "worker-progress-invariant"; throw error; } // workUnitId is globally unique within one candidate-search operation. // Do not reset its monotonicity merely because a display phase changed in // between heartbeats; that would hide real restarts/runaway producers. const unitKey = workUnitId; const previous = workUnits.get(unitKey); if (previous && (total !== previous.total || completed < previous.completed)) { const error = new Error(`Non-monotonic progress for ${phase}/${workUnitId}: ${completed}/${total} after ${previous.completed}/${previous.total}`); error.code = "worker-progress-invariant"; throw error; } const workerNow = nowMs(); const startedAtWorker = previous?.startedAtWorker ?? workerNow; const lastAdvancedAtWorker = !previous || completed > previous.completed ? workerNow : (previous.lastAdvancedAtWorker ?? startedAtWorker); workUnits.set(unitKey, { completed, total, phase, startedAtWorker, lastAdvancedAtWorker }); } eventSeq++; publishProgress({ id, type: "progress", progress: { ...progress, searchId, operationId: search?.operationId || searchId, candidateOrdinal, candidateCount, executionAttempt, workerEpoch, committedRevision: Number(search?.committedRevision || 0), eventSeq, counter: eventSeq, phase, phaseOrdinal, workUnitId, boundedWork, startedAtWorker: boundedWork ? workUnits.get(workUnitId)?.startedAtWorker : undefined, lastAdvancedAtWorker: boundedWork ? workUnits.get(workUnitId)?.lastAdvancedAtWorker : undefined, cooperative: progress.nonCooperative !== true, }, }); }; try { const attempts = []; let terminalResult = null; let successWorld = null; let successDelta = null; let successTargetHash = null; emitProgress({ status: "start", key: "search", phase: "search", workUnitId: "candidate-search", label: `Searching ${candidateCount} complete candidate${candidateCount === 1 ? "" : "s"}`, completed: 0, total: candidateCount }); for (let index = 0; index < candidatePlan.length; index++) { const candidate = candidatePlan[index]; const candidateOrdinal = Math.max(1, Number(candidate.candidateOrdinal || index + 1)); emitProgress({ status: "start", key: transactional ? "candidate-transaction" : "candidate-clone", phase: transactional ? "candidate-transaction" : "candidate-clone", label: transactional ? `Candidate ${candidateOrdinal}/${candidateCount}: capturing exact rollback state` : `Candidate ${candidateOrdinal}/${candidateCount}: preparing immutable baseline`, variant: candidate.variant >>> 0, workUnitId: "candidate-search", // candidateOrdinal is global across the full search, whereas `index` is // local to this Worker execution. Recovery can restart with candidates // 2..N after candidate 1 was already rejected, so using `index` here // made the shared search work unit move backward (2/3 -> 1/3). completed: Math.max(0, candidateOrdinal - 1), total: candidateCount, nonCooperative: true, }, candidateOrdinal); let candidateWorld; let transaction = null; try { if (transactional) { transaction = capturePatchTransactionSnapshot(world, { lightweight: false, isolateSourceMap: true, copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration", }); candidateWorld = world; } else { candidateWorld = cloneWorld(world); } } catch (error) { const failed = { ok: false, code: transactional ? "candidate-transaction-failed" : "candidate-clone-failed", reason: error?.message || String(error), }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, 0, "infrastructure-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0, }; break; } const startedAt = clock(); let result; try { result = generateCandidate(candidateWorld, rect, { ...(options || {}), seed: candidate.seed >>> 0, variant: candidate.variant >>> 0, maxQualityRetries: 0, // The production Worker mutates its committed mirror transactionally // and restores it after extracting the accepted delta. Direct callers // retain the immutable-clone reference path for independent tests. _workerOwnedPreview: !transactional, _externalTransactionSnapshot: transaction, onProgress: (progress) => { // Producer workUnitIds are invocation-local. The same complete // pipeline is executed again when a content-rejected candidate // advances to the next Variant, so namespace every explicit inner // unit by candidate ordinal before the search-wide monotonicity // validator sees it. Do not invent an ID for a bounded producer // that omitted one; emitProgress must still reject that protocol // violation instead of masking it. const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0 ? String(progress.workUnitId) : null; emitProgress({ ...progress, workUnitId: rawWorkUnitId ? `candidate-${candidateOrdinal}/${rawWorkUnitId}` : progress?.workUnitId, variant: candidate.variant >>> 0, seed: candidate.seed >>> 0, }, candidateOrdinal); }, }); result = normalizeCandidateResult(result); } catch (error) { if (transaction) restorePatchTransactionSnapshot(world, transaction); if (error?.code === "worker-progress-invariant") throw error; const wallMs = clock() - startedAt; const failed = { ok: false, code: "candidate-execution-error", reason: error?.message || String(error), stack: error?.stack || "", }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "execution-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "execution-error", nextVariant: candidate.variant >>> 0 }; break; } const wallMs = clock() - startedAt; if (result?.ok) { if (transaction) { try { successDelta = buildCommittedMirrorDeltaFromTransaction(transaction, candidateWorld); successTargetHash = hashCommittedWorld(candidateWorld); } catch (error) { restorePatchTransactionSnapshot(world, transaction); const failed = { ok: false, code: "candidate-delta-build-failed", reason: error?.message || String(error), }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "infrastructure-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0 }; break; } restorePatchTransactionSnapshot(world, transaction); } attempts.push(summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "success", executionAttempt)); result.searchAttempts = attempts; result.searchStatus = "succeeded"; result.candidateOrdinal = candidateOrdinal; result.candidateCount = candidateCount; result.actualVariant = candidate.variant >>> 0; result.actualSeed = candidate.seed >>> 0; result.nextVariant = ((candidate.variant >>> 0) + 1) >>> 0; if (successDelta?.previewDelta) result.previewDelta = { ...successDelta.previewDelta }; terminalResult = result; successWorld = transactional ? null : candidateWorld; emitProgress({ status: "done", key: "search", phase: "search", workUnitId: "candidate-search", label: `Candidate ${candidateOrdinal}/${candidateCount} accepted`, variant: candidate.variant >>> 0, completed: candidateOrdinal, total: candidateCount }, candidateOrdinal); break; } const invariant = isInvariantFailure(result); const contentRejected = isContentRejection(result); const status = invariant ? "invariant-breach" : contentRejected ? "rejected" : "failed"; const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt); attempts.push(attemptSummary); if (transaction) restorePatchTransactionSnapshot(world, transaction); emitProgress({ status: contentRejected ? "rejected" : "error", key: contentRejected ? "candidate-rejected" : "candidate-failed", phase: "candidate-result", label: contentRejected ? `Candidate ${candidateOrdinal}/${candidateCount} rejected; searching the next complete candidate` : `Candidate ${candidateOrdinal}/${candidateCount} stopped: ${result?.reason || result?.code || "failure"}`, variant: candidate.variant >>> 0, workUnitId: "candidate-search", completed: candidateOrdinal, total: candidateCount, code: result?.code || null, attemptSummary, }, candidateOrdinal); if (!contentRejected) { terminalResult = { ...(result || { ok: false }), searchAttempts: attempts, searchStatus: invariant ? "invariant-breach" : "failed", nextVariant: candidate.variant >>> 0, }; break; } // The rejected full world is no longer observable. Drop the last strong // references before cloning the next Variant so the browser may reclaim // its field buffers instead of retaining multiple complete candidates. candidateWorld = null; transaction = null; result = null; } if (!terminalResult) { const last = candidatePlan[candidatePlan.length - 1]; terminalResult = { ok: false, code: "patch-search-exhausted", reason: `All ${candidateCount} complete production candidates were rejected.`, searchStatus: "exhausted", searchAttempts: attempts, nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0, candidateCount, }; emitProgress({ status: "done", key: "search-exhausted", phase: "search", workUnitId: "candidate-search", label: terminalResult.reason, completed: candidateCount, total: candidateCount }); } if (successWorld && terminalResult?.ok) { emitProgress({ status: "start", key: "preview-delta", phase: "preview-delta", label: "Auditing preview changes", }, terminalResult.candidateOrdinal || 0); terminalResult.previewDelta = computePreviewDelta(world, successWorld, terminalResult.rects || rect, (completed, total, part) => { emitProgress({ status: "step", key: `preview-delta:${part}`, phase: "preview-delta", label: part === "raster" ? "Auditing preview raster changes" : "Auditing preview feature changes", workUnitId: "preview-delta-audit", completed, total, }, terminalResult.candidateOrdinal || 0); }); emitProgress({ status: "done", key: "preview-delta", phase: "preview-delta", label: "Preview change audit complete", }, terminalResult.candidateOrdinal || 0); } // Return only the accepted candidate world. Rejected candidate clones and // the immutable worker baseline remain worker-local and become collectible. return { id, ok: true, world: successWorld, transactionDelta: successDelta, targetHash: successTargetHash, result: terminalResult, searchId, workerEpoch, eventSeq, }; } catch (error) { return { id, ok: false, code: error?.code || "candidate-search-error", error: error?.message || String(error), stack: error?.stack || "", searchId, workerEpoch, eventSeq }; } } export async function runPatchCandidateSearchAsync(message, dependencies = {}) { const { id, world, rect, options, search } = message || {}; const cloneWorld = dependencies.cloneWorld || ((value) => structuredClone(value)); const generateCandidate = dependencies.generateCandidate || generatePatchAsync; const clock = dependencies.now || nowMs; const publishProgress = dependencies.onProgress || (() => {}); const transactional = dependencies.transactional === true; const candidatePlan = Array.isArray(search?.candidatePlan) && search.candidatePlan.length ? search.candidatePlan : [{ variant: options?.variant || 0, seed: options?.seed || world?.seed || 0 }]; const candidateCount = Math.max(candidatePlan.length, Number(search?.totalCandidateCount || candidatePlan.length)); const searchId = search?.searchId || `patch-${id}`; const workerEpoch = Number(search?.workerEpoch || 0); const executionAttempt = Math.max(1, Number(search?.executionAttempt || 1)); let eventSeq = 0; let phaseOrdinal = 0; let currentPhase = null; const workUnits = new Map(); const emitProgress = (progress = {}, candidateOrdinal = 0) => { const phase = String(progress.phase || progress.key || "patch"); if (phase !== currentPhase) { currentPhase = phase; phaseOrdinal++; } const hasCompleted = progress.completed != null; const hasTotal = progress.total != null; const completed = Number(progress.completed); const total = Number(progress.total); const boundedWork = hasCompleted || hasTotal; const explicitWorkUnitId = progress.workUnitId != null && String(progress.workUnitId).length > 0; const workUnitId = String(explicitWorkUnitId ? progress.workUnitId : (progress.key || phase)); if (boundedWork) { if (!hasCompleted || !hasTotal || !Number.isFinite(completed) || !Number.isFinite(total)) { const error = new Error(`Incomplete bounded progress for ${phase}/${workUnitId}: ${String(progress.completed)}/${String(progress.total)}`); error.code = "worker-progress-invariant"; throw error; } if (!explicitWorkUnitId) { const error = new Error(`Bounded progress is missing an explicit workUnitId for ${phase}/${workUnitId}.`); error.code = "worker-progress-invariant"; throw error; } if (completed < 0 || total < 0 || completed > total) { const error = new Error(`Invalid progress bounds for ${phase}/${workUnitId}: ${completed}/${total}`); error.code = "worker-progress-invariant"; throw error; } // workUnitId is globally unique within one candidate-search operation. // Do not reset its monotonicity merely because a display phase changed in // between heartbeats; that would hide real restarts/runaway producers. const unitKey = workUnitId; const previous = workUnits.get(unitKey); if (previous && (total !== previous.total || completed < previous.completed)) { const error = new Error(`Non-monotonic progress for ${phase}/${workUnitId}: ${completed}/${total} after ${previous.completed}/${previous.total}`); error.code = "worker-progress-invariant"; throw error; } const workerNow = nowMs(); const startedAtWorker = previous?.startedAtWorker ?? workerNow; const lastAdvancedAtWorker = !previous || completed > previous.completed ? workerNow : (previous.lastAdvancedAtWorker ?? startedAtWorker); workUnits.set(unitKey, { completed, total, phase, startedAtWorker, lastAdvancedAtWorker }); } eventSeq++; publishProgress({ id, type: "progress", progress: { ...progress, searchId, operationId: search?.operationId || searchId, candidateOrdinal, candidateCount, executionAttempt, workerEpoch, committedRevision: Number(search?.committedRevision || 0), eventSeq, counter: eventSeq, phase, phaseOrdinal, workUnitId, boundedWork, startedAtWorker: boundedWork ? workUnits.get(workUnitId)?.startedAtWorker : undefined, lastAdvancedAtWorker: boundedWork ? workUnits.get(workUnitId)?.lastAdvancedAtWorker : undefined, cooperative: progress.nonCooperative !== true, }, }); }; try { const attempts = []; let terminalResult = null; let successWorld = null; let successDelta = null; let successTargetHash = null; emitProgress({ status: "start", key: "search", phase: "search", workUnitId: "candidate-search", label: `Searching ${candidateCount} complete candidate${candidateCount === 1 ? "" : "s"}`, completed: 0, total: candidateCount }); for (let index = 0; index < candidatePlan.length; index++) { const candidate = candidatePlan[index]; const candidateOrdinal = Math.max(1, Number(candidate.candidateOrdinal || index + 1)); emitProgress({ status: "start", key: transactional ? "candidate-transaction" : "candidate-clone", phase: transactional ? "candidate-transaction" : "candidate-clone", label: transactional ? `Candidate ${candidateOrdinal}/${candidateCount}: capturing exact rollback state` : `Candidate ${candidateOrdinal}/${candidateCount}: preparing immutable baseline`, variant: candidate.variant >>> 0, workUnitId: "candidate-search", // candidateOrdinal is global across the full search, whereas `index` is // local to this Worker execution. Recovery can restart with candidates // 2..N after candidate 1 was already rejected, so using `index` here // made the shared search work unit move backward (2/3 -> 1/3). completed: Math.max(0, candidateOrdinal - 1), total: candidateCount, nonCooperative: true, }, candidateOrdinal); let candidateWorld; let transaction = null; try { if (transactional) { transaction = capturePatchTransactionSnapshot(world, { lightweight: false, isolateSourceMap: true, copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration", }); candidateWorld = world; } else { candidateWorld = cloneWorld(world); } } catch (error) { const failed = { ok: false, code: transactional ? "candidate-transaction-failed" : "candidate-clone-failed", reason: error?.message || String(error), }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, 0, "infrastructure-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0, }; break; } const startedAt = clock(); let result; try { result = await generateCandidate(candidateWorld, rect, { ...(options || {}), seed: candidate.seed >>> 0, variant: candidate.variant >>> 0, maxQualityRetries: 0, // The production Worker mutates its committed mirror transactionally // and restores it after extracting the accepted delta. Direct callers // retain the immutable-clone reference path for independent tests. _workerOwnedPreview: !transactional, _externalTransactionSnapshot: transaction, _precomputeRawCandidateBatch: dependencies.precomputeRawCandidateBatch, _precomputeRawCandidateSequence: dependencies.precomputeRawCandidateSequence, _rawCandidateParallelism: Math.max(1, Math.min(2, Number(dependencies.rawCandidateParallelism || 2))), onProgress: (progress) => { // Producer workUnitIds are invocation-local. The same complete // pipeline is executed again when a content-rejected candidate // advances to the next Variant, so namespace every explicit inner // unit by candidate ordinal before the search-wide monotonicity // validator sees it. Do not invent an ID for a bounded producer // that omitted one; emitProgress must still reject that protocol // violation instead of masking it. const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0 ? String(progress.workUnitId) : null; emitProgress({ ...progress, workUnitId: rawWorkUnitId ? `candidate-${candidateOrdinal}/${rawWorkUnitId}` : progress?.workUnitId, variant: candidate.variant >>> 0, seed: candidate.seed >>> 0, }, candidateOrdinal); }, }); result = normalizeCandidateResult(result); } catch (error) { if (transaction) restorePatchTransactionSnapshot(world, transaction); if (error?.code === "worker-progress-invariant") throw error; const wallMs = clock() - startedAt; const failed = { ok: false, code: "candidate-execution-error", reason: error?.message || String(error), stack: error?.stack || "", }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "execution-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "execution-error", nextVariant: candidate.variant >>> 0 }; break; } const wallMs = clock() - startedAt; if (result?.ok) { if (transaction) { try { successDelta = buildCommittedMirrorDeltaFromTransaction(transaction, candidateWorld); successTargetHash = hashCommittedWorld(candidateWorld); } catch (error) { restorePatchTransactionSnapshot(world, transaction); const failed = { ok: false, code: "candidate-delta-build-failed", reason: error?.message || String(error), }; attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "infrastructure-error", executionAttempt)); terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0 }; break; } restorePatchTransactionSnapshot(world, transaction); } attempts.push(summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "success", executionAttempt)); result.searchAttempts = attempts; result.searchStatus = "succeeded"; result.candidateOrdinal = candidateOrdinal; result.candidateCount = candidateCount; result.actualVariant = candidate.variant >>> 0; result.actualSeed = candidate.seed >>> 0; result.nextVariant = ((candidate.variant >>> 0) + 1) >>> 0; if (successDelta?.previewDelta) result.previewDelta = { ...successDelta.previewDelta }; terminalResult = result; successWorld = transactional ? null : candidateWorld; emitProgress({ status: "done", key: "search", phase: "search", workUnitId: "candidate-search", label: `Candidate ${candidateOrdinal}/${candidateCount} accepted`, variant: candidate.variant >>> 0, completed: candidateOrdinal, total: candidateCount }, candidateOrdinal); break; } const invariant = isInvariantFailure(result); const contentRejected = isContentRejection(result); const status = invariant ? "invariant-breach" : contentRejected ? "rejected" : "failed"; const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt); attempts.push(attemptSummary); if (transaction) restorePatchTransactionSnapshot(world, transaction); emitProgress({ status: contentRejected ? "rejected" : "error", key: contentRejected ? "candidate-rejected" : "candidate-failed", phase: "candidate-result", label: contentRejected ? `Candidate ${candidateOrdinal}/${candidateCount} rejected; searching the next complete candidate` : `Candidate ${candidateOrdinal}/${candidateCount} stopped: ${result?.reason || result?.code || "failure"}`, variant: candidate.variant >>> 0, workUnitId: "candidate-search", completed: candidateOrdinal, total: candidateCount, code: result?.code || null, attemptSummary, }, candidateOrdinal); if (!contentRejected) { terminalResult = { ...(result || { ok: false }), searchAttempts: attempts, searchStatus: invariant ? "invariant-breach" : "failed", nextVariant: candidate.variant >>> 0, }; break; } // The rejected full world is no longer observable. Drop the last strong // references before cloning the next Variant so the browser may reclaim // its field buffers instead of retaining multiple complete candidates. candidateWorld = null; transaction = null; result = null; } if (!terminalResult) { const last = candidatePlan[candidatePlan.length - 1]; terminalResult = { ok: false, code: "patch-search-exhausted", reason: `All ${candidateCount} complete production candidates were rejected.`, searchStatus: "exhausted", searchAttempts: attempts, nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0, candidateCount, }; emitProgress({ status: "done", key: "search-exhausted", phase: "search", workUnitId: "candidate-search", label: terminalResult.reason, completed: candidateCount, total: candidateCount }); } if (successWorld && terminalResult?.ok) { emitProgress({ status: "start", key: "preview-delta", phase: "preview-delta", label: "Auditing preview changes", }, terminalResult.candidateOrdinal || 0); terminalResult.previewDelta = computePreviewDelta(world, successWorld, terminalResult.rects || rect, (completed, total, part) => { emitProgress({ status: "step", key: `preview-delta:${part}`, phase: "preview-delta", label: part === "raster" ? "Auditing preview raster changes" : "Auditing preview feature changes", workUnitId: "preview-delta-audit", completed, total, }, terminalResult.candidateOrdinal || 0); }); emitProgress({ status: "done", key: "preview-delta", phase: "preview-delta", label: "Preview change audit complete", }, terminalResult.candidateOrdinal || 0); } // Return only the accepted candidate world. Rejected candidate clones and // the immutable worker baseline remain worker-local and become collectible. return { id, ok: true, world: successWorld, transactionDelta: successDelta, targetHash: successTargetHash, result: terminalResult, searchId, workerEpoch, eventSeq, }; } catch (error) { return { id, ok: false, code: error?.code || "candidate-search-error", error: error?.message || String(error), stack: error?.stack || "", searchId, workerEpoch, eventSeq }; } } if (typeof self !== "undefined") { self.onmessage = async (event) => { const incoming = event.data || {}; if (handleMirrorSyncMessage(incoming)) return; if (incoming.type === "patch-apply-discard") { if (incoming.applyToken) pendingApplyDeltas.delete(incoming.applyToken); return; } if (incoming.type === "patch-apply-ack") { const pending = pendingApplyDeltas.get(incoming.applyToken); let ok = false; let error = null; try { if (!pending) throw new Error("Pending patch delta is unavailable."); if (persistentCommittedRevision !== Number(incoming.baseCommittedRevision)) { throw new Error(`Committed mirror revision conflict (${persistentCommittedRevision} != ${incoming.baseCommittedRevision}).`); } applyCommittedWorldDelta(persistentCommittedMirror, pending.delta, { consumeMetadata: true }); const mirrorHash = hashCommittedWorld(persistentCommittedMirror); if (mirrorHash !== pending.targetHash) { throw new Error(`Committed mirror hash mismatch (${mirrorHash} != ${pending.targetHash}).`); } persistentCommittedRevision = Number(incoming.committedRevision); pendingApplyDeltas.delete(incoming.applyToken); ok = true; } catch (applyError) { error = applyError?.message || String(applyError); persistentCommittedMirror = null; persistentCommittedRevision = -1; pendingApplyDeltas.clear(); } self.postMessage({ type: "patch-apply-ack-result", ackId: incoming.ackId, applyToken: incoming.applyToken, ok, error, mirrorHash: ok ? pending?.targetHash || null : null, mirrorCommittedRevision: persistentCommittedRevision, }); return; } const requestedRevision = Number(incoming.search?.committedRevision ?? -1); if (incoming.world) { persistentCommittedMirror = incoming.world; persistentCommittedRevision = requestedRevision; pendingMirrorBootstrap = null; pendingApplyDeltas.clear(); } else if (!incoming.search?.reuseCommittedMirror || !persistentCommittedMirror || persistentCommittedRevision !== requestedRevision) { self.postMessage({ id: incoming.id, ok: false, error: "Committed worker mirror is unavailable or stale.", code: "worker-mirror-stale", searchId: incoming.search?.searchId || null, workerEpoch: Number(incoming.search?.workerEpoch || 0), }); return; } const payload = await runPatchCandidateSearchAsync({ ...incoming, world: persistentCommittedMirror }, { onProgress: (message) => self.postMessage(message), transactional: true, precomputeRawCandidateBatch, precomputeRawCandidateSequence: scheduleRawCandidateSequence, rawCandidateParallelism: 2, }); // Helper workers are scoped to one candidate-search operation. Releasing // them here prevents a completed/rejected large search from retaining two // full generator heaps while the coordinator waits for Apply/Alternative. await shutdownRawCandidateWorkers(); payload.mirrorCommittedRevision = persistentCommittedRevision; if (payload.ok && payload.result?.ok && (payload.transactionDelta || payload.world)) { payload.eventSeq = Number(payload.eventSeq || 0) + 1; self.postMessage({ id: incoming.id, type: "progress", progress: { searchId: payload.searchId, operationId: incoming.search?.operationId || payload.searchId, candidateOrdinal: Number(payload.result?.candidateOrdinal || 0), candidateCount: Number(payload.result?.candidateCount || incoming.search?.totalCandidateCount || 0), executionAttempt: Math.max(1, Number(incoming.search?.executionAttempt || 1)), workerEpoch: payload.workerEpoch, committedRevision: requestedRevision, eventSeq: payload.eventSeq, counter: payload.eventSeq, phase: "mirror-delta-build", phaseOrdinal: Number.MAX_SAFE_INTEGER - 1, workUnitId: "mirror-delta-build", boundedWork: false, cooperative: false, nonCooperative: true, status: "start", key: "mirror-delta-build", label: "Building transactional Apply delta", }, }); const applyToken = `${payload.searchId}:${payload.result.candidateOrdinal || 0}:${payload.result.actualVariant ?? payload.result.variant ?? 0}:${requestedRevision}`; try { const delta = payload.transactionDelta || buildCommittedMirrorDelta(persistentCommittedMirror, payload.world); const targetHash = payload.targetHash || hashCommittedWorld(payload.world); const mainThreadDelta = buildMainThreadTransferDelta(delta); pendingApplyDeltas.clear(); pendingApplyDeltas.set(applyToken, { delta, baseCommittedRevision: requestedRevision, targetHash }); // The main thread needs one transferable copy to materialize the // preview. Keep the original delta Worker-local for the later Apply ACK. payload.worldDelta = mainThreadDelta; payload.transactionDelta = null; payload.targetHash = null; payload.world = null; payload.result.applyToken = applyToken; payload.result.acceptedWorldHash = targetHash; } catch (deltaError) { pendingApplyDeltas.delete(applyToken); payload.transactionDelta = null; payload.targetHash = null; payload.world = null; payload.result = { ...payload.result, ok: false, code: "candidate-delta-build-failed", reason: deltaError?.message || String(deltaError), searchStatus: "infrastructure-error", applyToken: null, applyDeltaError: deltaError?.message || String(deltaError), }; } } if (payload.ok) { payload.eventSeq = Number(payload.eventSeq || 0) + 1; self.postMessage({ id: incoming.id, type: "progress", progress: { searchId: payload.searchId, operationId: incoming.search?.operationId || payload.searchId, candidateOrdinal: Number(payload.result?.candidateOrdinal || 0), candidateCount: Number(payload.result?.candidateCount || incoming.search?.totalCandidateCount || 0), executionAttempt: Math.max(1, Number(incoming.search?.executionAttempt || 1)), workerEpoch: payload.workerEpoch, committedRevision: requestedRevision, eventSeq: payload.eventSeq, counter: payload.eventSeq, phase: "result-serialization", phaseOrdinal: Number.MAX_SAFE_INTEGER, workUnitId: "result-serialization", boundedWork: false, cooperative: false, nonCooperative: true, status: "start", key: "result-serialization", label: "Serializing accepted patch result", }, }); } // Accepted transactional results keep all large typed payloads under the // world delta. Traversing the complete diagnostic/result graph here caused // a long no-progress pause at the final stage without finding additional // buffers. Legacy full-world payloads still transfer their world buffers. // Only the copied raster delta buffers are transferable. Metadata in // worldDelta deliberately shares the Worker-retained ACK graph until // postMessage clones it; transferring a nested typed metadata buffer would // detach the retained delta and make Apply fail. const transferRoot = payload.worldDelta ? { fields: payload.worldDelta.fields, generatedMask: payload.worldDelta.generatedMask } : (payload.world || null); const transfer = payload.ok && transferRoot ? Array.from(collectTransferableBuffers(transferRoot)) : []; self.postMessage(payload, transfer); }; }