1
This commit is contained in:
parent
1a3ba56d9a
commit
810ad6f5cb
33 changed files with 10710 additions and 1087 deletions
430
src/committedWorldDelta.js
Normal file
430
src/committedWorldDelta.js
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
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");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue