430 lines
18 KiB
JavaScript
430 lines
18 KiB
JavaScript
function typedArrayConstructor(name) {
|
|
return {
|
|
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
|
|
Int32Array, Uint32Array, Float32Array, Float64Array,
|
|
}[name] || null;
|
|
}
|
|
|
|
function applyTypedRowDelta(current, delta) {
|
|
if (!delta) return current;
|
|
const Constructor = typedArrayConstructor(delta.constructorName);
|
|
if (!Constructor) throw new Error(`Unsupported delta field type ${delta.constructorName}.`);
|
|
if (delta.replace) return new Constructor(delta.replace);
|
|
const target = ArrayBuffer.isView(current) && current.constructor?.name === delta.constructorName && current.length === delta.length
|
|
? current
|
|
: new Constructor(delta.length);
|
|
for (const row of delta.rows || []) target.set(row.values, row.start);
|
|
return target;
|
|
}
|
|
|
|
function applyExactObjectDelta(target = {}, delta = {}, { cloneValues = true } = {}) {
|
|
for (const key of delta.removed || []) delete target[key];
|
|
for (const [key, value] of Object.entries(delta.set || {})) target[key] = cloneValues ? structuredClone(value) : value;
|
|
for (const [key, splice] of Object.entries(delta.arraySplices || {})) {
|
|
const current = target[key];
|
|
if (!Array.isArray(current)) throw new Error(`Cannot apply array splice delta to non-array metadata key ${key}.`);
|
|
const start = Math.max(0, Math.min(current.length, Math.floor(Number(splice?.start || 0))));
|
|
const deleteCount = Math.max(0, Math.min(current.length - start, Math.floor(Number(splice?.deleteCount || 0))));
|
|
const items = Array.isArray(splice?.items) ? splice.items : [];
|
|
const inserted = cloneValues ? structuredClone(items) : items;
|
|
target[key] = current.slice(0, start).concat(inserted, current.slice(start + deleteCount));
|
|
}
|
|
return target;
|
|
}
|
|
|
|
export function applyCommittedWorldDelta(world, delta, { consumeMetadata = false, copyOnWrite = false } = {}) {
|
|
if (!world || !delta) throw new Error("Committed mirror delta is missing.");
|
|
if (world.width !== delta.width || world.height !== delta.height) {
|
|
throw new Error(`Committed mirror dimensions changed unexpectedly (${world.width}x${world.height} -> ${delta.width}x${delta.height}).`);
|
|
}
|
|
for (const [name, fieldDelta] of Object.entries(delta.fields || {})) {
|
|
if (fieldDelta.remove) delete world.fields[name];
|
|
else {
|
|
const current = world.fields?.[name];
|
|
const writable = copyOnWrite && ArrayBuffer.isView(current) && !fieldDelta.replace
|
|
? new current.constructor(current)
|
|
: current;
|
|
world.fields[name] = applyTypedRowDelta(writable, fieldDelta);
|
|
}
|
|
}
|
|
const generatedMask = copyOnWrite && delta.generatedMask && ArrayBuffer.isView(world.generatedMask) && !delta.generatedMask.replace
|
|
? new world.generatedMask.constructor(world.generatedMask)
|
|
: world.generatedMask;
|
|
world.generatedMask = applyTypedRowDelta(generatedMask, delta.generatedMask);
|
|
if (delta.sourceMapDelta || delta.metaDelta) {
|
|
// Clone all changed metadata in one graph. sourceMap diagnostics and
|
|
// lastPatchResult often share the same seam/path objects; cloning each key
|
|
// separately multiplied both allocation and retained heap.
|
|
// postMessage already gives the main thread an isolated object graph, and
|
|
// the Worker ACK consumes its retained delta exactly once. Those hot paths
|
|
// can adopt the delta values directly instead of cloning every changed
|
|
// feature/path/diagnostic a second time at peak memory. Keep clone-by-default
|
|
// for reusable library callers.
|
|
const exactValues = consumeMetadata ? {
|
|
sourceSet: delta.sourceMapDelta?.set || {},
|
|
metaSet: delta.metaDelta?.set || {},
|
|
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
|
|
metaArraySplices: delta.metaDelta?.arraySplices || {},
|
|
} : structuredClone({
|
|
sourceSet: delta.sourceMapDelta?.set || {},
|
|
metaSet: delta.metaDelta?.set || {},
|
|
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
|
|
metaArraySplices: delta.metaDelta?.arraySplices || {},
|
|
});
|
|
if (delta.sourceMapDelta) {
|
|
world.sourceMap = applyExactObjectDelta(world.sourceMap || {}, {
|
|
...delta.sourceMapDelta,
|
|
set: exactValues.sourceSet,
|
|
arraySplices: exactValues.sourceArraySplices,
|
|
}, { cloneValues: false });
|
|
}
|
|
if (delta.metaDelta) {
|
|
applyExactObjectDelta(world, {
|
|
...delta.metaDelta,
|
|
set: exactValues.metaSet,
|
|
arraySplices: exactValues.metaArraySplices,
|
|
}, { cloneValues: false });
|
|
}
|
|
}
|
|
else world.sourceMap = structuredClone(delta.sourceMap || {});
|
|
if (!delta.metaDelta) {
|
|
for (const key of delta.removedMetaKeys || []) delete world[key];
|
|
for (const [key, value] of Object.entries(delta.meta || {})) world[key] = structuredClone(value);
|
|
}
|
|
return world;
|
|
}
|
|
|
|
export function materializeCommittedWorldDelta(baseWorld, delta, { consumeMetadata = false } = {}) {
|
|
if (!baseWorld) throw new Error("Committed base world is missing.");
|
|
// Patch previews are immutable views. Copy the world containers, then clone
|
|
// only typed fields touched by row deltas. Exact metadata application replaces
|
|
// changed roots/arrays, so untouched production metadata can remain shared.
|
|
const previewWorld = {
|
|
...baseWorld,
|
|
fields: { ...(baseWorld.fields || {}) },
|
|
sourceMap: { ...(baseWorld.sourceMap || {}) },
|
|
};
|
|
return applyCommittedWorldDelta(previewWorld, delta, { consumeMetadata, copyOnWrite: true });
|
|
}
|
|
|
|
function cancellationError(message = "Committed world delta materialization cancelled.") {
|
|
if (typeof DOMException === "function") return new DOMException(message, "AbortError");
|
|
const error = new Error(message);
|
|
error.name = "AbortError";
|
|
return error;
|
|
}
|
|
|
|
async function cloneTypedArrayCooperatively(source, yieldControl, shouldCancel, chunkBytes) {
|
|
if (!ArrayBuffer.isView(source) || source instanceof DataView) return source;
|
|
const target = new source.constructor(source.length);
|
|
const bytesPerElement = Math.max(1, source.BYTES_PER_ELEMENT || 1);
|
|
const elementsPerChunk = Math.max(1, Math.floor(chunkBytes / bytesPerElement));
|
|
for (let offset = 0; offset < source.length; offset += elementsPerChunk) {
|
|
if (shouldCancel()) throw cancellationError();
|
|
const end = Math.min(source.length, offset + elementsPerChunk);
|
|
target.set(source.subarray(offset, end), offset);
|
|
if (end < source.length) await yieldControl();
|
|
}
|
|
return target;
|
|
}
|
|
|
|
async function applyTypedRowDeltaCooperatively(current, delta, options) {
|
|
if (!delta) return current;
|
|
const Constructor = typedArrayConstructor(delta.constructorName);
|
|
if (!Constructor) throw new Error(`Unsupported delta field type ${delta.constructorName}.`);
|
|
const { yieldControl, shouldCancel, chunkBytes } = options;
|
|
if (delta.replace) {
|
|
// The main thread owns transferred replacement buffers exclusively. Adopt
|
|
// them directly instead of making a second full-size copy at peak memory.
|
|
if (ArrayBuffer.isView(delta.replace) && delta.replace.constructor === Constructor) return delta.replace;
|
|
return cloneTypedArrayCooperatively(new Constructor(delta.replace), yieldControl, shouldCancel, chunkBytes);
|
|
}
|
|
let target;
|
|
if (ArrayBuffer.isView(current) && current.constructor?.name === delta.constructorName && current.length === delta.length) {
|
|
target = await cloneTypedArrayCooperatively(current, yieldControl, shouldCancel, chunkBytes);
|
|
} else {
|
|
target = new Constructor(delta.length);
|
|
}
|
|
let bytesSinceYield = 0;
|
|
for (const row of delta.rows || []) {
|
|
if (shouldCancel()) throw cancellationError();
|
|
target.set(row.values, row.start);
|
|
bytesSinceYield += row.values?.byteLength || 0;
|
|
if (bytesSinceYield >= chunkBytes) {
|
|
bytesSinceYield = 0;
|
|
await yieldControl();
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
|
|
export async function materializeCommittedWorldDeltaCooperative(baseWorld, delta, {
|
|
consumeMetadata = false,
|
|
yieldControl = () => Promise.resolve(),
|
|
shouldCancel = () => false,
|
|
chunkBytes = 4 * 1024 * 1024,
|
|
} = {}) {
|
|
if (!baseWorld) throw new Error("Committed base world is missing.");
|
|
if (!delta) throw new Error("Committed mirror delta is missing.");
|
|
if (baseWorld.width !== delta.width || baseWorld.height !== delta.height) {
|
|
throw new Error(`Committed mirror dimensions changed unexpectedly (${baseWorld.width}x${baseWorld.height} -> ${delta.width}x${delta.height}).`);
|
|
}
|
|
const previewWorld = {
|
|
...baseWorld,
|
|
fields: { ...(baseWorld.fields || {}) },
|
|
sourceMap: { ...(baseWorld.sourceMap || {}) },
|
|
};
|
|
for (const [name, fieldDelta] of Object.entries(delta.fields || {})) {
|
|
if (shouldCancel()) throw cancellationError();
|
|
if (fieldDelta.remove) delete previewWorld.fields[name];
|
|
else previewWorld.fields[name] = await applyTypedRowDeltaCooperatively(previewWorld.fields[name], fieldDelta, {
|
|
yieldControl, shouldCancel, chunkBytes,
|
|
});
|
|
await yieldControl();
|
|
}
|
|
if (delta.generatedMask) {
|
|
previewWorld.generatedMask = await applyTypedRowDeltaCooperatively(previewWorld.generatedMask, delta.generatedMask, {
|
|
yieldControl, shouldCancel, chunkBytes,
|
|
});
|
|
await yieldControl();
|
|
}
|
|
if (shouldCancel()) throw cancellationError();
|
|
if (delta.sourceMapDelta || delta.metaDelta) {
|
|
const exactValues = consumeMetadata ? {
|
|
sourceSet: delta.sourceMapDelta?.set || {},
|
|
metaSet: delta.metaDelta?.set || {},
|
|
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
|
|
metaArraySplices: delta.metaDelta?.arraySplices || {},
|
|
} : structuredClone({
|
|
sourceSet: delta.sourceMapDelta?.set || {},
|
|
metaSet: delta.metaDelta?.set || {},
|
|
sourceArraySplices: delta.sourceMapDelta?.arraySplices || {},
|
|
metaArraySplices: delta.metaDelta?.arraySplices || {},
|
|
});
|
|
if (delta.sourceMapDelta) {
|
|
previewWorld.sourceMap = applyExactObjectDelta(previewWorld.sourceMap || {}, {
|
|
...delta.sourceMapDelta,
|
|
set: exactValues.sourceSet,
|
|
arraySplices: exactValues.sourceArraySplices,
|
|
}, { cloneValues: false });
|
|
await yieldControl();
|
|
}
|
|
if (shouldCancel()) throw cancellationError();
|
|
if (delta.metaDelta) {
|
|
applyExactObjectDelta(previewWorld, {
|
|
...delta.metaDelta,
|
|
set: exactValues.metaSet,
|
|
arraySplices: exactValues.metaArraySplices,
|
|
}, { cloneValues: false });
|
|
await yieldControl();
|
|
}
|
|
} else {
|
|
previewWorld.sourceMap = consumeMetadata ? (delta.sourceMap || {}) : structuredClone(delta.sourceMap || {});
|
|
}
|
|
if (!delta.metaDelta) {
|
|
for (const key of delta.removedMetaKeys || []) delete previewWorld[key];
|
|
for (const [key, value] of Object.entries(delta.meta || {})) {
|
|
if (shouldCancel()) throw cancellationError();
|
|
previewWorld[key] = consumeMetadata ? value : structuredClone(value);
|
|
await yieldControl();
|
|
}
|
|
}
|
|
return previewWorld;
|
|
}
|
|
|
|
function mixHashText(hash, text) {
|
|
let value = hash >>> 0;
|
|
for (let index = 0; index < text.length; index++) {
|
|
value ^= text.charCodeAt(index);
|
|
value = Math.imul(value, 16777619) >>> 0;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function mixHashValue(hash, value) {
|
|
let next = hash >>> 0;
|
|
if (value == null) return mixHashText(next, String(value));
|
|
if (ArrayBuffer.isView(value)) {
|
|
next = mixHashText(next, `${value.constructor.name}:${value.length}:`);
|
|
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
for (let index = 0; index < bytes.length; index++) {
|
|
next ^= bytes[index];
|
|
next = Math.imul(next, 16777619) >>> 0;
|
|
}
|
|
return next;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
next = mixHashText(next, `[${value.length}:`);
|
|
for (const item of value) next = mixHashValue(next, item);
|
|
return next;
|
|
}
|
|
if (value instanceof Map) {
|
|
const entries = [...value.entries()].sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
return mixHashValue(mixHashText(next, `Map:${entries.length}:`), entries);
|
|
}
|
|
if (value instanceof Set) return mixHashValue(mixHashText(next, `Set:${value.size}:`), [...value].sort());
|
|
if (typeof value === "object") {
|
|
const keys = Object.keys(value).sort();
|
|
next = mixHashText(next, `{${keys.length}:`);
|
|
for (const key of keys) {
|
|
next = mixHashText(next, key);
|
|
next = mixHashValue(next, value[key]);
|
|
}
|
|
return next;
|
|
}
|
|
return mixHashText(next, `${typeof value}:${String(value)}`);
|
|
}
|
|
|
|
export function hashCommittedWorld(world) {
|
|
let hash = 2166136261 >>> 0;
|
|
hash = mixHashText(hash, `${world?.width || 0}x${world?.height || 0}|${world?.originX || 0},${world?.originY || 0}`);
|
|
for (const name of Object.keys(world?.fields || {}).sort()) {
|
|
hash = mixHashText(hash, `|${name}:`);
|
|
const field = world.fields[name];
|
|
if (!ArrayBuffer.isView(field)) continue;
|
|
const bytes = new Uint8Array(field.buffer, field.byteOffset, field.byteLength);
|
|
for (let index = 0; index < bytes.length; index++) {
|
|
hash ^= bytes[index];
|
|
hash = Math.imul(hash, 16777619) >>> 0;
|
|
}
|
|
}
|
|
if (ArrayBuffer.isView(world?.generatedMask)) {
|
|
hash = mixHashText(hash, "|generatedMask:");
|
|
const bytes = new Uint8Array(world.generatedMask.buffer, world.generatedMask.byteOffset, world.generatedMask.byteLength);
|
|
for (let index = 0; index < bytes.length; index++) {
|
|
hash ^= bytes[index];
|
|
hash = Math.imul(hash, 16777619) >>> 0;
|
|
}
|
|
}
|
|
hash = mixHashText(hash, "|sourceMap:");
|
|
hash = mixHashValue(hash, world?.sourceMap || {});
|
|
const meta = {};
|
|
for (const [key, value] of Object.entries(world || {})) {
|
|
if (key === "fields" || key === "generatedMask" || key === "sourceMap") continue;
|
|
meta[key] = value;
|
|
}
|
|
hash = mixHashText(hash, "|meta:");
|
|
hash = mixHashValue(hash, meta);
|
|
return hash.toString(16).padStart(8, "0");
|
|
}
|
|
|
|
|
|
async function cooperativeHashYield(state) {
|
|
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
|
|
state.pending = 0;
|
|
await state.yieldControl();
|
|
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
|
|
}
|
|
|
|
async function mixHashTextAsync(hash, text, state) {
|
|
let value = hash >>> 0;
|
|
let index = 0;
|
|
while (index < text.length) {
|
|
const start = index;
|
|
const capacity = Math.max(1, state.yieldEvery - state.pending);
|
|
const end = Math.min(text.length, index + capacity);
|
|
for (; index < end; index++) {
|
|
value ^= text.charCodeAt(index);
|
|
value = Math.imul(value, 16777619) >>> 0;
|
|
}
|
|
state.pending += end - start;
|
|
if (state.pending >= state.yieldEvery) await cooperativeHashYield(state);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
async function mixHashBytesAsync(hash, bytes, state) {
|
|
let value = hash >>> 0;
|
|
let index = 0;
|
|
while (index < bytes.length) {
|
|
const start = index;
|
|
const capacity = Math.max(1, state.yieldEvery - state.pending);
|
|
const end = Math.min(bytes.length, index + capacity);
|
|
for (; index < end; index++) {
|
|
value ^= bytes[index];
|
|
value = Math.imul(value, 16777619) >>> 0;
|
|
}
|
|
state.pending += end - start;
|
|
if (state.pending >= state.yieldEvery) await cooperativeHashYield(state);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
async function mixHashValueAsync(hash, value, state) {
|
|
let next = hash >>> 0;
|
|
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
|
|
if (value == null) return mixHashTextAsync(next, String(value), state);
|
|
if (ArrayBuffer.isView(value)) {
|
|
next = await mixHashTextAsync(next, `${value.constructor.name}:${value.length}:`, state);
|
|
return mixHashBytesAsync(next, new Uint8Array(value.buffer, value.byteOffset, value.byteLength), state);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
next = await mixHashTextAsync(next, `[${value.length}:`, state);
|
|
for (const item of value) next = await mixHashValueAsync(next, item, state);
|
|
return next;
|
|
}
|
|
if (value instanceof Map) {
|
|
const entries = [...value.entries()].sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
next = await mixHashTextAsync(next, `Map:${entries.length}:`, state);
|
|
return mixHashValueAsync(next, entries, state);
|
|
}
|
|
if (value instanceof Set) {
|
|
next = await mixHashTextAsync(next, `Set:${value.size}:`, state);
|
|
return mixHashValueAsync(next, [...value].sort(), state);
|
|
}
|
|
if (typeof value === "object") {
|
|
const keys = Object.keys(value).sort();
|
|
next = await mixHashTextAsync(next, `{${keys.length}:`, state);
|
|
for (const key of keys) {
|
|
next = await mixHashTextAsync(next, key, state);
|
|
next = await mixHashValueAsync(next, value[key], state);
|
|
}
|
|
return next;
|
|
}
|
|
return mixHashTextAsync(next, `${typeof value}:${String(value)}`, state);
|
|
}
|
|
|
|
// Bit-identical cooperative counterpart to hashCommittedWorld(). Yielding is
|
|
// only inserted between chunks; byte/text order and FNV-1a arithmetic are
|
|
// unchanged. This lets the main thread verify a transferred transactional
|
|
// preview without creating an uncancellable multi-megabyte long task.
|
|
export async function hashCommittedWorldAsync(world, {
|
|
yieldEvery = 262_144,
|
|
yieldControl = null,
|
|
shouldAbort = () => false,
|
|
} = {}) {
|
|
const fallbackYield = () => {
|
|
if (typeof globalThis.scheduler?.yield === "function") return globalThis.scheduler.yield();
|
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
};
|
|
const state = {
|
|
yieldEvery: Math.max(1, Math.floor(Number(yieldEvery) || 262_144)),
|
|
pending: 0,
|
|
shouldAbort: typeof shouldAbort === "function" ? shouldAbort : () => false,
|
|
yieldControl: typeof yieldControl === "function" ? yieldControl : fallbackYield,
|
|
};
|
|
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
|
|
let hash = 2166136261 >>> 0;
|
|
hash = await mixHashTextAsync(hash, `${world?.width || 0}x${world?.height || 0}|${world?.originX || 0},${world?.originY || 0}`, state);
|
|
for (const name of Object.keys(world?.fields || {}).sort()) {
|
|
hash = await mixHashTextAsync(hash, `|${name}:`, state);
|
|
const field = world.fields[name];
|
|
if (!ArrayBuffer.isView(field)) continue;
|
|
hash = await mixHashBytesAsync(hash, new Uint8Array(field.buffer, field.byteOffset, field.byteLength), state);
|
|
}
|
|
if (ArrayBuffer.isView(world?.generatedMask)) {
|
|
hash = await mixHashTextAsync(hash, "|generatedMask:", state);
|
|
hash = await mixHashBytesAsync(hash, new Uint8Array(world.generatedMask.buffer, world.generatedMask.byteOffset, world.generatedMask.byteLength), state);
|
|
}
|
|
hash = await mixHashTextAsync(hash, "|sourceMap:", state);
|
|
hash = await mixHashValueAsync(hash, world?.sourceMap || {}, state);
|
|
const meta = {};
|
|
for (const [key, value] of Object.entries(world || {})) {
|
|
if (key === "fields" || key === "generatedMask" || key === "sourceMap") continue;
|
|
meta[key] = value;
|
|
}
|
|
hash = await mixHashTextAsync(hash, "|meta:", state);
|
|
hash = await mixHashValueAsync(hash, meta, state);
|
|
if (state.shouldAbort()) throw cancellationError("Committed world hash cancelled.");
|
|
return hash.toString(16).padStart(8, "0");
|
|
}
|