feat(chord): add optimized delta engine prototypes

This commit is contained in:
Mario Zechner 2026-09-23 02:23:16 +02:00
parent 9a139c62bf
commit 4bc1a2fe53
24 changed files with 7322 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# Astra trusted contract
Astra is an exact overlay tracker for trusted mutable live state. `track()` and
`prepareReplace()` take ownership of their roots in O(1); callers must not mutate
transferred roots except through an active Astra draft. Successful adoption
mutates the committed root in place, so previously retained committed references
observe later transactions.
Inputs and placements must already be alias-free strict JSON trees: plain objects,
dense arrays, strings, booleans, finite numbers, and `null`. Cycles, accessors,
proxies, symbols, classes, sparse arrays, `undefined`, and non-finite numbers are
outside the contract. Astra deliberately does not validate or freeze complete
input trees.
Supported draft operations use ordinary property access, primitive array indices,
and non-reentrant array callbacks. The following are outside the trusted contract:
- coercion callbacks that mutate the draft while an array method is converting an argument;
- mutations or `prepare()` calls from inside a `sort()` comparator;
- bigint or object-valued comparator results requiring coercion;
- non-primitive array indices.
These exclusions avoid callback-order and coercion machinery on the overlay hot
paths. Behavior for excluded operations is unspecified.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
# Trusted COW contract
This variant provides immutable copy-on-write revisions without recursive input validation or deep freezing.
- `track(initial)` and `prepareReplace(value)` take ownership. Inputs are alias-free, acyclic strict JSON made from dense arrays, plain data objects, finite numbers, strings, booleans, and `null`. Accessors, custom mutation hooks, symbols, classes, sparse arrays, and external proxies are outside the contract.
- Do not mutate transferred roots, `tracker.value`, `Prepared.base`, `Prepared.value`, or `Prepared.ops` externally. `Prepared.value` and `Prepared.ops` are independent immutable ownership transfers.
- Values assigned through a draft are cloned by value for each placement. Mutate transaction state only through its draft handles.
- Every change must settle exactly once through `prepare()` followed by `adopt()`, or through `abort()`. Draft handles are invalid after preparation or abortion.
- Array indices and mutator index arguments must be primitive numbers. Objects with coercion callbacks and mutations caused during argument coercion are outside the contract.
- Sort comparators must be synchronous and pure: they return ordinary numbers and do not mutate drafts, prepare transactions, or start nested operations.
Violating this contract is programmer error and may not be detected at runtime.

View file

@ -0,0 +1,71 @@
import type { JsonValue } from "../../types.ts";
import {
apply as applyMutable,
assertValidOp,
type Op,
type Path,
PathError,
type Seg,
UnsafePathError,
} from "../index.ts";
type JsonContainer = JsonValue[] | Record<string, JsonValue>;
/** Apply one batch while copying each touched container at most once. */
export function applyImmutable<T>(target: T | undefined, operations: readonly Op[]): T {
let root = target as unknown as JsonValue;
const owned = new WeakSet<object>();
for (const operation of operations) {
assertValidOp(operation);
if (operation[0] === "r") {
root = operation[1];
continue;
}
const path = operation[0] === "p" || operation[0] === "m" ? operation[1] : operation[1].slice(0, -1);
root = copyPath(root, path, owned);
root = applyMutable(root, [operation]);
}
return root as unknown as T;
}
function copyPath(root: JsonValue, path: Path, owned: WeakSet<object>): JsonValue {
if (!isContainer(root)) throw new PathError(path);
let copiedRoot = root;
if (!owned.has(root)) {
copiedRoot = shallowCopy(root);
owned.add(copiedRoot);
}
let destination = copiedRoot as JsonContainer;
for (const segment of path) {
if (Array.isArray(destination) && typeof segment !== "number") throw new UnsafePathError(segment);
if (!Object.hasOwn(destination, segment)) throw new PathError(path);
let child = (destination as Record<Seg, JsonValue>)[segment]!;
if (!isContainer(child)) throw new PathError(path);
if (!owned.has(child)) {
const copy = shallowCopy(child);
defineData(destination, segment, copy);
owned.add(copy);
child = copy;
}
destination = child;
}
return copiedRoot;
}
function shallowCopy(value: JsonContainer): JsonContainer {
if (Array.isArray(value)) return Array.from(value);
const result = Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype) as Record<
string,
JsonValue
>;
for (const key of Object.keys(value)) defineData(result, key, value[key]!);
return result;
}
function defineData(target: object, key: PropertyKey, value: JsonValue): void {
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
}
function isContainer(value: unknown): value is JsonContainer {
return value !== null && typeof value === "object";
}

View file

@ -0,0 +1,562 @@
import type { JsonValue } from "../../types.ts";
import { type NonEmptyPath, type Op, overlap, type Path, RESERVED_SEGMENTS } from "../index.ts";
import type { DiffHints } from "./draft.ts";
const DEFAULT_OVERLAP_SCAN = 65_536;
const MAX_DELTA_OPERATIONS = 4_096;
const overflowedBatches = new WeakSet<Op[]>();
const emitOperation = (operations: Op[], operation: Op): void => {
if (overflowedBatches.has(operations)) return;
if (operations.length >= MAX_DELTA_OPERATIONS) {
overflowedBatches.add(operations);
return;
}
operations.push(operation);
};
const isContainer = (value: JsonValue): value is JsonValue[] | Record<string, JsonValue> =>
value !== null && typeof value === "object";
const emitSet = (path: Path, value: JsonValue, operations: Op[]): void => {
if (path.length === 0) emitOperation(operations, ["r", value]);
else emitOperation(operations, ["s", path as NonEmptyPath, value]);
};
const equalJson = (left: JsonValue, right: JsonValue): boolean => {
if (left === right) return true;
if (!isContainer(left) || !isContainer(right) || Array.isArray(left) !== Array.isArray(right)) return false;
if (Array.isArray(left)) {
const other = right as JsonValue[];
if (left.length !== other.length) return false;
for (let index = 0; index < left.length; index++) {
if (!equalJson(left[index]!, other[index]!)) return false;
}
return true;
}
const other = right as Record<string, JsonValue>;
const keys = Object.keys(left);
if (keys.length !== Object.keys(other).length) return false;
for (const key of keys) {
if (!Object.hasOwn(other, key) || !equalJson(left[key]!, other[key]!)) return false;
}
return true;
};
const permutation = (before: readonly JsonValue[], after: readonly JsonValue[]): number[] | undefined => {
if (before.length !== after.length) return undefined;
// Cheap native probes before building a map: shifted/appended arrays fail here.
if (before.indexOf(after[after.length - 1]!) === -1 || before.indexOf(after[0]!) === -1) return undefined;
// A single index per value; duplicates upgrade to an index list.
const positions = new Map<JsonValue, number | number[]>();
for (let index = 0; index < before.length; index++) {
const value = before[index]!;
const entry = positions.get(value);
if (entry === undefined) positions.set(value, index);
else if (typeof entry === "number") positions.set(value, [entry, index]);
else entry.push(index);
}
const used = new Map<JsonValue, number>();
const result = new Array<number>(after.length);
for (let index = 0; index < after.length; index++) {
const value = after[index]!;
const entry = positions.get(value);
if (entry === undefined) return undefined;
if (typeof entry === "number") {
if (used.has(value)) return undefined;
used.set(value, 1);
result[index] = entry;
continue;
}
const taken = used.get(value) ?? 0;
if (taken === entry.length) return undefined;
used.set(value, taken + 1);
result[index] = entry[taken]!;
}
return result;
};
const emitString = (before: string, after: string, path: NonEmptyPath, operations: Op[]): void => {
if (before === after) return;
if (after.length > before.length && after.slice(0, before.length) === before) {
emitOperation(operations, ["a", path, after.slice(before.length)]);
return;
}
const shared = overlap(before, after, DEFAULT_OVERLAP_SCAN);
if (shared === 0) {
emitOperation(operations, ["s", path, after]);
return;
}
emitOperation(operations, ["t", path, before.length - shared]);
if (after.length > shared) emitOperation(operations, ["a", path, after.slice(shared)]);
};
type ArrayMatch = readonly [before: number, after: number];
type MatchCandidate = {
before: number;
after: number;
previous: number;
};
const MAX_IDENTITY_CANDIDATES = 200_000;
const MAX_SEMANTIC_CELLS = 65_536;
const sameValue = (left: JsonValue, right: JsonValue): boolean => left === right || equalJson(left, right);
const lcsMatches = (
before: readonly JsonValue[],
after: readonly JsonValue[],
equal: (left: JsonValue, right: JsonValue) => boolean,
maxCells: number,
): ArrayMatch[] | undefined => {
if (before.length === 0 || after.length === 0) return [];
if (before.length * after.length > maxCells) return undefined;
const width = after.length + 1;
const lengths = new Uint32Array((before.length + 1) * width);
for (let left = before.length - 1; left >= 0; left--) {
for (let right = after.length - 1; right >= 0; right--) {
const at = left * width + right;
lengths[at] = equal(before[left]!, after[right]!)
? lengths[(left + 1) * width + right + 1]! + 1
: Math.max(lengths[(left + 1) * width + right]!, lengths[left * width + right + 1]!);
}
}
const matches: ArrayMatch[] = [];
let left = 0;
let right = 0;
while (left < before.length && right < after.length) {
if (
equal(before[left]!, after[right]!) &&
lengths[left * width + right] === lengths[(left + 1) * width + right + 1]! + 1
) {
matches.push([left++, right++]);
} else if (lengths[(left + 1) * width + right]! >= lengths[left * width + right + 1]!) left += 1;
else right += 1;
}
return matches;
};
const semanticallyAligned = (left: JsonValue, right: JsonValue): boolean => {
if (sameValue(left, right)) return true;
if (!isContainer(left) || !isContainer(right) || Array.isArray(left) !== Array.isArray(right)) return false;
if (Array.isArray(left) && Array.isArray(right)) {
if (left.length !== right.length) return false;
return left.some((value, index) => isContainer(value) && value === right[index]);
}
const leftObject = left as Record<string, JsonValue>;
const rightObject = right as Record<string, JsonValue>;
for (const key of Object.keys(leftObject)) {
const value = leftObject[key];
if (isContainer(value) && Object.hasOwn(rightObject, key) && value === rightObject[key]) return true;
}
return false;
};
const indexOfIdentity = (values: readonly JsonValue[], value: JsonValue, start: number, end: number): number => {
if (!isContainer(value)) return -1; // primitives are not identity anchors
const at = values.indexOf(value, start);
return at === -1 || at >= end ? -1 : at;
};
const lowerBound = (values: readonly number[], value: number): number => {
let low = 0;
let high = values.length;
while (low < high) {
const middle = (low + high) >>> 1;
if (values[middle]! < value) low = middle + 1;
else high = middle;
}
return low;
};
const identitySubsequence = (
before: readonly JsonValue[],
beforeStart: number,
beforeEnd: number,
after: readonly JsonValue[],
afterStart: number,
afterEnd: number,
): ArrayMatch[] | undefined => {
const beforeCount = beforeEnd - beforeStart;
const afterCount = afterEnd - afterStart;
const matches: ArrayMatch[] = [];
if (afterCount < beforeCount) {
let beforeIndex = beforeStart;
for (let afterIndex = afterStart; afterIndex < afterEnd; afterIndex++) {
while (beforeIndex < beforeEnd && before[beforeIndex] !== after[afterIndex]) beforeIndex += 1;
if (beforeIndex === beforeEnd) return undefined;
matches.push([beforeIndex++, afterIndex]);
}
return matches;
}
if (beforeCount < afterCount) {
let afterIndex = afterStart;
for (let beforeIndex = beforeStart; beforeIndex < beforeEnd; beforeIndex++) {
while (afterIndex < afterEnd && before[beforeIndex] !== after[afterIndex]) afterIndex += 1;
if (afterIndex === afterEnd) return undefined;
matches.push([beforeIndex, afterIndex++]);
}
return matches;
}
return undefined;
};
const greedyIdentityAnchors = (
positions: ReadonlyMap<JsonValue, readonly number[]>,
after: readonly JsonValue[],
afterStart: number,
afterEnd: number,
): ArrayMatch[] => {
const matches: ArrayMatch[] = [];
let previous = -1;
for (let afterIndex = afterStart; afterIndex < afterEnd; afterIndex++) {
const candidates = positions.get(after[afterIndex]!);
if (candidates === undefined) continue;
const at = lowerBound(candidates, previous + 1);
const beforeIndex = candidates[at];
if (beforeIndex === undefined) continue;
matches.push([beforeIndex, afterIndex]);
previous = beforeIndex;
}
return matches;
};
const identityAnchors = (
before: readonly JsonValue[],
beforeStart: number,
beforeEnd: number,
after: readonly JsonValue[],
afterStart: number,
afterEnd: number,
): ArrayMatch[] => {
const positions = new Map<JsonValue, number[]>();
for (let index = beforeStart; index < beforeEnd; index++) {
const value = before[index]!;
const existing = positions.get(value);
if (existing === undefined) positions.set(value, [index]);
else existing.push(index);
}
let candidateCount = 0;
for (let index = afterStart; index < afterEnd; index++) {
candidateCount += positions.get(after[index]!)?.length ?? 0;
if (candidateCount > MAX_IDENTITY_CANDIDATES) {
return greedyIdentityAnchors(positions, after, afterStart, afterEnd);
}
}
if (candidateCount === 0) return [];
const candidates: MatchCandidate[] = [];
const tails: number[] = [];
const tailValues: number[] = [];
for (let afterIndex = afterStart; afterIndex < afterEnd; afterIndex++) {
const beforePositions = positions.get(after[afterIndex]!);
if (beforePositions === undefined) continue;
for (let index = beforePositions.length - 1; index >= 0; index--) {
const beforeIndex = beforePositions[index]!;
const at = lowerBound(tailValues, beforeIndex);
const candidateIndex = candidates.length;
candidates.push({ before: beforeIndex, after: afterIndex, previous: at === 0 ? -1 : tails[at - 1]! });
tails[at] = candidateIndex;
tailValues[at] = beforeIndex;
}
}
const matches: ArrayMatch[] = [];
let candidateIndex = tails[tails.length - 1] ?? -1;
while (candidateIndex >= 0) {
const candidate = candidates[candidateIndex]!;
matches.push([candidate.before, candidate.after]);
candidateIndex = candidate.previous;
}
matches.reverse();
return matches;
};
const processArrayMatches = (
before: JsonValue[],
after: JsonValue[],
path: Path,
operations: Op[],
hints: DiffHints,
beforeStart: number,
beforeEnd: number,
afterStart: number,
afterEnd: number,
outputStart: number,
matches: readonly ArrayMatch[],
): void => {
let beforeAt = beforeStart;
let afterAt = afterStart;
let outputAt = outputStart;
for (const [beforeMatch, afterMatch] of matches) {
if (overflowedBatches.has(operations)) return;
diffArrayRegion(before, after, path, operations, hints, beforeAt, beforeMatch, afterAt, afterMatch, outputAt);
outputAt += afterMatch - afterAt;
if (!sameValue(before[beforeMatch]!, after[afterMatch]!)) {
diffValue(before[beforeMatch]!, after[afterMatch]!, [...path, outputAt], operations, hints);
}
outputAt += 1;
beforeAt = beforeMatch + 1;
afterAt = afterMatch + 1;
}
if (!overflowedBatches.has(operations)) {
diffArrayRegion(before, after, path, operations, hints, beforeAt, beforeEnd, afterAt, afterEnd, outputAt);
}
};
function diffArrayRegion(
before: JsonValue[],
after: JsonValue[],
path: Path,
operations: Op[],
hints: DiffHints,
beforeStart: number,
beforeEnd: number,
afterStart: number,
afterEnd: number,
outputStart: number,
): void {
if (overflowedBatches.has(operations)) return;
while (beforeStart < beforeEnd && afterStart < afterEnd && sameValue(before[beforeStart]!, after[afterStart]!)) {
beforeStart += 1;
afterStart += 1;
outputStart += 1;
}
while (beforeStart < beforeEnd && afterStart < afterEnd && sameValue(before[beforeEnd - 1]!, after[afterEnd - 1]!)) {
beforeEnd -= 1;
afterEnd -= 1;
}
const beforeCount = beforeEnd - beforeStart;
const afterCount = afterEnd - afterStart;
if (beforeCount === 0 && afterCount === 0) return;
if (beforeCount === 0 || afterCount === 0) {
emitOperation(operations, ["p", path, outputStart, beforeCount, after.slice(afterStart, afterEnd)]);
return;
}
if (beforeCount === afterCount) {
// Streaming positional alignment: equivalent to collecting positional
// matches and diffing the gaps between them, without a tuple per match.
let matched = false;
let gapStart = -1;
for (let offset = 0; offset <= beforeCount; offset++) {
const same = offset < beforeCount && sameValue(before[beforeStart + offset]!, after[afterStart + offset]!);
if (same) {
matched = true;
if (gapStart !== -1) {
if (overflowedBatches.has(operations)) return;
diffArrayRegion(
before,
after,
path,
operations,
hints,
beforeStart + gapStart,
beforeStart + offset,
afterStart + gapStart,
afterStart + offset,
outputStart + gapStart,
);
gapStart = -1;
}
} else if (gapStart === -1) gapStart = offset;
}
if (matched) {
if (gapStart !== -1 && !overflowedBatches.has(operations)) {
diffArrayRegion(
before,
after,
path,
operations,
hints,
beforeStart + gapStart,
beforeEnd,
afterStart + gapStart,
afterEnd,
outputStart + gapStart,
);
}
return;
}
}
// Identity run fast paths (queue shift+push, unshift+pop). The run must be
// longer than both remainders so it is the unique longest anchor chain; the
// output then equals the LIS alignment below without a map or match list.
{
const k = indexOfIdentity(before, after[afterStart]!, beforeStart, beforeEnd);
if (k !== -1) {
let run = 0;
const limit = Math.min(beforeEnd - k, afterCount);
// Container-only runs: object identity is unique, so this run is the unique longest anchor chain.
while (run < limit && before[k + run] === after[afterStart + run] && isContainer(before[k + run]!)) run += 1;
if (k + run === beforeEnd && run > k - beforeStart && run > afterCount - run) {
if (k > beforeStart) emitOperation(operations, ["p", path, outputStart, k - beforeStart, []]);
if (run < afterCount) {
emitOperation(operations, ["p", path, outputStart + run, 0, after.slice(afterStart + run, afterEnd)]);
}
return;
}
}
const j = indexOfIdentity(after, before[beforeStart]!, afterStart, afterEnd);
if (j !== -1) {
let run = 0;
const limit = Math.min(afterEnd - j, beforeCount);
while (run < limit && before[beforeStart + run] === after[j + run] && isContainer(after[j + run]!)) run += 1;
if (j + run === afterEnd && run > j - afterStart && run > beforeCount - run) {
const inserted = j - afterStart;
if (inserted > 0) emitOperation(operations, ["p", path, outputStart, 0, after.slice(afterStart, j)]);
if (run < beforeCount) {
emitOperation(operations, ["p", path, outputStart + inserted + run, beforeCount - run, []]);
}
return;
}
}
}
if (beforeCount === 1 && afterCount === 1) {
// Every alignment strategy below ends in diffValue for a 1x1 region.
diffValue(before[beforeStart]!, after[afterStart]!, [...path, outputStart], operations, hints);
return;
}
const subsequence = identitySubsequence(before, beforeStart, beforeEnd, after, afterStart, afterEnd);
if (subsequence !== undefined && subsequence.length > 0) {
processArrayMatches(
before,
after,
path,
operations,
hints,
beforeStart,
beforeEnd,
afterStart,
afterEnd,
outputStart,
subsequence,
);
return;
}
const identity = identityAnchors(before, beforeStart, beforeEnd, after, afterStart, afterEnd);
if (identity.length > 0) {
processArrayMatches(
before,
after,
path,
operations,
hints,
beforeStart,
beforeEnd,
afterStart,
afterEnd,
outputStart,
identity,
);
return;
}
const semantic = lcsMatches(
before.slice(beforeStart, beforeEnd),
after.slice(afterStart, afterEnd),
semanticallyAligned,
MAX_SEMANTIC_CELLS,
);
if (semantic !== undefined && semantic.length > 0) {
const absolute = semantic.map(
([beforeIndex, afterIndex]) => [beforeStart + beforeIndex, afterStart + afterIndex] as const,
);
processArrayMatches(
before,
after,
path,
operations,
hints,
beforeStart,
beforeEnd,
afterStart,
afterEnd,
outputStart,
absolute,
);
return;
}
if (beforeCount === 1 && afterCount === 1) {
diffValue(before[beforeStart]!, after[afterStart]!, [...path, outputStart], operations, hints);
return;
}
emitOperation(operations, ["p", path, outputStart, beforeCount, after.slice(afterStart, afterEnd)]);
}
const diffArray = (before: JsonValue[], after: JsonValue[], path: Path, operations: Op[], hints: DiffHints): void => {
if (before === after) return;
const hint = hints.get(after);
if (hint?.base === before && hint.keys.length < 2_048) {
for (const index of hint.keys) diffValue(before[index]!, after[index]!, [...path, index], operations, hints);
return;
}
if (equalJson(before, after)) return;
if (
before.length === after.length &&
before.length > 1 &&
!sameValue(before[0]!, after[0]!) &&
!sameValue(before[before.length - 1]!, after[after.length - 1]!)
) {
const order = permutation(before, after);
if (order !== undefined) {
emitOperation(operations, ["m", path, order]);
return;
}
}
diffArrayRegion(before, after, path, operations, hints, 0, before.length, 0, after.length, 0);
};
const diffObject = (
before: Record<string, JsonValue>,
after: Record<string, JsonValue>,
path: Path,
operations: Op[],
hints: DiffHints,
): void => {
const beforeKeys = Object.keys(before);
const afterKeys = Object.keys(after);
if ([...beforeKeys, ...afterKeys].some((key) => RESERVED_SEGMENTS.has(key))) {
if (!equalJson(before, after)) emitSet(path, after, operations);
return;
}
for (const key of afterKeys) {
if (overflowedBatches.has(operations)) return;
if (Object.hasOwn(before, key)) diffValue(before[key]!, after[key]!, [...path, key], operations, hints);
else emitSet([...path, key], after[key]!, operations);
}
for (const key of beforeKeys) {
if (overflowedBatches.has(operations)) return;
if (!Object.hasOwn(after, key)) emitOperation(operations, ["d", [...path, key] as unknown as NonEmptyPath]);
}
};
const diffValue = (before: JsonValue, after: JsonValue, path: Path, operations: Op[], hints: DiffHints): void => {
if (before === after || overflowedBatches.has(operations)) return;
if (typeof before === "string" && typeof after === "string" && path.length > 0) {
emitString(before, after, path as NonEmptyPath, operations);
return;
}
if (Array.isArray(before) && Array.isArray(after)) {
diffArray(before, after, path, operations, hints);
return;
}
if (isContainer(before) && isContainer(after) && !Array.isArray(before) && !Array.isArray(after)) {
diffObject(before, after, path, operations, hints);
return;
}
emitSet(path, after, operations);
};
/** Compute a normalized batch, with deterministic operation-count safety folding only. */
export function diffRevisions(before: JsonValue, after: JsonValue, hints: DiffHints = new WeakMap()): Op[] {
const operations: Op[] = [];
diffValue(before, after, [], operations, hints);
if (overflowedBatches.has(operations)) return [["r", after]];
return operations;
}

View file

@ -0,0 +1,717 @@
import type { JsonValue } from "../../types.ts";
/** Mutable transaction-scoped view of a JSON value, preserving tuple positions. */
export type Draft<T, Depth extends readonly unknown[] = []> = Depth["length"] extends 8
? T
: T extends null | boolean | number | string
? T
: T extends (...args: never[]) => unknown
? T
: T extends object
? { -readonly [Key in keyof T]: Draft<T[Key], [...Depth, unknown]> }
: T;
type Container = Record<string, unknown> | unknown[];
type ChildKey = string | number;
export type ArrayDiffHint = {
readonly base: JsonValue[];
readonly keys: readonly number[];
};
export type DiffHints = WeakMap<object, ArrayDiffHint>;
export type ProduceMetadata<T> = {
value: T;
hints: DiffHints;
};
type DraftContext = {
active: boolean;
states: WeakMap<object, DraftState>;
created: DraftState[];
hints: DiffHints;
};
type ChildIndex = DraftState | Map<ChildKey, DraftState>;
type ChangedIndex = number | Set<number>;
class DraftState {
context: DraftContext;
base: Container;
copy: Container | undefined;
parent: DraftState | undefined;
key: ChildKey | undefined;
proxy: object;
target: object;
dirty = false;
structural = false;
children: ChildIndex | undefined;
changedIndices: ChangedIndex | undefined;
finalized: Container | undefined;
constructor(context: DraftContext, base: Container, parent: DraftState | undefined, key: ChildKey | undefined) {
this.context = context;
this.base = base;
this.copy = undefined;
this.parent = parent;
this.key = key;
this.proxy = this;
this.target = this;
this.children = undefined;
this.changedIndices = undefined;
this.finalized = undefined;
}
}
const DRAFT_STATE = Symbol("cow draft state");
const RELEASED: Record<string, never> = {};
const MAX_NATIVE_ARRAY_INSERT_ITEMS = 10_000;
const ARRAY_MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
export type DraftTransaction<T extends object> = {
readonly state: Draft<T>;
finish(): ProduceMetadata<T>;
abort(): void;
};
export function createDraftTransaction<T extends object>(base: T): DraftTransaction<T> {
const context: DraftContext = {
active: true,
states: new WeakMap(),
created: [],
hints: new WeakMap(),
};
const root = stateFor(context, base as Container, undefined, undefined);
const release = (): void => {
assertActive(context);
context.active = false;
for (const state of context.created) {
state.base = RELEASED;
state.copy = undefined;
state.parent = undefined;
state.key = undefined;
state.proxy = RELEASED;
state.target = RELEASED;
state.children = undefined;
state.changedIndices = undefined;
state.finalized = undefined;
}
context.created.length = 0;
context.states = new WeakMap();
};
return {
state: root.proxy as Draft<T>,
finish() {
assertActive(context);
try {
return { value: finalize(root) as T, hints: context.hints };
} finally {
release();
}
},
abort: release,
};
}
/** Compatibility helper for synchronous COW recipes. */
export function produceWithMetadata<T extends object>(base: T, recipe: (draft: Draft<T>) => void): ProduceMetadata<T> {
const transaction = createDraftTransaction(base);
try {
const outcome = (recipe as (draft: Draft<T>) => unknown)(transaction.state);
if (isPromiseLike(outcome)) {
void Promise.resolve(outcome).catch(() => undefined);
throw new TypeError("Replicated state change callbacks must be synchronous");
}
return transaction.finish();
} catch (error) {
try {
transaction.abort();
} catch {
// finish() already released the transaction.
}
throw error;
}
}
export function produce<T extends object>(base: T, recipe: (draft: Draft<T>) => void): T {
return produceWithMetadata(base, recipe).value;
}
function stateFor(
context: DraftContext,
base: Container,
parent: DraftState | undefined,
key: ChildKey | undefined,
): DraftState {
const existing = context.states.get(base);
if (existing !== undefined) return existing;
const state = new DraftState(context, base, parent, key);
const target: object = Array.isArray(base) ? [] : state;
if (Array.isArray(base)) {
(target as unknown[]).length = base.length;
Object.defineProperty(target, DRAFT_STATE, { value: state, configurable: true });
}
state.target = target;
state.proxy = new Proxy(target, SHARED_HANDLER);
context.states.set(base, state);
context.created.push(state);
return state;
}
const SHARED_HANDLER: ProxyHandler<object> = {
deleteProperty(target, property): boolean {
const state = targetState(target);
assertWritable(state, property);
if (typeof property === "symbol") throw new TypeError("Symbol writes are not supported");
if (Array.isArray(state.base)) throw new TypeError("Draft arrays cannot contain holes");
return deleteObjectProperty(state, property);
},
defineProperty(target): never {
assertActive(targetState(target).context);
throw new TypeError("Defining draft properties is not supported");
},
get(target, property): unknown {
const state = targetState(target);
assertActive(state.context);
if (property === DRAFT_STATE) return state;
if (Array.isArray(state.base) && typeof property === "string" && ARRAY_MUTATORS.has(property)) {
return MUTATORS[property as keyof typeof MUTATORS];
}
const value = Reflect.get(currentValue(state), property, state.proxy);
return draftValue(state.context, value, state, childKey(property));
},
getOwnPropertyDescriptor(target, property): PropertyDescriptor | undefined {
const state = targetState(target);
assertActive(state.context);
const descriptor = Reflect.getOwnPropertyDescriptor(currentValue(state), property);
if (descriptor === undefined) return undefined;
if (Array.isArray(state.base) && property === "length") {
syncArrayTarget(state);
return Reflect.getOwnPropertyDescriptor(state.target, property);
}
return {
configurable: true,
enumerable: descriptor.enumerable,
writable: true,
value:
"value" in descriptor ? draftValue(state.context, descriptor.value, state, childKey(property)) : undefined,
};
},
getPrototypeOf(target): object | null {
const state = targetState(target);
assertActive(state.context);
return Object.getPrototypeOf(state.base);
},
has(target, property): boolean {
const state = targetState(target);
assertActive(state.context);
return Reflect.has(currentValue(state), property);
},
isExtensible(target): boolean {
assertActive(targetState(target).context);
return true;
},
ownKeys(target): ArrayLike<string | symbol> {
const state = targetState(target);
assertActive(state.context);
return Reflect.ownKeys(currentValue(state));
},
preventExtensions(target): never {
assertActive(targetState(target).context);
throw new TypeError("Drafts cannot be made non-extensible");
},
set(target, property, value): boolean {
const state = targetState(target);
assertWritable(state, property);
return writeProperty(state, property, value);
},
setPrototypeOf(target): never {
assertActive(targetState(target).context);
throw new TypeError("Changing a draft prototype is not supported");
},
};
function targetState(target: object): DraftState {
return Array.isArray(target)
? (target as unknown as Record<symbol, DraftState>)[DRAFT_STATE]!
: (target as DraftState);
}
function childKey(property: PropertyKey): ChildKey | undefined {
if (typeof property === "symbol") return undefined;
const index = arrayIndex(property);
return index ?? property;
}
function draftState(value: unknown): DraftState | undefined {
if (!isContainer(value)) return undefined;
return (value as Record<symbol, DraftState | undefined>)[DRAFT_STATE];
}
function draftValue(context: DraftContext, value: unknown, parent?: DraftState, key?: ChildKey): unknown {
if (!isContainer(value)) return value;
const existingDraft = draftState(value);
if (existingDraft !== undefined) return value;
return stateFor(context, value, parent, key).proxy;
}
function currentValue(state: DraftState): Container {
assertActive(state.context);
return state.copy ?? state.base;
}
function ensureCopy(state: DraftState): Container {
markChanged(state);
if (state.copy !== undefined) return state.copy;
state.copy = shallowCopy(state.base);
return state.copy;
}
function shallowCopy(value: Container): Container {
if (Array.isArray(value)) return Array.from(value);
if (Object.getPrototypeOf(value) === null)
return Object.assign(Object.create(null) as Record<string, unknown>, value);
return { ...value };
}
function markChanged(state: DraftState): void {
let current: DraftState | undefined = state;
while (current !== undefined && !current.dirty) {
current.dirty = true;
const parent: DraftState | undefined = current.parent;
if (parent !== undefined && current.key !== undefined) registerChild(parent, current.key, current);
current = parent;
}
}
function registerChild(parent: DraftState, key: ChildKey, child: DraftState): void {
const children = parent.children;
if (children === undefined) parent.children = child;
else if (children instanceof Map) children.set(key, child);
else if (children !== child)
parent.children = new Map([
[children.key!, children],
[key, child],
]);
if (Array.isArray(parent.base) && typeof key === "number") markArrayIndex(parent, key);
}
function markArrayIndex(state: DraftState, index: number): void {
const changed = state.changedIndices;
if (changed === undefined) state.changedIndices = index;
else if (changed instanceof Set) changed.add(index);
else if (changed !== index) state.changedIndices = new Set([changed, index]);
}
function writeProperty(state: DraftState, property: string | symbol, supplied: unknown): boolean {
if (typeof property === "symbol") throw new TypeError("Symbol writes are not supported");
if (Array.isArray(state.base)) {
if (property === "length") return writeArrayLength(state, supplied);
const index = arrayIndex(property);
if (index === undefined) throw new TypeError("Only array indices and length can be written");
const current = currentValue(state) as unknown[];
if (index > current.length) throw new TypeError("Draft arrays cannot contain holes");
const stored = clonePlacement(supplied, state.context);
if (index < current.length && Object.is(current[index], stored)) return true;
if (index < current.length) detachCurrentChild(state, current[index]);
const copy = ensureCopy(state) as unknown[];
defineData(copy, String(index), stored);
if (index === current.length) state.structural = true;
else markArrayIndex(state, index);
syncArrayTarget(state);
return true;
}
if (supplied === undefined) return deleteObjectProperty(state, property);
const stored = clonePlacement(supplied, state.context);
const current = currentValue(state) as Record<string, unknown>;
if (Object.hasOwn(current, property) && !isContainer(stored) && Object.is(current[property], stored)) return true;
if (Object.hasOwn(current, property)) detachCurrentChild(state, current[property]);
const copy = ensureCopy(state) as Record<string, unknown>;
defineData(copy, property, stored);
return true;
}
function writeArrayLength(state: DraftState, supplied: unknown): boolean {
const probe: unknown[] = [];
Reflect.set(probe, "length", supplied);
const next = probe.length;
const current = (currentValue(state) as unknown[]).length;
if (next === current) return true;
const copy = ensureCopy(state) as unknown[];
if (next < current) copy.length = next;
else {
copy.length = next;
for (let index = current; index < next; index++) copy[index] = null;
}
state.structural = true;
syncArrayTarget(state);
return true;
}
function deleteObjectProperty(state: DraftState, property: string): boolean {
const current = currentValue(state);
if (!Object.hasOwn(current, property)) return true;
detachCurrentChild(state, (current as Record<string, unknown>)[property]);
Reflect.deleteProperty(ensureCopy(state), property);
return true;
}
function detachCurrentChild(parent: DraftState, value: unknown): void {
if (!isContainer(value)) return;
const child = draftState(value) ?? parent.context.states.get(value);
if (child?.parent !== parent) return;
const children = parent.children;
if (children === child) parent.children = undefined;
else if (children instanceof Map && child.key !== undefined && children.get(child.key) === child) {
children.delete(child.key);
}
child.parent = undefined;
child.key = undefined;
}
function arrayIndex(property: PropertyKey): number | undefined {
if (typeof property !== "string" || property === "") return undefined;
const index = Number(property);
if (!Number.isInteger(index) || index < 0 || index >= 4_294_967_295 || String(index) !== property) return undefined;
return index;
}
function mutatorState(receiver: unknown): DraftState | undefined {
if (!isContainer(receiver)) return undefined;
return draftState(receiver);
}
const MUTATORS = {
push(this: unknown, ...args: unknown[]): number {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.push, this, args) as number;
assertActive(state.context);
if (args.length === 0) return (currentValue(state) as unknown[]).length;
const target = ensureCopy(state) as unknown[];
for (const item of cloneItems(args, state.context)) defineData(target, String(target.length), item);
state.structural = true;
syncArrayTarget(state);
return target.length;
},
pop(this: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.pop, this, []);
assertActive(state.context);
const current = currentValue(state) as unknown[];
if (current.length === 0) return undefined;
const target = ensureCopy(state) as unknown[];
const value = target[target.length - 1];
target.length -= 1;
state.structural = true;
syncArrayTarget(state);
return draftValue(state.context, value, state, target.length);
},
shift(this: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.shift, this, []);
assertActive(state.context);
const current = currentValue(state) as unknown[];
if (current.length === 0) return undefined;
const target = ensureCopy(state) as unknown[];
const value = Reflect.apply(Array.prototype.shift, target, []);
state.structural = true;
syncArrayTarget(state);
return draftValue(state.context, value, state, 0);
},
unshift(this: unknown, ...args: unknown[]): number {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.unshift, this, args) as number;
assertActive(state.context);
if (args.length === 0) return (currentValue(state) as unknown[]).length;
const target = ensureCopy(state) as unknown[];
const items = cloneItems(args, state.context);
insertChunked(target, 0, items);
state.structural = true;
syncArrayTarget(state);
return target.length;
},
splice(this: unknown, ...args: unknown[]): unknown[] {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.splice, this, args) as unknown[];
assertActive(state.context);
const length = (currentValue(state) as unknown[]).length;
const start = args.length === 0 ? 0 : clampIndex(toIntegerOrInfinity(args[0]), length);
const remove =
args.length === 0
? 0
: args.length === 1
? length - start
: Math.min(Math.max(toIntegerOrInfinity(args[1]), 0), length - start);
const inputs = cloneItems(args.slice(2), state.context);
if (remove === 0 && inputs.length === 0) return [];
const target = ensureCopy(state) as unknown[];
const removed = spliceCapturedLength(target, start, remove, inputs, length);
state.structural = true;
syncArrayTarget(state);
return removed.map((value, offset) => draftValue(state.context, value, state, start + offset));
},
sort(this: unknown, comparator?: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.sort, this, [comparator]);
assertActive(state.context);
if (comparator !== undefined && typeof comparator !== "function")
throw new TypeError("Comparator must be a function");
const target = ensureCopy(state) as unknown[];
state.structural = true;
Reflect.apply(Array.prototype.sort, target, [
(left: unknown, right: unknown): number => {
const draftedLeft = draftValue(state.context, left, state);
const draftedRight = draftValue(state.context, right, state);
if (typeof comparator === "function") {
// Array.prototype.sort performs ToNumber on the return value. Returning
// it directly preserves native bigint TypeError behavior.
return Reflect.apply(comparator, undefined, [draftedLeft, draftedRight]) as number;
}
const leftString = String(draftedLeft);
const rightString = String(draftedRight);
return leftString < rightString ? -1 : leftString > rightString ? 1 : 0;
},
]);
syncArrayTarget(state);
return state.proxy;
},
reverse(this: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.reverse, this, []);
assertActive(state.context);
const target = ensureCopy(state) as unknown[];
if (target.length > 1) {
Reflect.apply(Array.prototype.reverse, target, []);
state.structural = true;
}
return state.proxy;
},
fill(this: unknown, supplied: unknown, startArg?: unknown, endArg?: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.fill, this, [supplied, startArg, endArg]);
assertActive(state.context);
const length = (currentValue(state) as unknown[]).length;
const start = startArg === undefined ? 0 : clampIndex(toIntegerOrInfinity(startArg), length);
const end = endArg === undefined ? length : clampIndex(toIntegerOrInfinity(endArg), length);
if (end <= start) return state.proxy;
const target = ensureCopy(state) as unknown[];
for (let index = start; index < end; index++) target[index] = clonePlacement(supplied, state.context);
state.structural = true;
return state.proxy;
},
copyWithin(this: unknown, targetArg: unknown, startArg: unknown, endArg?: unknown): unknown {
const state = mutatorState(this);
if (state === undefined) return Reflect.apply(Array.prototype.copyWithin, this, [targetArg, startArg, endArg]);
assertActive(state.context);
const length = (currentValue(state) as unknown[]).length;
const targetIndex = clampIndex(toIntegerOrInfinity(targetArg), length);
const start = clampIndex(toIntegerOrInfinity(startArg), length);
const end = endArg === undefined ? length : clampIndex(toIntegerOrInfinity(endArg), length);
const count = Math.min(Math.max(end - start, 0), length - targetIndex);
if (count === 0) return state.proxy;
const source = (currentValue(state) as unknown[]).slice(start, start + count);
const target = ensureCopy(state) as unknown[];
for (let offset = 0; offset < count; offset++)
target[targetIndex + offset] = cloneLogical(source[offset], state.context);
state.structural = true;
return state.proxy;
},
};
function cloneItems(values: readonly unknown[], context: DraftContext): unknown[] {
return values.map((value) => clonePlacement(value, context));
}
function clonePlacement(value: unknown, _context: DraftContext): unknown {
if (!isContainer(value)) return value;
const state = draftState(value);
return state === undefined ? cloneJson(value) : cloneState(state);
}
/** Fast trusted strict-JSON clone. Inputs are caller-guaranteed plain, dense, acyclic trees. */
export function cloneJson<T>(value: T): T {
if (!isContainer(value)) return value;
if (Array.isArray(value)) return value.map((child) => cloneJson(child)) as T;
const result = Object.create(Object.getPrototypeOf(value)) as Record<string, unknown>;
for (const key of Object.keys(value)) defineData(result, key, cloneJson((value as Record<string, unknown>)[key]));
return result as T;
}
function cloneState(state: DraftState): Container {
assertActive(state.context);
const context = state.context;
const current = state.copy ?? state.base;
if (Array.isArray(current)) {
const result = new Array<unknown>(current.length);
for (let index = 0; index < current.length; index++) result[index] = cloneLogical(current[index], context);
return result;
}
const result = Object.create(Object.getPrototypeOf(current)) as Record<string, unknown>;
for (const key of Object.keys(current)) defineData(result, key, cloneLogical(current[key], context));
return result;
}
function cloneLogical(value: unknown, context: DraftContext): unknown {
if (!isContainer(value)) return value;
const proxyState = draftState(value);
if (proxyState !== undefined) return cloneState(proxyState);
const state = context.states.get(value);
return state === undefined ? cloneJson(value) : cloneState(state);
}
function finalize(state: DraftState): Container {
if (state.finalized !== undefined) return state.finalized;
if (!state.dirty) return state.base;
const current = state.copy ?? state.base;
let result = current;
if (Array.isArray(current) && state.structural) {
for (let index = 0; index < current.length; index++) {
const value = current[index];
if (!isContainer(value)) continue;
const child = draftState(value) ?? state.context.states.get(value);
if (child === undefined || !child.dirty) continue;
const next = finalize(child);
if (next === value) continue;
if (result === state.base) result = shallowCopy(state.base);
(result as unknown[])[index] = next;
}
} else {
forEachDirtyChild(state, (key, child) => {
const value = (current as Record<ChildKey, unknown>)[key];
if (value !== child.base && draftState(value) !== child) return;
const next = finalize(child);
if (next === value) return;
if (result === state.base) result = shallowCopy(state.base);
defineData(result, String(key), next);
if (Array.isArray(current) && typeof key === "number") markArrayIndex(state, key);
});
}
if (result !== state.base && shallowEqual(state.base, result)) result = state.base;
state.finalized = result;
if (
Array.isArray(state.base) &&
Array.isArray(result) &&
result !== state.base &&
!state.structural &&
state.base.length === result.length
) {
const base = state.base as unknown[];
const output = result as unknown[];
const changed = changedIndexList(state);
if (changed !== undefined) {
const keys = changed.filter((index) => index < base.length && index < output.length);
state.context.hints.set(result, { base: state.base as JsonValue[], keys });
}
}
return result;
}
function forEachDirtyChild(state: DraftState, visit: (key: ChildKey, child: DraftState) => void): void {
const children = state.children;
if (children === undefined) return;
if (children instanceof Map) {
for (const [key, child] of children) if (child.dirty) visit(key, child);
} else if (children.dirty && children.key !== undefined) visit(children.key, children);
}
function changedIndexList(state: DraftState): number[] | undefined {
const changed = state.changedIndices;
if (changed === undefined) return [];
// The differ only consumes narrow hints. Avoid materializing and sorting a
// dense set that it would immediately discard.
if (changed instanceof Set) {
if (changed.size >= 2_048) return undefined;
return [...changed].sort((left, right) => left - right);
}
return [changed];
}
function shallowEqual(left: Container, right: Container): boolean {
if (Array.isArray(left) !== Array.isArray(right)) return false;
if (Array.isArray(left) && Array.isArray(right) && left.length !== right.length) return false;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if (leftKeys.length !== rightKeys.length) return false;
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
for (const key of leftKeys) {
if (!Object.hasOwn(right, key) || !Object.is(leftRecord[key], rightRecord[key])) return false;
}
return true;
}
function defineData(target: object, key: PropertyKey, value: unknown): void {
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
}
function spliceCapturedLength(
target: unknown[],
start: number,
remove: number,
items: readonly unknown[],
capturedLength: number,
): unknown[] {
const removed = new Array<unknown>(remove);
for (let offset = 0; offset < remove; offset++) {
defineData(removed, String(offset), denseArrayValue(target, start + offset));
}
if (items.length < remove) {
for (let source = start + remove; source < capturedLength; source++) {
defineData(target, String(source - remove + items.length), denseArrayValue(target, source));
}
} else if (items.length > remove) {
for (let source = capturedLength - 1; source >= start + remove; source--) {
defineData(target, String(source - remove + items.length), denseArrayValue(target, source));
}
}
for (let offset = 0; offset < items.length; offset++) defineData(target, String(start + offset), items[offset]);
target.length = capturedLength - remove + items.length;
return removed;
}
function denseArrayValue(target: readonly unknown[], index: number): unknown {
return index < target.length && Object.hasOwn(target, index) && target[index] !== undefined ? target[index] : null;
}
function insertChunked(target: unknown[], index: number, values: readonly unknown[]): void {
for (let offset = 0; offset < values.length; offset += MAX_NATIVE_ARRAY_INSERT_ITEMS) {
target.splice(index + offset, 0, ...values.slice(offset, offset + MAX_NATIVE_ARRAY_INSERT_ITEMS));
}
}
function toIntegerOrInfinity(value: unknown): number {
const number = +(value as number);
if (Number.isNaN(number) || number === 0) return 0;
return Number.isFinite(number) ? Math.trunc(number) : number;
}
function clampIndex(value: number, length: number): number {
if (value === Number.NEGATIVE_INFINITY) return 0;
if (value < 0) return Math.max(length + value, 0);
return Math.min(value, length);
}
function syncArrayTarget(state: DraftState): void {
(state.target as unknown[]).length = ((state.copy ?? state.base) as unknown[]).length;
}
function assertWritable(state: DraftState, property: PropertyKey): void {
assertActive(state.context);
if (typeof property === "symbol") throw new TypeError("Symbol writes are not supported");
}
function assertActive(context: DraftContext): void {
if (!context.active) throw new TypeError("Cannot use a draft outside its change callback");
}
function isContainer(value: unknown): value is Container {
return value !== null && typeof value === "object";
}
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return (
((typeof value === "object" && value !== null) || typeof value === "function") &&
typeof (value as { then?: unknown }).then === "function"
);
}

View file

@ -0,0 +1,20 @@
export type { JsonValue, NonEmptyPath, Op, Path, PathRef, Seg, WireOp } from "../index.ts";
export {
apply,
assertSafePath,
assertValidOp,
assertValidWireOp,
decoder,
encoder,
isBase,
isReplace,
overlap,
PathError,
RESERVED_SEGMENTS,
UnsafePathError,
} from "../index.ts";
export { applyImmutable } from "./apply-immutable.ts";
export { diffRevisions } from "./diff.ts";
export type { Draft } from "./draft.ts";
export type { Change, Prepared, Tracker } from "./tracker.ts";
export { track } from "./tracker.ts";

View file

@ -0,0 +1,225 @@
import type { JsonValue } from "../../types.ts";
import type { Op } from "../index.ts";
import { applyImmutable } from "./apply-immutable.ts";
import { diffRevisions } from "./diff.ts";
import { cloneJson, createDraftTransaction, type Draft, type DraftTransaction, type ProduceMetadata } from "./draft.ts";
export interface Prepared<T extends object> {
readonly base: T;
readonly value: T;
readonly ops: readonly Op[];
abort(): void;
}
export interface Change<T extends object> {
readonly state: Draft<T>;
prepare(): Prepared<T>;
abort(): void;
}
export interface Tracker<T extends object> {
readonly value: T;
readonly revision: number;
beginChange(): Change<T>;
prepareReplace(value: T): Prepared<T>;
adopt(prepared: Prepared<T>): void;
}
type PreparedMetadata = {
readonly owner: object;
readonly revision: number;
status: "prepared" | "consumed" | "aborted";
};
const PREPARED = new WeakMap<object, PreparedMetadata>();
class PreparedImpl<T extends object> implements Prepared<T> {
readonly base: T;
readonly value: T;
readonly ops: readonly Op[];
constructor(metadata: PreparedMetadata, base: T, value: T, ops: readonly Op[]) {
this.base = base;
this.value = value;
this.ops = ops;
PREPARED.set(this, metadata);
Object.freeze(this);
}
abort(): void {
const metadata = PREPARED.get(this)!;
if (metadata.status === "prepared") metadata.status = "aborted";
}
}
class ChangeImpl<T extends object> implements Change<T> {
readonly state: Draft<T>;
#tracker: TrackerImpl<T> | undefined;
#transaction: DraftTransaction<T> | undefined;
#preparedMetadata: PreparedMetadata | undefined;
#status: "open" | "prepared" | "aborted" = "open";
constructor(tracker: TrackerImpl<T>, transaction: DraftTransaction<T>) {
this.#tracker = tracker;
this.#transaction = transaction;
this.state = transaction.state;
}
prepare(): Prepared<T> {
if (this.#status !== "open") throw new Error("Change has already been settled");
const tracker = this.#tracker!;
const transaction = this.#transaction!;
try {
const prepared = tracker.prepareDraft(transaction);
this.#preparedMetadata = PREPARED.get(prepared)!;
this.#status = "prepared";
return prepared;
} catch (error) {
this.#status = "aborted";
throw error;
} finally {
this.#tracker = undefined;
this.#transaction = undefined;
}
}
abort(): void {
if (this.#status === "aborted") return;
if (this.#status === "prepared") {
if (this.#preparedMetadata?.status === "prepared") this.#preparedMetadata.status = "aborted";
this.#preparedMetadata = undefined;
this.#status = "aborted";
return;
}
this.#status = "aborted";
const tracker = this.#tracker!;
const transaction = this.#transaction!;
this.#tracker = undefined;
this.#transaction = undefined;
try {
transaction.abort();
} finally {
tracker.releaseChange();
}
}
}
class TrackerImpl<T extends object> implements Tracker<T> {
readonly #owner = {};
#value: T;
#revision = 0;
#changing = false;
constructor(initial: T) {
this.#value = initial;
}
get value(): T {
return this.#value;
}
get revision(): number {
return this.#revision;
}
beginChange(): Change<T> {
this.#reserveChange();
try {
return new ChangeImpl(this, createDraftTransaction(this.#value));
} catch (error) {
this.#changing = false;
throw error;
}
}
prepareReplace(value: T): Prepared<T> {
this.#reserveChange();
try {
return this.#prepare({ value, hints: new WeakMap() }, true);
} finally {
this.#changing = false;
}
}
adopt(prepared: Prepared<T>): void {
const metadata = PREPARED.get(prepared as object);
if (metadata?.owner !== this.#owner) throw new Error("Prepared change belongs to a different tracker");
if (metadata.status === "consumed") throw new Error("Prepared change has already been used");
if (metadata.status === "aborted") throw new Error("Prepared change has been aborted");
if (this.#changing) throw new Error("Cannot adopt while a change is active");
if (metadata.revision !== this.#revision || this.#value !== prepared.base)
throw new Error("Prepared change is stale");
metadata.status = "consumed";
this.#value = prepared.value;
this.#revision += 1;
}
prepareDraft(transaction: DraftTransaction<T>): PreparedImpl<T> {
try {
return this.#prepare(transaction.finish());
} finally {
this.#changing = false;
}
}
releaseChange(): void {
this.#changing = false;
}
#reserveChange(): void {
if (this.#changing) throw new Error("Tracker already has an active change");
this.#changing = true;
}
#prepare(produced: ProduceMetadata<T>, replacement = false): PreparedImpl<T> {
const base = this.#value;
const operations = diffRevisions(
base as unknown as JsonValue,
produced.value as unknown as JsonValue,
produced.hints,
);
if (operations.length === 0) {
return new PreparedImpl({ owner: this.#owner, revision: this.#revision, status: "prepared" }, base, base, []);
}
// Trusted ownership rule: value and public ops are independent immutable
// transfers. Draft revisions are reconstructed from a private detached batch:
// differ payloads may reference retained base objects, so applying them directly
// could introduce aliases. Replacements are already caller-transferred roots.
// No freeze or validation runs.
const value = replacement ? produced.value : applyImmutable(base, detachOperations(operations));
return new PreparedImpl(
{ owner: this.#owner, revision: this.#revision, status: "prepared" },
base,
value,
detachOperations(operations),
);
}
}
/** Take O(1) ownership of an alias-free mutable strict-JSON root. */
export function track<T extends object>(initial: T): Tracker<T> {
return new TrackerImpl(initial);
}
function detachOperations(operations: readonly Op[]): Op[] {
return operations.map((operation): Op => {
switch (operation[0]) {
case "r":
return ["r", cloneJson(operation[1])];
case "s":
return ["s", operation[1], cloneJson(operation[2])];
case "p":
return ["p", operation[1], operation[2], operation[3], operation[4].map((value) => cloneJson(value))];
case "m":
return ["m", operation[1], operation[2].slice()];
case "d":
return operation;
case "a":
return operation;
case "t":
return operation;
default:
throw new TypeError(`Unknown operation ${(operation as readonly [unknown])[0]}`);
}
});
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { setImmediate } from "node:timers/promises";
import { diffRevisions } from "../../src/delta/diff.ts";
import { type Draft, produceWithMetadata } from "../../src/delta/draft.ts";
import type { Op } from "../../src/delta/index.ts";
import { JsonRevisionStore } from "../../src/delta/value.ts";
import type { JsonValue } from "../../src/types.ts";
type Scenario = "import" | "committed-read" | "draft-read" | "sparse" | "queue" | "sort" | "dense" | "unshift";
type Row = { id: number; value: number; payload: string };
type Document = { rows: Row[] };
const MiB = 1024 * 1024;
const scenario = process.argv[2] as Scenario;
const size = Number(process.argv[3] ?? 100_000);
const trial = Number(process.argv[4] ?? 0);
function fixture(count: number): Document {
return {
rows: Array.from({ length: count }, (_, id) => ({ id, value: id, payload: `row-${id}` })),
};
}
function sum(document: Document | Draft<Document>): number {
let checksum = 0;
for (let index = 0; index < document.rows.length; index++) checksum += document.rows[index]!.value;
return checksum;
}
async function gc(): Promise<void> {
assert.ok(global.gc, "benchmark worker requires --expose-gc");
for (let index = 0; index < 3; index++) {
await setImmediate();
global.gc();
}
}
await gc();
const baselineHeap = process.memoryUsage().heapUsed;
let input: Document | undefined = fixture(size);
if (scenario === "sort") input.rows.reverse();
const store = new JsonRevisionStore();
const importStart = performance.now();
let value = store.import(input);
const importMs = performance.now() - importStart;
input = undefined;
await gc();
const readyHeap = process.memoryUsage().heapUsed;
let peakHeap = readyHeap;
let mutateMs = 0;
let prepareMs = 0;
let serializeMs = 0;
let adoptMs = 0;
let opBytes = 0;
let operationCount = 0;
let checksum = 0;
const queueOperations = Math.min(size, 1_000);
if (scenario === "committed-read") {
const start = performance.now();
checksum = sum(value);
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
} else if (scenario !== "import") {
const draftStart = performance.now();
const produced = produceWithMetadata(value, (draft) => {
const start = performance.now();
switch (scenario) {
case "draft-read":
checksum = sum(draft);
break;
case "sparse":
for (let index = 0; index < size; index += 1_000) draft.rows[index]!.value = -index;
checksum = draft.rows.at(-1)!.value;
break;
case "queue":
for (let index = 0; index < queueOperations; index++) {
draft.rows.shift();
draft.rows.push({ id: size + index, value: index, payload: "queue" });
}
checksum = draft.rows.length;
break;
case "sort":
draft.rows.sort((left, right) => left.id - right.id);
checksum = draft.rows[0]!.id;
break;
case "dense":
for (let index = 0; index < size; index++) draft.rows[index]!.value = -index;
checksum = draft.rows.at(-1)!.value;
break;
case "unshift": {
const inserted = Array.from({ length: 100_000 }, (_, id) => ({ id: -id, value: id, payload: "inserted" }));
Reflect.apply(draft.rows.unshift, draft.rows, inserted);
checksum = draft.rows.length;
break;
}
}
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
});
const draftFinalizeMs = performance.now() - draftStart - mutateMs;
const prepareStart = performance.now();
const next = produced.value === value ? value : store.commit(produced.value, produced.owned);
const operations: readonly Op[] =
next === value ? [] : diffRevisions(value as unknown as JsonValue, next as unknown as JsonValue);
prepareMs = draftFinalizeMs + performance.now() - prepareStart;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const serializeStart = performance.now();
const serialized = JSON.stringify(operations);
serializeMs = performance.now() - serializeStart;
opBytes = Buffer.byteLength(serialized);
operationCount = operations.length;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const adoptStart = performance.now();
value = next;
adoptMs = performance.now() - adoptStart;
}
await gc();
const retainedHeap = process.memoryUsage().heapUsed;
console.log(
JSON.stringify({
mode: "head",
scenario,
size,
trial,
queueOperations,
importMs,
mutateMs,
prepareMs,
serializeMs,
adoptMs,
opBytes,
operationCount,
checksum,
readyMiB: (readyHeap - baselineHeap) / MiB,
transientMiB: (peakHeap - readyHeap) / MiB,
retainedMiB: (retainedHeap - baselineHeap) / MiB,
maxRssMiB: process.resourceUsage().maxRSS / 1024,
}),
);

View file

@ -0,0 +1,134 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
type Mode = "astra" | "delta" | "head";
type Scenario = "import" | "committed-read" | "draft-read" | "sparse" | "queue" | "sort" | "dense" | "unshift";
type Result = {
mode: Mode;
scenario: Scenario;
size: number;
trial: number;
queueOperations: number;
importMs: number;
mutateMs: number;
prepareMs: number;
serializeMs: number;
adoptMs: number;
opBytes: number;
operationCount: number;
checksum: number;
readyMiB: number;
transientMiB: number;
retainedMiB: number;
maxRssMiB: number;
};
type Failure = { mode: Mode; scenario: Scenario; trial: number; stderr: string };
const scenarios: readonly Scenario[] = [
"import",
"committed-read",
"draft-read",
"sparse",
"queue",
"sort",
"dense",
"unshift",
];
const modes: readonly Mode[] = ["astra", "delta", "head"];
const quick = process.argv.includes("--quick");
const size = quick ? 10_000 : 100_000;
const trials = quick ? 1 : 3;
const root = fileURLToPath(new URL("../../../..", import.meta.url));
const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
const headRoot = join(tmpdir(), `chord-astra-head-${process.pid}`);
const headFiles = [
"packages/chord/src/state/diff.ts",
"packages/chord/src/state/draft.ts",
"packages/chord/src/state/value.ts",
"packages/chord/src/delta/index.ts",
"packages/chord/src/types.ts",
] as const;
function writeHeadFixture(): string {
for (const path of headFiles) {
const destination = join(headRoot, path);
mkdirSync(dirname(destination), { recursive: true });
writeFileSync(destination, execFileSync("git", ["show", `HEAD:${path}`], { cwd: root }));
}
const worker = join(headRoot, "packages/chord/test/delta-astra/benchmark-head.worker.ts");
mkdirSync(dirname(worker), { recursive: true });
const source = readFileSync(new URL("./benchmark-head.worker.ts", import.meta.url), "utf8")
.replaceAll("../../src/delta/diff.ts", "../../src/state/diff.ts")
.replaceAll("../../src/delta/draft.ts", "../../src/state/draft.ts")
.replaceAll("../../src/delta/value.ts", "../../src/state/value.ts");
writeFileSync(worker, source);
writeFileSync(join(headRoot, "package.json"), JSON.stringify({ type: "module" }));
return worker;
}
function average(values: readonly number[]): number | null {
return values.length === 0 ? null : values.reduce((total, value) => total + value, 0) / values.length;
}
const headWorker = writeHeadFixture();
const currentWorker = fileURLToPath(new URL("./benchmark.worker.ts", import.meta.url));
const results: Result[] = [];
const failures: Failure[] = [];
try {
for (let trial = 0; trial < trials; trial++) {
for (const scenario of scenarios) {
for (const mode of modes) {
const worker = mode === "head" ? headWorker : currentWorker;
const args =
mode === "head"
? [scenario, String(size), String(trial)]
: [mode, scenario, String(size), String(trial)];
const child = spawnSync(process.execPath, ["--expose-gc", "--max-old-space-size=4096", worker, ...args], {
cwd: mode === "head" ? headRoot : root,
encoding: "utf8",
timeout: 300_000,
maxBuffer: 10 * 1024 * 1024,
});
if (child.status !== 0) {
const failure = { mode, scenario, trial, stderr: child.stderr } satisfies Failure;
failures.push(failure);
console.log(JSON.stringify(failure));
continue;
}
const result = JSON.parse(child.stdout) as Result;
results.push(result);
console.log(JSON.stringify(result));
}
}
}
} finally {
rmSync(headRoot, { recursive: true, force: true });
}
const summary = scenarios.flatMap((scenario) =>
modes.map((mode) => {
const selected = results.filter((result) => result.mode === mode && result.scenario === scenario);
return {
mode,
scenario,
importMs: average(selected.map((result) => result.importMs)),
mutateMs: average(selected.map((result) => result.mutateMs)),
prepareMs: average(selected.map((result) => result.prepareMs)),
serializeMs: average(selected.map((result) => result.serializeMs)),
adoptMs: average(selected.map((result) => result.adoptMs)),
opBytes: average(selected.map((result) => result.opBytes)),
readyMiB: average(selected.map((result) => result.readyMiB)),
transientMiB: average(selected.map((result) => result.transientMiB)),
retainedMiB: average(selected.map((result) => result.retainedMiB)),
maxRssMiB: average(selected.map((result) => result.maxRssMiB)),
failures: failures.filter((failure) => failure.mode === mode && failure.scenario === scenario).length,
};
}),
);
const report = { node: process.version, head, size, trials, results, failures, summary };
const output = join(tmpdir(), "chord-astra-benchmark.json");
writeFileSync(output, JSON.stringify(report, null, 2));
console.log(JSON.stringify({ output, node: process.version, head, size, trials, summary }));

View file

@ -0,0 +1,142 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { setImmediate } from "node:timers/promises";
import { track as trackAstra } from "../../src/delta/astra/index.ts";
import type { Draft } from "../../src/delta/draft.ts";
import type { Op } from "../../src/delta/index.ts";
import { track as trackDelta } from "../../src/delta/tracker.ts";
type Mode = "astra" | "delta";
type Scenario = "import" | "committed-read" | "draft-read" | "sparse" | "queue" | "sort" | "dense" | "unshift";
type Row = { id: number; value: number; payload: string };
type Document = { rows: Row[] };
type PreparedLike = { readonly value: Readonly<Document>; readonly ops: readonly Op[] };
type ChangeLike = { readonly state: Draft<Document>; prepare(): PreparedLike };
type TrackerLike = { readonly value: Document; beginChange(): ChangeLike; adopt(prepared: PreparedLike): void };
const MiB = 1024 * 1024;
const mode = process.argv[2] as Mode;
const scenario = process.argv[3] as Scenario;
const size = Number(process.argv[4] ?? 100_000);
const trial = Number(process.argv[5] ?? 0);
function fixture(count: number): Document {
return {
rows: Array.from({ length: count }, (_, id) => ({ id, value: id, payload: `row-${id}` })),
};
}
function sum(document: Document | Draft<Document>): number {
let checksum = 0;
for (let index = 0; index < document.rows.length; index++) checksum += document.rows[index]!.value;
return checksum;
}
async function gc(): Promise<void> {
assert.ok(global.gc, "benchmark worker requires --expose-gc");
for (let index = 0; index < 3; index++) {
await setImmediate();
global.gc();
}
}
await gc();
const baselineHeap = process.memoryUsage().heapUsed;
let input: Document | undefined = fixture(size);
if (scenario === "sort") input.rows.reverse();
const importStart = performance.now();
const tracker = (mode === "astra" ? trackAstra(input) : trackDelta(input)) as TrackerLike;
const importMs = performance.now() - importStart;
input = undefined;
await gc();
const readyHeap = process.memoryUsage().heapUsed;
let peakHeap = readyHeap;
let mutateMs = 0;
let prepareMs = 0;
let serializeMs = 0;
let adoptMs = 0;
let opBytes = 0;
let operationCount = 0;
let checksum = 0;
const queueOperations = Math.min(size, 1_000);
if (scenario === "committed-read") {
const start = performance.now();
checksum = sum(tracker.value);
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
} else if (scenario !== "import") {
let change: ChangeLike | undefined = tracker.beginChange();
const start = performance.now();
switch (scenario) {
case "draft-read":
checksum = sum(change.state);
break;
case "sparse":
for (let index = 0; index < size; index += 1_000) change.state.rows[index]!.value = -index;
checksum = change.state.rows.at(-1)!.value;
break;
case "queue":
for (let index = 0; index < queueOperations; index++) {
change.state.rows.shift();
change.state.rows.push({ id: size + index, value: index, payload: "queue" });
}
checksum = change.state.rows.length;
break;
case "sort":
change.state.rows.sort((left, right) => left.id - right.id);
checksum = change.state.rows[0]!.id;
break;
case "dense":
for (let index = 0; index < size; index++) change.state.rows[index]!.value = -index;
checksum = change.state.rows.at(-1)!.value;
break;
case "unshift": {
const inserted = Array.from({ length: 100_000 }, (_, id) => ({ id: -id, value: id, payload: "inserted" }));
Reflect.apply(change.state.rows.unshift, change.state.rows, inserted);
checksum = change.state.rows.length;
break;
}
}
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const prepareStart = performance.now();
let prepared: PreparedLike | undefined = change.prepare();
prepareMs = performance.now() - prepareStart;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const serializeStart = performance.now();
const serialized = JSON.stringify(prepared.ops);
serializeMs = performance.now() - serializeStart;
opBytes = Buffer.byteLength(serialized);
operationCount = prepared.ops.length;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const adoptStart = performance.now();
tracker.adopt(prepared);
adoptMs = performance.now() - adoptStart;
change = undefined;
prepared = undefined;
}
await gc();
const retainedHeap = process.memoryUsage().heapUsed;
console.log(
JSON.stringify({
mode,
scenario,
size,
trial,
queueOperations,
importMs,
mutateMs,
prepareMs,
serializeMs,
adoptMs,
opBytes,
operationCount,
checksum,
readyMiB: (readyHeap - baselineHeap) / MiB,
transientMiB: (peakHeap - readyHeap) / MiB,
retainedMiB: (retainedHeap - baselineHeap) / MiB,
maxRssMiB: process.resourceUsage().maxRSS / 1024,
}),
);

View file

@ -0,0 +1,27 @@
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
describe("astra overlay retention", () => {
it.each([
"aborted",
"dropped-prepared",
"settled-change",
"stale-unprepared",
"stale-held",
"held-revoked",
"registry-churn",
])(
"releases or invalidates %s",
(scenario) => {
const child = spawnSync(
process.execPath,
["--expose-gc", fileURLToPath(new URL("./retention.worker.ts", import.meta.url)), scenario],
{ encoding: "utf8", timeout: 60_000 },
);
expect(child.error, child.stderr).toBeUndefined();
expect(child.status, `${child.stdout}\n${child.stderr}`).toBe(0);
},
65_000,
);
});

View file

@ -0,0 +1,106 @@
import assert from "node:assert/strict";
import { setImmediate } from "node:timers/promises";
import { track } from "../../src/delta/astra/index.ts";
async function collect(reference: WeakRef<object>): Promise<void> {
assert.ok(global.gc, "worker requires --expose-gc");
for (let attempt = 0; attempt < 40; attempt++) {
await setImmediate();
global.gc();
if (reference.deref() === undefined) return;
}
assert.fail("overlay data is still retained");
}
const retainedRevoked: object[] = [];
const retainedSettledChanges: object[] = [];
const retainedStaleChanges: object[] = [];
const scenario = process.argv[2];
if (scenario === "aborted") {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const reference = new WeakRef(change.state.payload.rows);
change.abort();
await collect(reference);
assert.deepEqual(tracker.value, { payload: null });
} else if (scenario === "dropped-prepared") {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
const makeReference = (): WeakRef<object> => {
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const prepared = change.prepare();
return new WeakRef(prepared.value.payload!.rows);
};
const reference = makeReference();
await collect(reference);
assert.deepEqual(tracker.value, { payload: null });
} else if (scenario === "settled-change") {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
const makeReference = (): WeakRef<object> => {
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const prepared = change.prepare();
const reference = new WeakRef(prepared.value.payload!.rows);
retainedSettledChanges.push(change);
return reference;
};
const reference = makeReference();
await collect(reference);
assert.deepEqual(tracker.value, { payload: null });
} else if (scenario === "stale-unprepared") {
const setup = (): {
tracker: ReturnType<typeof track<{ payload: { rows: { value: number }[] } }>>;
reference: WeakRef<object>;
} => {
const initial = { payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) } };
const tracker = track(initial);
const reference = new WeakRef(initial.payload.rows);
retainedStaleChanges.push(tracker.beginChange());
const winner = tracker.prepareReplace({ payload: { rows: [{ value: -1 }] } });
tracker.adopt(winner);
return { tracker, reference };
};
const { tracker, reference } = setup();
await collect(reference);
assert.deepEqual(tracker.value, { payload: { rows: [{ value: -1 }] } });
assert.throws(() => (retainedStaleChanges[0] as { state: { payload: unknown } }).state.payload, TypeError);
} else if (scenario === "stale-held") {
const tracker = track({ child: { value: 0 } });
let prepared = (() => {
const change = tracker.beginChange();
change.state.child.value = 1;
return change.prepare();
})();
const held = prepared.value;
const wrapper = new WeakRef(prepared);
prepared = undefined as never;
await collect(wrapper);
const winner = tracker.prepareReplace({ child: { value: 2 } });
tracker.adopt(winner);
assert.throws(() => held.child.value, TypeError);
} else if (scenario === "held-revoked") {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const held = change.state.payload;
const child = new WeakRef(change.state.payload.rows);
retainedRevoked.push(held);
change.abort();
await collect(child);
assert.throws(() => held.rows, TypeError);
} else if (scenario === "registry-churn") {
const tracker = track({ value: 0 });
for (let index = 0; index < 100_000; index++) {
const change = tracker.beginChange();
change.state.value = index;
change.abort();
}
const winner = tracker.prepareReplace({ value: 1 });
tracker.adopt(winner);
assert.deepEqual(tracker.value, { value: 1 });
} else {
throw new Error(`unknown scenario: ${scenario}`);
}
console.log(JSON.stringify({ scenario, passed: true }));

View file

@ -0,0 +1,95 @@
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
type Mode = "cow" | "delta" | "astra";
type Scenario = "import" | "committed-read" | "draft-read" | "sparse" | "queue" | "sort" | "dense" | "unshift";
type Result = {
mode: Mode;
scenario: Scenario;
size: number;
trial: number;
queueOperations: number;
importMs: number;
mutateMs: number;
prepareMs: number;
serializeMs: number;
adoptMs: number;
opBytes: number;
operationCount: number;
checksum: number;
readyMiB: number;
transientMiB: number;
retainedMiB: number;
maxRssMiB: number;
};
type Failure = { mode: Mode; scenario: Scenario; trial: number; stderr: string };
const scenarios: readonly Scenario[] = [
"import",
"committed-read",
"draft-read",
"sparse",
"queue",
"sort",
"dense",
"unshift",
];
const modes: readonly Mode[] = ["cow", "delta", "astra"];
const quick = process.argv.includes("--quick");
const size = quick ? 10_000 : 100_000;
const trials = quick ? 1 : 3;
const root = fileURLToPath(new URL("../../../..", import.meta.url));
const worker = fileURLToPath(new URL("./benchmark.worker.ts", import.meta.url));
const results: Result[] = [];
const failures: Failure[] = [];
for (let trial = 0; trial < trials; trial++) {
for (const scenario of scenarios) {
for (const mode of modes) {
const child = spawnSync(
process.execPath,
["--expose-gc", "--max-old-space-size=4096", worker, mode, scenario, String(size), String(trial)],
{ cwd: root, encoding: "utf8", timeout: 300_000, maxBuffer: 10 * 1024 * 1024 },
);
if (child.status !== 0) {
const failure = { mode, scenario, trial, stderr: child.stderr } satisfies Failure;
failures.push(failure);
console.log(JSON.stringify(failure));
} else {
const result = JSON.parse(child.stdout) as Result;
results.push(result);
console.log(JSON.stringify(result));
}
}
}
}
const average = (values: readonly number[]): number | null =>
values.length === 0 ? null : values.reduce((total, value) => total + value, 0) / values.length;
const summary = scenarios.flatMap((scenario) =>
modes.map((mode) => {
const selected = results.filter((result) => result.mode === mode && result.scenario === scenario);
return {
mode,
scenario,
importMs: average(selected.map((result) => result.importMs)),
mutateMs: average(selected.map((result) => result.mutateMs)),
prepareMs: average(selected.map((result) => result.prepareMs)),
serializeMs: average(selected.map((result) => result.serializeMs)),
adoptMs: average(selected.map((result) => result.adoptMs)),
opBytes: average(selected.map((result) => result.opBytes)),
operationCount: average(selected.map((result) => result.operationCount)),
readyMiB: average(selected.map((result) => result.readyMiB)),
transientMiB: average(selected.map((result) => result.transientMiB)),
retainedMiB: average(selected.map((result) => result.retainedMiB)),
maxRssMiB: average(selected.map((result) => result.maxRssMiB)),
};
}),
);
const output = join(tmpdir(), "chord-cow-benchmark.json");
writeFileSync(output, JSON.stringify({ node: process.version, size, trials, results, failures, summary }, null, 2));
console.log(JSON.stringify({ output, node: process.version, size, trials, failures: failures.length, summary }));

View file

@ -0,0 +1,145 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { setImmediate } from "node:timers/promises";
import { track as trackAstra } from "../../src/delta/astra/index.ts";
import { track as trackCow } from "../../src/delta/cow/index.ts";
import type { Draft } from "../../src/delta/draft.ts";
import type { Op } from "../../src/delta/index.ts";
import { track as trackDelta } from "../../src/delta/tracker.ts";
type Mode = "cow" | "astra" | "delta";
type Scenario = "import" | "committed-read" | "draft-read" | "sparse" | "queue" | "sort" | "dense" | "unshift";
type Row = { id: number; value: number; payload: string };
type Document = { rows: Row[] };
type PreparedLike = { readonly value: Readonly<Document>; readonly ops: readonly Op[] };
type ChangeLike = { readonly state: Draft<Document>; prepare(): PreparedLike };
type TrackerLike = { readonly value: Document; beginChange(): ChangeLike; adopt(prepared: PreparedLike): void };
const MiB = 1024 * 1024;
const mode = process.argv[2] as Mode;
const scenario = process.argv[3] as Scenario;
const size = Number(process.argv[4] ?? 100_000);
const trial = Number(process.argv[5] ?? 0);
function fixture(count: number): Document {
return {
rows: Array.from({ length: count }, (_, id) => ({ id, value: id, payload: `row-${id}` })),
};
}
function sum(document: Document | Draft<Document>): number {
let checksum = 0;
for (let index = 0; index < document.rows.length; index++) checksum += document.rows[index]!.value;
return checksum;
}
async function gc(): Promise<void> {
assert.ok(global.gc, "benchmark worker requires --expose-gc");
for (let index = 0; index < 3; index++) {
await setImmediate();
global.gc();
}
}
await gc();
const baselineHeap = process.memoryUsage().heapUsed;
let input: Document | undefined = fixture(size);
if (scenario === "sort") input.rows.reverse();
const importStart = performance.now();
const tracker = (
mode === "cow" ? trackCow(input) : mode === "astra" ? trackAstra(input) : trackDelta(input)
) as TrackerLike;
const importMs = performance.now() - importStart;
input = undefined;
await gc();
const readyHeap = process.memoryUsage().heapUsed;
let peakHeap = readyHeap;
let mutateMs = 0;
let prepareMs = 0;
let serializeMs = 0;
let adoptMs = 0;
let opBytes = 0;
let operationCount = 0;
let checksum = 0;
const queueOperations = Math.min(size, 1_000);
if (scenario === "committed-read") {
const start = performance.now();
checksum = sum(tracker.value);
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
} else if (scenario !== "import") {
let change: ChangeLike | undefined = tracker.beginChange();
const start = performance.now();
switch (scenario) {
case "draft-read":
checksum = sum(change.state);
break;
case "sparse":
for (let index = 0; index < size; index += 1_000) change.state.rows[index]!.value = -index;
checksum = change.state.rows.at(-1)!.value;
break;
case "queue":
for (let index = 0; index < queueOperations; index++) {
change.state.rows.shift();
change.state.rows.push({ id: size + index, value: index, payload: "queue" });
}
checksum = change.state.rows.length;
break;
case "sort":
change.state.rows.sort((left, right) => left.id - right.id);
checksum = change.state.rows[0]!.id;
break;
case "dense":
for (let index = 0; index < size; index++) change.state.rows[index]!.value = -index;
checksum = change.state.rows.at(-1)!.value;
break;
case "unshift": {
const inserted = Array.from({ length: 100_000 }, (_, id) => ({ id: -id, value: id, payload: "inserted" }));
Reflect.apply(change.state.rows.unshift, change.state.rows, inserted);
checksum = change.state.rows.length;
break;
}
}
mutateMs = performance.now() - start;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const prepareStart = performance.now();
let prepared: PreparedLike | undefined = change.prepare();
prepareMs = performance.now() - prepareStart;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const serializeStart = performance.now();
const serialized = JSON.stringify(prepared.ops);
serializeMs = performance.now() - serializeStart;
opBytes = Buffer.byteLength(serialized);
operationCount = prepared.ops.length;
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
const adoptStart = performance.now();
tracker.adopt(prepared);
adoptMs = performance.now() - adoptStart;
change = undefined;
prepared = undefined;
}
await gc();
const retainedHeap = process.memoryUsage().heapUsed;
console.log(
JSON.stringify({
mode,
scenario,
size,
trial,
queueOperations,
importMs,
mutateMs,
prepareMs,
serializeMs,
adoptMs,
opBytes,
operationCount,
checksum,
readyMiB: (readyHeap - baselineHeap) / MiB,
transientMiB: (peakHeap - readyHeap) / MiB,
retainedMiB: (retainedHeap - baselineHeap) / MiB,
maxRssMiB: process.resourceUsage().maxRSS / 1024,
}),
);

View file

@ -0,0 +1,501 @@
import { describe, expect, it } from "vitest";
import {
apply,
applyImmutable,
assertValidOp,
assertValidWireOp,
decoder,
encoder,
isBase,
type JsonValue,
type Op,
overlap,
track,
UnsafePathError,
type WireOp,
} from "../../src/delta/cow/index.ts";
describe("immutable tracker lifecycle", () => {
it("keeps a draft alive across await and adopts only a prepared change", async () => {
const input = { count: 1, nested: { text: "a" }, values: [1] };
const tracker = track(input);
expect(tracker.value).toBe(input);
const change = tracker.beginChange();
change.state.count = 2;
await Promise.resolve();
change.state.nested.text += "b";
change.state.values.push(2);
expect(tracker.value).toEqual(input);
const prepared = change.prepare();
expect(prepared.base).toBe(tracker.value);
expect(prepared.value).toEqual({ count: 2, nested: { text: "ab" }, values: [1, 2] });
expect(prepared.ops).toEqual([
["s", ["count"], 2],
["a", ["nested", "text"], "b"],
["p", ["values"], 1, 0, [2]],
]);
expect(() => change.state.count).toThrow(TypeError);
expect(tracker.value).toEqual(input);
tracker.adopt(prepared);
expect(tracker.value).toBe(prepared.value);
});
it("aborts and revokes without changing the committed value", () => {
const tracker = track({ child: { value: 1 } });
const change = tracker.beginChange();
const child = change.state.child;
child.value = 2;
change.abort();
expect(tracker.value.child.value).toBe(1);
expect(() => child.value).toThrow(TypeError);
expect(() => change.abort()).not.toThrow();
expect(() => change.prepare()).toThrow(/settled/);
const next = tracker.beginChange();
next.abort();
});
it("grows arrays with explicit nulls and revokes after preparation", () => {
const tracker = track({ values: [1, 2] as Array<number | null> });
const change = tracker.beginChange();
change.state.values.length = 4;
const prepared = change.prepare();
expect(prepared.value.values).toEqual([1, 2, null, null]);
expect(() => change.state.values).toThrow(TypeError);
tracker.adopt(prepared);
});
it("normalizes a deep no-op to exact previous identity", () => {
const tracker = track({ value: { nested: [1, 2] } });
const change = tracker.beginChange();
change.state.value = { nested: [1, 2] };
const prepared = change.prepare();
expect(prepared.value).toBe(prepared.base);
expect(prepared.ops).toEqual([]);
tracker.adopt(prepared);
expect(tracker.value).toBe(prepared.base);
});
it("takes O(1) replacement ownership and applies no-op normalization", () => {
const tracker = track({ nested: { value: 1 } });
const replacement = { nested: { value: 2 } };
const prepared = tracker.prepareReplace(replacement);
expect(prepared.value).toBe(replacement);
tracker.adopt(prepared);
const noOp = tracker.prepareReplace({ nested: { value: 2 } });
expect(noOp.value).toBe(tracker.value);
expect(noOp.ops).toEqual([]);
});
it("detaches operation payloads from candidate placements", () => {
const tracker = track({ rows: [] as { id: number }[] });
const change = tracker.beginChange();
change.state.rows.push({ id: 1 });
const prepared = change.prepare();
const splice = prepared.ops[0]!;
if (splice[0] !== "p") throw new Error("expected splice");
expect(splice[4][0]).not.toBe(prepared.value.rows[0]);
tracker.adopt(prepared);
tracker.value.rows[0]!.id = 2;
expect(splice[4]).toEqual([{ id: 1 }]);
});
it("rejects foreign, stale, repeated, and overlapping changes", () => {
const first = track({ value: 0 });
const second = track({ value: 0 });
const change = first.beginChange();
expect(() => first.beginChange()).toThrow(/active/);
change.state.value = 1;
const prepared = change.prepare();
expect(() => second.adopt(prepared)).toThrow(/different tracker/);
first.adopt(prepared);
expect(() => first.adopt(prepared)).toThrow(/already been used/);
const stale = first.prepareReplace({ value: 2 });
const winner = first.prepareReplace({ value: 3 });
first.adopt(winner);
expect(() => first.adopt(stale)).toThrow(/stale/);
expect(() => first.adopt(stale)).toThrow(/stale/);
});
it("invalidates a prepared result when its change is aborted", () => {
const tracker = track({ value: 0 });
const change = tracker.beginChange();
change.state.value = 1;
const prepared = change.prepare();
change.abort();
expect(() => tracker.adopt(prepared)).toThrow(/aborted/);
expect(() => change.abort()).not.toThrow();
});
it("makes competing same-base preparations stale after adopting a no-op", () => {
const tracker = track({ value: { count: 1 } });
const first = tracker.prepareReplace({ value: { count: 1 } });
const competing = tracker.prepareReplace({ value: { count: 1 } });
expect(first.value).toBe(first.base);
expect(competing.value).toBe(competing.base);
tracker.adopt(first);
expect(() => tracker.adopt(first)).toThrow(/already been used/);
expect(() => tracker.adopt(competing)).toThrow(/stale/);
});
it("keeps large prepareReplace array edits narrow", () => {
const rows = Array.from({ length: 10_000 }, (_, value) => ({ value, stable: { value } }));
const tracker = track({ rows });
const replacement = tracker.value.rows.slice();
replacement[5_000] = { value: -1, stable: replacement[5_000]!.stable };
const prepared = tracker.prepareReplace({ rows: replacement });
expect(prepared.ops).toEqual([["s", ["rows", 5_000, "value"], -1]]);
expect(JSON.stringify(prepared.ops).length).toBeLessThan(100);
expect(applyImmutable(prepared.base, prepared.ops)).toEqual(prepared.value);
});
});
describe("canonical strings", () => {
it("emits append and rolling-window operations", () => {
const tracker = track({ text: "abcdefgh" });
let change = tracker.beginChange();
change.state.text += "ij";
let prepared = change.prepare();
expect(prepared.ops).toEqual([["a", ["text"], "ij"]]);
tracker.adopt(prepared);
change = tracker.beginChange();
change.state.text = `${change.state.text.slice(3)}xyz`;
prepared = change.prepare();
expect(prepared.ops).toEqual([
["t", ["text"], 3],
["a", ["text"], "xyz"],
]);
});
it("finds bounded overlaps", () => {
expect(overlap("abcdefgh", "defghxyz", 65_536)).toBe(5);
expect(overlap("abcdef", "defghi", 0)).toBe(0);
});
});
describe("operation application and validation", () => {
it("applies mutable and immutable operations", () => {
const operations: Op[] = [
["a", ["text"], "b"],
["p", ["values"], 1, 1, [3, 4]],
["s", ["nested", "value"], 2],
];
const base = { text: "a", values: [1, 2], nested: { value: 1 }, stable: { value: 9 } };
const immutable = applyImmutable(base, operations);
expect(immutable).toEqual({ text: "ab", values: [1, 3, 4], nested: { value: 2 }, stable: { value: 9 } });
expect(base).toEqual({ text: "a", values: [1, 2], nested: { value: 1 }, stable: { value: 9 } });
expect(immutable.stable).toBe(base.stable);
expect(apply(structuredClone(base), operations)).toEqual(immutable);
});
it("supports root replacement, root splice, and permutations", () => {
const base: Op[] = [["r", [1, 2, 3]]];
expect(isBase(base)).toBe(true);
let value = apply<number[]>(undefined, base);
value = apply(value, [["p", [], 1, 1, [4]]]);
value = apply(value, [["m", [], [2, 0, 1]]]);
expect(value).toEqual([3, 1, 4]);
});
it("rejects unsafe and malformed paths", () => {
expect(() => apply({}, [["s", ["constructor", "prototype", "x"], true]])).toThrow(UnsafePathError);
expect(() => apply({ values: [1] }, [["s", ["values", 3], 2]])).toThrow(UnsafePathError);
expect(() => apply({ value: 1 }, [["a", ["value"], "x"]])).toThrow();
expect(() => assertValidOp(["s", "value", 1] as never)).toThrow();
expect(() => assertValidOp(["m", [], [0, 0]] as never)).toThrow(/bijection/);
});
it("validates immutable operations before traversing the target", () => {
let reads = 0;
const target = Object.defineProperty({}, "trap", {
enumerable: true,
get() {
reads += 1;
return {};
},
});
expect(() => applyImmutable(target, [["s", "bad-path", 1] as never])).toThrow(/path/);
expect(reads).toBe(0);
});
it("validates decoded and wire vocabularies separately", () => {
expect(() => assertValidOp(["s", ["value"], 1])).not.toThrow();
expect(() => assertValidOp(["s", 1] as never)).toThrow();
expect(() => assertValidWireOp(["s", 1])).not.toThrow();
expect(() => assertValidWireOp(["#", 0, ["value"]])).not.toThrow();
});
});
describe("codec", () => {
it("interns paths, omits adjacent paths, and round-trips", () => {
const path = ["nested", "text"] as const;
const enc = encoder();
const dec = decoder();
const first: Op[] = [
["t", path, 1],
["a", path, "x"],
];
expect(dec.decode(enc.encode(first))).toEqual(first);
const second: Op[] = [["a", path, "y"]];
const wire = enc.encode(second);
expect(wire).toEqual([
["#", 0, path],
["a", 0, "y"],
]);
expect(dec.decode(wire)).toEqual(second);
});
it("resets path dictionaries on a base", () => {
const enc = encoder();
const path = ["value"] as const;
enc.encode([["s", path, 1]]);
enc.encode([["s", path, 2]]);
expect(enc.encode([["r", { value: 3 }]])).toEqual([["r", { value: 3 }]]);
expect(enc.encode([["s", path, 4]])).toEqual([["s", path, 4]]);
});
it("rejects unresolved short forms and unsafe interned paths", () => {
expect(() => decoder().decode([["a", "x"]])).toThrow();
const wire = [
["#", 0, ["__proto__"]],
["s", 0, true],
] as unknown as WireOp[];
expect(() => decoder().decode(wire)).toThrow(UnsafePathError);
});
it("omits an adjacent repeated path", () => {
const enc = encoder();
const path = ["value"] as const;
expect(
enc.encode([
["s", path, 1],
["s", path, 2],
]),
).toEqual([
["s", path, 1],
["s", 2],
]);
});
it("interns on second use rather than first", () => {
const enc = encoder();
const path = ["a", "deep"] as const;
expect(enc.encode([["a", path, "1"]])).toEqual([["a", path, "1"]]);
expect(enc.encode([["a", path, "2"]])).toEqual([
["#", 0, path],
["a", 0, "2"],
]);
});
it("does not collide paths containing null characters", () => {
const operations: Op[] = [
["s", ["a\u0000b"], 1],
["s", ["a", "b"], 2],
];
expect(decoder().decode(encoder().encode(operations))).toEqual(operations);
});
it("clears decoder ids on a base batch", () => {
const dec = decoder();
dec.decode([
["#", 0, ["a"]],
["a", 0, "1"],
]);
dec.decode([["r", { a: "" }]]);
expect(() => dec.decode([["a", 0, "2"]])).toThrow();
});
it("makes batches after a base self-contained", () => {
const enc = encoder();
const path = ["a", "deep"] as const;
enc.encode([["a", path, "1"]]);
enc.encode([["a", path, "2"]]);
const base = enc.encode([["r", { a: { deep: "x" } }]]);
const after = enc.encode([["a", path, "3"]]);
expect(after).toEqual([["a", path, "3"]]);
const dec = decoder();
expect(dec.decode(base)).toEqual([["r", { a: { deep: "x" } }]]);
expect(dec.decode(after)).toEqual([["a", path, "3"]]);
});
it("round-trips deterministic mixed operation streams", () => {
const batches: Op[][] = Array.from({ length: 100 }, (_, index) => [
["s", ["rows", index, "value"], index],
["a", ["output"], String(index)],
["p", ["tail"], index, 0, [index]],
]);
const enc = encoder();
const dec = decoder();
expect(batches.map((batch) => dec.decode(enc.encode(batch)))).toEqual(batches);
});
});
describe("immutable application ownership", () => {
it("does not mutate a replacement payload targeted by a later operation", () => {
const replacement = { nested: { value: 1 } };
const next = applyImmutable<{ nested: { value: number } }>(undefined, [
["r", replacement],
["s", ["nested", "value"], 2],
]);
expect(replacement.nested.value).toBe(1);
expect(next.nested.value).toBe(2);
});
it("adopts a mutable replacement payload rather than copying it", () => {
const batch: Op[] = [["r", { value: 0 }]];
const first = apply<{ value: number }>(undefined, batch);
const second = apply<{ value: number }>(undefined, batch);
first.value = 1;
expect(second.value).toBe(1);
});
});
describe("path and prototype safety", () => {
it("rejects constructor walks and forbidden interned paths", () => {
expect(() => apply({}, JSON.parse('[["s",["constructor","prototype","gadget"],true]]'))).toThrow();
expect(({} as Record<string, unknown>).gadget).toBeUndefined();
const wire = [
["#", 0, ["__proto__", "w"]],
["s", 0, true],
] as unknown as WireOp[];
expect(() => decoder().decode(wire)).toThrow();
});
it("does not run inherited setters", () => {
Object.defineProperty(Object.prototype, "trap", {
set() {
throw new Error("inherited setter ran");
},
configurable: true,
});
try {
expect(() => apply({}, [["s", ["trap"], 1]])).not.toThrow();
expect(apply({}, [["s", ["trap"], 1]])).toEqual({ trap: 1 });
} finally {
delete (Object.prototype as Record<string, unknown>).trap;
}
});
it("allows reserved names inside values without prototype pollution", () => {
const value = JSON.parse('{"__proto__":{"z":1}}') as JsonValue;
const out = apply({}, [["s", ["value"], value]]);
expect(Object.hasOwn((out as { value: object }).value, "__proto__")).toBe(true);
expect(({} as Record<string, unknown>).z).toBeUndefined();
});
it("imports own properties without invoking inherited setters", () => {
const root = {} as Record<string, JsonValue>;
Object.defineProperty(root, "trap", { value: 1, writable: true, enumerable: true, configurable: true });
Object.defineProperty(Object.prototype, "trap", {
set() {
throw new Error("inherited setter ran");
},
configurable: true,
});
try {
expect(() => track(root)).not.toThrow();
} finally {
delete (Object.prototype as Record<string, unknown>).trap;
}
let value: { values: number[] } | undefined;
Object.defineProperty(Array.prototype, "0", {
set() {
throw new Error("inherited array setter ran");
},
configurable: true,
});
try {
value = track({ values: [1, 2] }).value;
} finally {
delete (Array.prototype as unknown as Record<number, unknown>)[0];
}
expect(value).toEqual({ values: [1, 2] });
});
});
describe("array index safety", () => {
it("writes an existing index and appends exactly one past the end", () => {
expect(apply({ values: [1, 2, 3] }, [["s", ["values", 1], 9]])).toEqual({ values: [1, 9, 3] });
expect(apply({ values: [1, 2, 3] }, [["s", ["values", 3], 9]])).toEqual({ values: [1, 2, 3, 9] });
});
it("rejects gaps, huge indices, and string-spelled indices", () => {
expect(() => apply({ values: [1, 2, 3] }, [["s", ["values", 5], 9]])).toThrow();
expect(() => apply({ values: [] }, [["s", ["values", 4_294_967_290], 1]])).toThrow();
expect(() => apply({ values: [1] }, [["s", ["values", "0"], 9]])).toThrow();
expect(() => apply({ values: ["a"] }, [["a", ["values", "0"], "b"]])).toThrow();
});
it("allows explicit growth values and rejects deletion past the end", () => {
expect(apply({ values: [1] }, [["p", ["values"], 1, 0, [null, null, 9]]])).toEqual({
values: [1, null, null, 9],
});
expect(() => apply({ values: [1] }, [["d", ["values", 1]]])).toThrow();
});
it("applies large splice payloads without spreading them at once", () => {
const items = Array<JsonValue>(300_000).fill(null);
const result = apply({ values: [] as JsonValue[] }, [["p", ["values"], 0, 0, items]]);
expect(result.values).toHaveLength(items.length);
});
});
describe("operation structure safety", () => {
it("rejects unknown verbs, malformed tuples, paths, and splice payloads", () => {
expect(() => apply({ value: 1 }, [["ZZZ", ["value"], 9] as never])).toThrow();
expect(() => apply({ values: [1] }, [["p", ["values"], 0, 0, "not-an-array"] as never])).toThrow();
expect(() => apply({ value: 1 }, [["s", "value", 9] as never])).toThrow();
expect(() => apply({ value: 1 }, [{ op: "s" } as never])).toThrow();
expect(() => apply({ value: 1 }, [null as never])).toThrow();
});
it("rejects invalid append and truncation operations", () => {
expect(() => apply({ value: 1 }, [["a", ["missing"], "x"]])).toThrow();
expect(() => apply({ value: 1 }, [["a", ["value"], "x"]])).toThrow();
expect(() => apply({ value: "abc" }, [["t", ["value"], -1]])).toThrow();
expect(() => decoder().decode([["t", ["value"], -1]])).toThrow();
});
it("clamps splice removal past the end", () => {
expect(apply({ values: [1, 2] }, [["p", ["values"], 0, 1e9, []]])).toEqual({ values: [] });
});
});
describe("operation assertions", () => {
it("accepts decoded operations and rejects wire-only forms", () => {
for (const operation of [
["r", { value: 1 }],
["s", ["value"], 1],
["d", ["value"]],
["a", ["value"], "x"],
["t", ["value"], 2],
["p", ["value"], 0, 0, []],
["m", ["value"], [0]],
] as never[]) {
expect(() => assertValidOp(operation)).not.toThrow();
}
for (const wireOnly of [
["s", 1],
["d"],
["a", "x"],
["t", 2],
["p", 0, 0, []],
["#", 0, ["value"]],
["s", 0, 1],
] as never[]) {
expect(() => assertValidOp(wireOnly)).toThrow();
expect(() => assertValidWireOp(wireOnly)).not.toThrow();
}
});
it("does not recursively inspect operation payloads", () => {
expect(() => assertValidOp(["s", ["value"], new Map()] as never)).not.toThrow();
expect(() => assertValidWireOp(["r", new Date()] as never)).not.toThrow();
});
});

View file

@ -0,0 +1,538 @@
import { describe, expect, it } from "vitest";
import { apply, applyImmutable, type Draft, type Op, track } from "../../src/delta/cow/index.ts";
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
describe("optimized COW transaction invariants", () => {
it("matches candidate, detached replay, and adopted value", () => {
const tracker = track({ rows: [{ value: 1 }], text: "a" });
const base = clone(tracker.value);
const change = tracker.beginChange();
change.state.rows[0]!.value = 2;
change.state.rows.push({ value: 3 });
change.state.text += "b";
const prepared = change.prepare();
const candidate = clone(prepared.value);
const operations = clone(prepared.ops);
expect(apply(base, operations)).toEqual(candidate);
tracker.adopt(prepared);
expect(tracker.value).toEqual(candidate);
});
it("clones external and draft placements independently", () => {
const tracker = track({
source: { value: 1 },
left: null as { value: number } | null,
right: null as { value: number } | null,
rows: [] as { value: number }[],
});
const external = { value: 2 };
const change = tracker.beginChange();
change.state.left = external;
change.state.right = change.state.source;
change.state.rows.push(external, external, change.state.source);
external.value = 9;
change.state.source.value = 7;
const prepared = change.prepare();
expect(prepared.value).toEqual({
source: { value: 7 },
left: { value: 2 },
right: { value: 1 },
rows: [{ value: 2 }, { value: 2 }, { value: 1 }],
});
expect(prepared.value.rows[0]).not.toBe(prepared.value.rows[1]);
});
it("distinguishes raw committed and draft-subtree assignments", () => {
const tracker = track({
source: { value: 1 },
rawCopy: null as { value: number } | null,
draftCopy: null as { value: number } | null,
});
const raw = tracker.value.source;
const change = tracker.beginChange();
change.state.source.value = 5;
change.state.rawCopy = raw;
change.state.draftCopy = change.state.source;
expect(change.prepare().value).toEqual({ source: { value: 5 }, rawCopy: { value: 1 }, draftCopy: { value: 5 } });
});
it("keeps Prepared.value authoritative and independent from public operations", () => {
const tracker = track({ rows: [] as { value: number }[] });
const change = tracker.beginChange();
change.state.rows.push({ value: 1 });
const prepared = change.prepare();
const splice = prepared.ops[0];
if (splice?.[0] !== "p") throw new Error("expected splice");
expect(splice[4][0]).not.toBe(prepared.value.rows[0]);
(splice[4][0] as { value: number }).value = 99;
expect(prepared.value.rows[0]!.value).toBe(1);
tracker.adopt(prepared);
expect(tracker.value).toBe(prepared.value);
expect(tracker.value.rows[0]!.value).toBe(1);
});
it("supports a MemoryStorage-style recursive clone", () => {
const storageClone = <T>(value: T): T => {
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(storageClone) as T;
const output: Record<string, unknown> = {};
for (const key of Object.keys(value)) output[key] = storageClone((value as Record<string, unknown>)[key]);
return output as T;
};
const tracker = track({ rows: [{ value: 1 }] });
const change = tracker.beginChange();
change.state.rows[0]!.value = 2;
change.state.rows.push({ value: 3 });
expect(storageClone(change.prepare().value)).toEqual({ rows: [{ value: 2 }, { value: 3 }] });
});
it("folds reserved-key changes without prototype pollution", () => {
const initial = JSON.parse('{"safe":{"__proto__":{"value":1}}}') as {
safe: { __proto__: { value: number } };
};
const tracker = track(initial);
const change = tracker.beginChange();
change.state.safe.__proto__.value = 2;
const prepared = change.prepare();
expect(applyImmutable(tracker.value, prepared.ops)).toEqual(prepared.value);
tracker.adopt(prepared);
expect(({} as Record<string, unknown>).value).toBeUndefined();
});
});
describe("logical placement cloning", () => {
it("does not let a detached dirty object handle overwrite its replacement", () => {
const tracker = track({ child: { value: 1 }, stable: { value: 9 } });
const change = tracker.beginChange();
const held = change.state.child;
held.value = 2;
change.state.child = { value: 3 };
held.value = 4;
const prepared = change.prepare();
expect(prepared.value).toEqual({ child: { value: 3 }, stable: { value: 9 } });
expect(apply(clone(tracker.value), clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
expect(tracker.value.child.value).toBe(3);
});
it("does not let a detached dirty array handle overwrite its replacement", () => {
const tracker = track({ rows: [{ value: 1 }, { value: 9 }] });
const change = tracker.beginChange();
const held = change.state.rows[0]!;
held.value = 2;
change.state.rows[0] = { value: 3 };
held.value = 4;
const prepared = change.prepare();
expect(prepared.value).toEqual({ rows: [{ value: 3 }, { value: 9 }] });
expect(apply(clone(tracker.value), clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
expect(tracker.value.rows[0]!.value).toBe(3);
});
it("copyWithin clones pending dirty descendants with primitive indices", () => {
const tracker = track({ rows: [{ nested: { value: 1 } }, { nested: { value: 2 } }] });
const change = tracker.beginChange();
change.state.rows[0]!.nested.value = 9;
change.state.rows.copyWithin(1, 0, 1);
const prepared = change.prepare();
expect(prepared.value).toEqual({ rows: [{ nested: { value: 9 } }, { nested: { value: 9 } }] });
expect(prepared.value.rows[0]).not.toBe(prepared.value.rows[1]);
expect(apply(clone(tracker.value), clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
});
it("keeps diff-aligned moved placements independent in the authoritative candidate", () => {
const tracker = track({
rows: [
{ id: 0, score: 0, label: "r-0" },
{ id: 1, score: 1 },
{ id: 2, score: 2 },
{ id: 2, score: 2 },
{ id: 200106, score: 16, label: "r-200106" },
{ id: 4, score: 200103 },
{ id: 5, score: 200002 },
{ id: 6, score: 6, label: "r-6" },
{ id: 7, score: 7 },
{ id: 200001, score: 13, label: "r-200001" },
],
text: "start-200100-200107",
meta: { count: 0 },
});
const base = clone(tracker.value);
const change = tracker.beginChange();
let held = change.state.rows[5]!;
change.state.rows.unshift({ id: 200200, score: 13 });
held.score += 1_000;
change.state.meta.count -= 2;
held = change.state.rows[3]!;
change.state.rows.unshift({ id: 200202, score: 15, label: "r-200202" });
held.score += 1_000;
change.state.text += "-200203";
change.state.rows[0] = { id: 200204, score: 12 };
change.state.rows[1] = change.state.rows[2]!;
change.state.text += "-200206";
change.state.rows.sort((left, right) => left.id - right.id || left.score - right.score);
const prepared = change.prepare();
expect(prepared.value.rows[0]!.id).toBe(0);
expect(prepared.value.rows[1]!.id).toBe(0);
expect(prepared.value.rows[0]).not.toBe(prepared.value.rows[1]);
expect(apply(base, clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
const next = tracker.beginChange();
const secondScore = next.state.rows[1]!.score;
next.state.rows[0]!.score = 99;
const nextPrepared = next.prepare();
expect(nextPrepared.value.rows[0]!.score).toBe(99);
expect(nextPrepared.value.rows[1]!.score).toBe(secondScore);
expect(nextPrepared.value.rows[0]).not.toBe(nextPrepared.value.rows[1]);
nextPrepared.abort();
});
it("fuzzes deeply-equal duplicate placements through movement and stable sort", () => {
for (let seed = 1; seed <= 60; seed++) {
const initial = {
rows: Array.from({ length: 20 }, (_, index) => ({
id: Math.floor(index / 2),
score: Math.floor(index / 2),
label: `row-${Math.floor(index / 2)}`,
})),
};
const tracker = track(initial);
const expected = clone(initial);
const change = tracker.beginChange();
const heldIndex = seed % expected.rows.length;
const expectedHeld = expected.rows[heldIndex]!;
const draftHeld = change.state.rows[heldIndex]!;
expected.rows.unshift({ id: 100 + seed, score: seed, label: `front-${seed}` });
change.state.rows.unshift({ id: 100 + seed, score: seed, label: `front-${seed}` });
expectedHeld.score += 1_000;
draftHeld.score += 1_000;
expected.rows.unshift({ id: 200 + seed, score: seed, label: `front2-${seed}` });
change.state.rows.unshift({ id: 200 + seed, score: seed, label: `front2-${seed}` });
const source = (seed * 7) % expected.rows.length;
let target = (seed * 11 + 3) % expected.rows.length;
if (target === source) target = (target + 1) % expected.rows.length;
expected.rows[target] = clone(expected.rows[source]!);
change.state.rows[target] = change.state.rows[source]!;
expected.rows.sort((left, right) => left.id - right.id || left.score - right.score);
change.state.rows.sort((left, right) => left.id - right.id || left.score - right.score);
const prepared = change.prepare();
expect(prepared.value, `candidate seed ${seed}`).toEqual(expected);
expect(new Set(prepared.value.rows).size, `identity seed ${seed}`).toBe(prepared.value.rows.length);
expect(apply(clone(initial), clone(prepared.ops)), `replay seed ${seed}`).toEqual(expected);
prepared.abort();
}
});
it("clones a dirty draft subtree from its source tracker context", () => {
const source = track({ subtree: { nested: { value: 1 }, rows: [{ value: 2 }] } });
const destination = track({ copy: null as { nested: { value: number }; rows: { value: number }[] } | null });
const sourceChange = source.beginChange();
sourceChange.state.subtree.nested.value = 7;
sourceChange.state.subtree.rows[0]!.value = 8;
const destinationChange = destination.beginChange();
destinationChange.state.copy = sourceChange.state.subtree;
const prepared = destinationChange.prepare();
expect(prepared.value.copy).toEqual({ nested: { value: 7 }, rows: [{ value: 8 }] });
expect(apply(clone(destination.value), clone(prepared.ops))).toEqual(prepared.value);
destination.adopt(prepared);
sourceChange.abort();
});
});
describe("native mutator parity", () => {
const compareNumbers = (
nativeMutation: (values: number[]) => void,
draftMutation: (values: number[]) => void,
): void => {
const native = [1, 2, 3];
nativeMutation(native);
const tracker = track({ values: [1, 2, 3] });
const change = tracker.beginChange();
draftMutation(change.state.values);
const prepared = change.prepare();
expect(prepared.value.values).toEqual(native);
tracker.adopt(prepared);
};
it("matches captured-length splice coercion when coercion mutates the array", () => {
compareNumbers(
(values) => {
values.splice(
{
valueOf() {
values.push(4);
return 1;
},
} as unknown as number,
1,
9,
);
},
(values) => {
values.splice(
{
valueOf() {
values.push(4);
return 1;
},
} as unknown as number,
1,
9,
);
},
);
});
it("keeps reentrant splice shrink results dense strict JSON", () => {
const tracker = track({ values: [1, 2, 3] as Array<number | null> });
const change = tracker.beginChange();
change.state.values.splice(
{
valueOf() {
change.state.values.length = 1;
return 0;
},
} as unknown as number,
0,
9,
);
const prepared = change.prepare();
expect(prepared.value.values).toEqual([9, 1, null, null]);
expect(Object.keys(prepared.value.values)).toEqual(["0", "1", "2", "3"]);
expect(prepared.value.values.every((value) => value !== undefined)).toBe(true);
expect(apply(clone(tracker.value), clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
});
it("matches fill and copyWithin coercion side effects", () => {
compareNumbers(
(values) => {
values.fill(7, {
valueOf() {
values.push(4);
return 1;
},
} as unknown as number);
},
(values) => {
values.fill(7, {
valueOf() {
values.push(4);
return 1;
},
} as unknown as number);
},
);
compareNumbers(
(values) => {
values.copyWithin(
{
valueOf() {
values.push(4);
return 1;
},
} as unknown as number,
0,
2,
);
},
(values) => {
values.copyWithin(
{
valueOf() {
values.push(4);
return 1;
},
} as unknown as number,
0,
2,
);
},
);
});
it("matches comparator direct-index and structural writes", () => {
type Row = { rank: number; touches: number };
const mutate = (values: Row[]): void => {
let changed = false;
values.sort((left, right) => {
left.touches += 1;
right.touches += 1;
if (!changed) {
changed = true;
values[0] = { rank: 9, touches: 0 };
values.push({ rank: 3, touches: 0 });
}
return left.rank - right.rank;
});
};
const native: Row[] = [
{ rank: 2, touches: 0 },
{ rank: 1, touches: 0 },
];
mutate(native);
const tracker = track({
values: [
{ rank: 2, touches: 0 },
{ rank: 1, touches: 0 },
],
});
const change = tracker.beginChange();
mutate(change.state.values);
const prepared = change.prepare();
expect(prepared.value.values).toEqual(native);
expect(apply(clone(tracker.value), clone(prepared.ops))).toEqual(prepared.value);
tracker.adopt(prepared);
});
it("rejects bigint comparator results like native sort", () => {
expect(() => [2, 1].sort(() => 1n as unknown as number)).toThrow(TypeError);
const tracker = track({ values: [2, 1] });
const change = tracker.beginChange();
expect(() => change.state.values.sort(() => 1n as unknown as number)).toThrow(TypeError);
change.abort();
expect(tracker.value.values).toEqual([2, 1]);
});
it("forwards borrowed mutators to ordinary generic array receivers", () => {
const tracker = track({ values: [1, 2, 3] });
const change = tracker.beginChange();
const receiver = [3, 1, 2];
expect(Reflect.apply(change.state.values.push, receiver, [4])).toBe(4);
expect(Reflect.apply(change.state.values.splice, receiver, [1, 1, 5])).toEqual([1]);
expect(Reflect.apply(change.state.values.sort, receiver, [(left: number, right: number) => left - right])).toBe(
receiver,
);
expect(Reflect.apply(change.state.values.fill, receiver, [7, 1, 2])).toBe(receiver);
expect(Reflect.apply(change.state.values.copyWithin, receiver, [2, 0, 1])).toBe(receiver);
expect(receiver).toEqual([2, 7, 2, 5]);
expect(change.state.values).toEqual([1, 2, 3]);
change.abort();
});
});
describe("batched immutable application", () => {
it("copies shared ancestors once while preserving untouched subtrees", () => {
const base = {
rows: [
{ a: 1, b: 2 },
{ a: 3, b: 4 },
],
stable: { value: 9 },
};
const operations: Op[] = [
["s", ["rows", 0, "a"], 10],
["s", ["rows", 0, "b"], 20],
["s", ["rows", 1, "a"], 30],
];
const result = applyImmutable(base, operations);
expect(result).toEqual({
rows: [
{ a: 10, b: 20 },
{ a: 30, b: 4 },
],
stable: { value: 9 },
});
expect(base).toEqual({
rows: [
{ a: 1, b: 2 },
{ a: 3, b: 4 },
],
stable: { value: 9 },
});
expect(result.stable).toBe(base.stable);
});
it("does not mutate replacement or insertion payloads targeted later in the batch", () => {
const replacement = { rows: [{ value: 1 }] };
const result = applyImmutable<typeof replacement>(undefined, [
["r", replacement],
["s", ["rows", 0, "value"], 2],
]);
expect(result.rows[0]!.value).toBe(2);
expect(replacement.rows[0]!.value).toBe(1);
});
});
type Item = { id: number; score: number };
type Document = { rows: Item[]; text: string; meta: { revision: number; label?: string } };
type MutableDocument = Document | Draft<Document>;
function mutate(document: MutableDocument, choice: number, value: number): void {
const item = (): Item => ({ id: value, score: value % 7 });
switch (choice) {
case 0:
document.text += `-${value}`;
break;
case 1:
document.rows.push(item());
break;
case 2:
document.rows.unshift(item());
break;
case 3:
if (document.rows.length > 0) document.rows.shift();
break;
case 4:
if (document.rows.length > 0) document.rows.pop();
break;
case 5: {
const index = document.rows.length === 0 ? 0 : value % (document.rows.length + 1);
document.rows.splice(index, document.rows.length === 0 ? 0 : value % 2, item());
break;
}
case 6:
document.rows.reverse();
break;
case 7:
document.rows.sort((left, right) => left.id - right.id);
break;
case 8:
if (document.rows.length > 0) document.rows[value % document.rows.length]!.score = value;
break;
case 9:
document.meta.revision += 1;
document.meta.label = `r-${value}`;
break;
default:
delete document.meta.label;
}
}
it("converges through randomized multi-operation transactions", () => {
for (let seed = 1; seed <= 30; seed++) {
let randomState = seed;
const random = (): number => {
randomState = (randomState + 0x6d2b79f5) | 0;
let value = Math.imul(randomState ^ (randomState >>> 15), 1 | randomState);
value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
};
const initial: Document = {
rows: Array.from({ length: 4 }, (_, id) => ({ id, score: 0 })),
text: "start",
meta: { revision: 0 },
};
const tracker = track(initial);
const expected = clone(initial);
for (let transaction = 0; transaction < 20; transaction++) {
const base = clone(tracker.value);
const change = tracker.beginChange();
for (let operation = 0; operation < 5; operation++) {
const choice = Math.floor(random() * 11);
const value = seed * 10_000 + transaction * 10 + operation;
mutate(expected, choice, value);
mutate(change.state, choice, value);
}
const prepared = change.prepare();
const candidate = clone(prepared.value);
expect(candidate, `candidate seed ${seed} transaction ${transaction}`).toEqual(expected);
expect(apply(base, clone(prepared.ops)), `replay seed ${seed} transaction ${transaction}`).toEqual(expected);
tracker.adopt(prepared);
expect(tracker.value, `adopt seed ${seed} transaction ${transaction}`).toEqual(expected);
}
}
}, 30_000);

View file

@ -0,0 +1,267 @@
import { describe, expect, it } from "vitest";
import {
applyImmutable,
assertValidOp,
assertValidWireOp,
decoder,
diffRevisions,
encoder,
type Op,
} from "../../src/delta/cow/index.ts";
import type { JsonValue } from "../../src/types.ts";
const expectDiff = (before: JsonValue, after: JsonValue, expected: unknown): void => {
const operations = diffRevisions(before, after);
expect(operations).toEqual(expected);
expect(applyImmutable(before, operations)).toEqual(after);
};
describe("immutable revision diff", () => {
it("emits sets and deletes", () => {
expectDiff({ keep: 1, change: 1, remove: true }, { keep: 1, change: 2, add: 3 }, [
["s", ["change"], 2],
["s", ["add"], 3],
["d", ["remove"]],
]);
});
it("emits string append and front truncation", () => {
expectDiff({ text: "hello" }, { text: "hello world" }, [["a", ["text"], " world"]]);
expectDiff({ text: "hello world" }, { text: "world" }, [["t", ["text"], 6]]);
expectDiff({ text: "abcdefgh" }, { text: "defghxyz" }, [
["t", ["text"], 3],
["a", ["text"], "xyz"],
]);
});
it("represents array insertion, removal, and shift with splices", () => {
const a = { id: "a" };
const b = { id: "b" };
const c = { id: "c" };
expectDiff({ values: [a, b] }, { values: [a, c, b] }, [["p", ["values"], 1, 0, [c]]]);
expectDiff({ values: [a, b, c] }, { values: [b, c] }, [["p", ["values"], 0, 1, []]]);
});
it("collapses a same-length queue update to two splices", () => {
const a = { id: "a" };
const b = { id: "b" };
const c = { id: "c" };
const d = { id: "d" };
expectDiff({ values: [a, b, c] }, { values: [b, c, d] }, [
["p", ["values"], 0, 1, []],
["p", ["values"], 2, 0, [d]],
]);
});
it("emits a permutation for a pure reorder", () => {
const a = { id: "a" };
const b = { id: "b" };
const c = { id: "c" };
expectDiff({ values: [a, b, c] }, { values: [c, a, b] }, [["m", ["values"], [2, 0, 1]]]);
});
it("normalizes reordered distinct deeply-equal objects to a no-op", () => {
const first = { nested: { value: 1 } };
const second = { nested: { value: 1 } };
expect(first).not.toBe(second);
expect(diffRevisions({ values: [first, second] }, { values: [second, first] })).toEqual([]);
});
it("validates and encodes permutations", () => {
const operations: Op[] = [
["m", ["values"], [2, 0, 1]],
["m", ["values"], [1, 2, 0]],
];
for (const operation of operations) assertValidOp(operation);
const wire = encoder().encode(operations);
expect(wire).toEqual([
["m", ["values"], [2, 0, 1]],
["m", [1, 2, 0]],
]);
for (const operation of wire) assertValidWireOp(operation);
expect(decoder().decode(wire)).toEqual(operations);
expect(() => assertValidOp(["m", ["values"], [0, 0]])).toThrow(/bijection/);
});
it("emits nothing for deeply equal reconstructed values", () => {
expect(diffRevisions({ value: { nested: [1, 2] } }, { value: { nested: [1, 2] } })).toEqual([]);
expect(diffRevisions({ values: [{ id: 1 }, { id: 2 }] }, { values: [{ id: 1 }, { id: 2 }] })).toEqual([]);
expect(diffRevisions({ values: [true, true, true] }, { values: [true, true, true] })).toEqual([]);
});
it("keeps a leaf edit inside a reconstructed array narrow", () => {
expectDiff(
{
values: [
{ id: 1, label: "one" },
{ id: 2, label: "two" },
],
},
{
values: [
{ id: 1, label: "one" },
{ id: 2, label: "changed" },
],
},
[["s", ["values", 1, "label"], "changed"]],
);
});
it("emits payload-free splices for scattered removals", () => {
const [a, b, c, d, e] = ["a", "b", "c", "d", "e"].map((id) => ({ id }));
expectDiff({ values: [a, b, c, d, e] }, { values: [a, c, e] }, [
["p", ["values"], 1, 1, []],
["p", ["values"], 2, 1, []],
]);
});
it.each([
["front", [1, 2, 3, 4], [2, 3, 4], [["p", ["values"], 0, 1, []]]],
["tail", [1, 2, 3, 4], [1, 2, 3], [["p", ["values"], 3, 1, []]]],
["middle", [1, 2, 3, 4], [1, 3, 4], [["p", ["values"], 1, 1, []]]],
["all", [1, 2, 3, 4], [], [["p", ["values"], 0, 4, []]]],
["none", [1, 2, 3, 4], [1, 2, 3, 4], []],
] as const)("encodes %s removal canonically", (_name, before, after, expected) => {
expectDiff({ values: [...before] }, { values: [...after] }, expected);
});
it("does not field-diff unrelated shifted objects with common fields", () => {
const before = [1, 2, 3].map((value) => ({ type: "row", value }));
const after = [2, 3, 4].map((value) => ({ type: "row", value }));
expectDiff({ values: before }, { values: after }, [
["p", ["values"], 0, 1, []],
["p", ["values"], 2, 0, [{ type: "row", value: 4 }]],
]);
});
it("does not treat coincidental id or key fields as structural identity", () => {
const left = { value: "left" };
const right = { value: "right" };
const before = [left, { id: 1, key: "a", value: "first" }, { id: 2, key: "b", value: "second" }, right];
const replacements = [
{ id: 2, key: "b", value: "edited-second" },
{ id: 1, key: "a", value: "edited-first" },
];
const after = [left, ...replacements, right];
expectDiff({ values: before }, { values: after }, [["p", ["values"], 1, 2, replacements]]);
});
it("combines removals, append, and a shared-subtree survivor edit", () => {
const a = { id: "a", stable: {}, detail: { text: "a" } };
const b = { id: "b", stable: {}, detail: { text: "b" } };
const c = { id: "c", stable: {}, detail: { text: "c" } };
const d = { id: "d", stable: {}, detail: { text: "d" } };
const changedC = { id: "c", stable: c.stable, detail: { text: "changed" } };
const appended = { id: "e", stable: {}, detail: { text: "e" } };
expectDiff({ values: [a, b, c, d] }, { values: [a, changedC, d, appended] }, [
["p", ["values"], 1, 1, []],
["a", ["values", 1, "detail", "text"], "hanged"],
["p", ["values"], 3, 0, [appended]],
]);
});
it.each([256 * 1024, 1024 * 1024])("does not retain a %i-byte removed-neighbor payload", (size) => {
const payload = "x".repeat(size);
const retained = [
{ id: "a", payload },
{ id: "b", payload },
{ id: "c", payload },
{ id: "d", payload },
{ id: "e", payload },
];
const before = Object.freeze({ values: Object.freeze(retained.slice()) });
const after = Object.freeze({ values: Object.freeze([retained[0]!, retained[2]!, retained[4]!]) });
const operations = diffRevisions(before as unknown as JsonValue, after as unknown as JsonValue);
expect(operations).toEqual([
["p", ["values"], 1, 1, []],
["p", ["values"], 2, 1, []],
]);
expect(JSON.stringify(operations).length).toBeLessThan(100);
expect(applyImmutable(before, operations)).toEqual(after);
expect(before.values).toEqual(retained);
});
it.each([1_001, 10_000])("keeps push, pop, and middle removal narrow at %,i items", (size) => {
const values = Array.from({ length: size }, (_, value) => ({ value }));
const appended = { value: size };
expectDiff({ values }, { values: [...values, appended] }, [["p", ["values"], size, 0, [appended]]]);
expectDiff({ values }, { values: values.slice(0, -1) }, [["p", ["values"], size - 1, 1, []]]);
const middle = Math.floor(size / 2);
expectDiff({ values }, { values: [...values.slice(0, middle), ...values.slice(middle + 1)] }, [
["p", ["values"], middle, 1, []],
]);
});
it("keeps forty-thousand-row sparse edits narrow", () => {
const values = Array.from({ length: 40_000 }, (_, value) => ({ value, stable: { value } }));
const after = values.slice();
const changed: number[] = [];
for (let index = 100; index < after.length; index += 400) {
after[index] = { value: -index, stable: values[index]!.stable };
changed.push(index);
}
const operations = diffRevisions({ values }, { values: after });
expect(operations).toEqual(changed.map((index) => ["s", ["values", index, "value"], -index]));
expect(JSON.stringify(operations).length).toBeLessThan(7_500);
expect(applyImmutable({ values }, operations)).toEqual({ values: after });
});
it("keeps a reconstructed large-array leaf edit narrow", () => {
const before = Array.from({ length: 1_000 }, (_, value) => ({ value, label: `row-${value}` }));
const after = structuredClone(before);
after[700]!.label = "changed";
const operations = diffRevisions({ values: before }, { values: after });
expect(operations).toEqual([["s", ["values", 700, "label"], "changed"]]);
expect(JSON.stringify(operations).length).toBeLessThan(100);
expect(applyImmutable({ values: before }, operations)).toEqual({ values: after });
});
it("splices an ambiguous equal-count moved-and-edited gap", () => {
const left = { value: "left" };
const right = { value: "right" };
const before = [left, { id: 1, value: "a" }, { id: 2, value: "b" }, right];
const replacements = [
{ id: 2, value: "edited" },
{ id: 1, value: "also-edited" },
];
const after = [left, ...replacements, right];
expectDiff({ values: before }, { values: after }, [["p", ["values"], 1, 2, replacements]]);
});
it("keeps a large rotation payload-free", () => {
const values = Array.from({ length: 10_000 }, (_, value) => ({ value }));
const rotated = [...values.slice(1_000), ...values.slice(0, 1_000)];
const operations = diffRevisions({ values }, { values: rotated });
expect(operations).toHaveLength(1);
expect(operations[0]?.[0]).toBe("m");
expect(JSON.stringify(operations).length).toBeLessThan(60_000);
expect(applyImmutable({ values }, operations)).toEqual({ values: rotated });
});
it("encodes five hundred unshifts without snapshotting retained rows", () => {
const retained = Array.from({ length: 10_000 }, (_, value) => ({ value, payload: "x".repeat(100) }));
const inserted = Array.from({ length: 500 }, (_, value) => ({ value: -value - 1 }));
const operations = diffRevisions({ values: retained }, { values: [...inserted, ...retained] });
expect(operations).toEqual([["p", ["values"], 0, 0, inserted]]);
expect(JSON.stringify(operations).length).toBeLessThan(20_000);
expect(applyImmutable({ values: retained }, operations)).toEqual({ values: [...inserted, ...retained] });
});
it("bounds wide-object operation emission with a root replacement", () => {
const before: Record<string, JsonValue> = {};
const after: Record<string, JsonValue> = {};
for (let index = 0; index < 20_000; index++) {
before[`field${index}`] = 0;
after[`field${index}`] = 1;
}
expect(diffRevisions(before, after)).toEqual([["r", after]]);
});
it("uses normalized array fallback without payload-cost policy", () => {
const before = { values: Array.from({ length: 40_000 }, () => 0) };
const after = { values: Array.from({ length: 40_000 }, () => 1) };
const operations = diffRevisions(before, after);
expect(operations).toEqual([["p", ["values"], 0, 40_000, after.values]]);
expect(applyImmutable(before, operations)).toEqual(after);
});
});

View file

@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import { applyImmutable, type Draft, track } from "../../src/delta/cow/index.ts";
describe("copy-on-write change drafts", () => {
it("copies only changed branches", () => {
const tracker = track({
changed: { count: 1, sibling: { value: "kept" } },
untouched: { value: 2 },
});
const base = tracker.value;
const change = tracker.beginChange();
const first = change.state.changed;
expect(change.state.changed).toBe(first);
change.state.changed.count = 3;
const prepared = change.prepare();
expect(prepared.value).not.toBe(base);
expect(prepared.value.changed).not.toBe(base.changed);
expect(prepared.value.changed.sibling).toBe(base.changed.sibling);
expect(prepared.value.untouched).toBe(base.untouched);
});
it("supports frozen bases, deletion, and native array mutators", () => {
const tracker = track({ optional: "remove" as string | undefined, values: [3, 1, 2] });
const change = tracker.beginChange();
delete change.state.optional;
change.state.values.push(4);
expect(change.state.values.pop()).toBe(4);
change.state.values.unshift(0);
expect(change.state.values.shift()).toBe(0);
expect(change.state.values.splice(1, 1, 5, 4)).toEqual([1]);
change.state.values.sort((left, right) => left - right);
change.state.values.reverse();
change.state.values.fill(9, 1, 3);
change.state.values.copyWithin(1, 0, 2);
const prepared = change.prepare();
expect(prepared.value).toEqual({ values: [5, 5, 9, 2] });
});
it("copies inserted draft values without aliasing their original handle", () => {
const tracker = track({ values: [{ value: 1 }, { value: 2 }] });
const change = tracker.beginChange();
const held = change.state.values[0]!;
change.state.values.unshift(held);
held.value = 9;
const prepared = change.prepare();
expect(prepared.value.values).toEqual([{ value: 1 }, { value: 9 }, { value: 2 }]);
expect(prepared.value.values[0]).not.toBe(prepared.value.values[1]);
});
it("keeps comparator edits when sorting object drafts", () => {
const tracker = track({
rows: [
{ rank: 2, comparisons: 0 },
{ rank: 1, comparisons: 0 },
],
});
const change = tracker.beginChange();
change.state.rows.sort((left, right) => {
left.comparisons++;
right.comparisons++;
return left.rank - right.rank;
});
const prepared = change.prepare();
expect(prepared.value.rows.map((row) => row.rank)).toEqual([1, 2]);
expect(prepared.value.rows.map((row) => row.comparisons)).toEqual([1, 1]);
});
it("drops writes through detached child handles", () => {
const tracker = track({ child: { value: "removed" }, items: [{ value: "removed" }, { value: "kept" }] });
const change = tracker.beginChange();
const child = change.state.child;
const shifted = change.state.items.shift();
delete (change.state as { child?: Draft<{ value: string }>; items: Draft<{ value: string }[]> }).child;
child.value = "detached child";
if (shifted !== undefined) shifted.value = "detached item";
expect(change.prepare().value).toEqual({ items: [{ value: "kept" }] });
});
it.each(["unshift", "splice"] as const)("inserts 100,000 items with %s without argument overflow", (method) => {
const tracker = track({ values: [-1] });
const change = tracker.beginChange();
const items = Array.from({ length: 100_000 }, (_, value) => value);
if (method === "unshift") Reflect.apply(change.state.values.unshift, change.state.values, items);
else Reflect.apply(change.state.values.splice, change.state.values, [1, 0, ...items]);
const prepared = change.prepare();
expect(prepared.value.values).toHaveLength(100_001);
expect(prepared.value.values[method === "unshift" ? 0 : 1]).toBe(0);
expect(prepared.value.values[method === "unshift" ? 99_999 : 100_000]).toBe(99_999);
expect(applyImmutable(prepared.base, prepared.ops)).toEqual(prepared.value);
});
it("rejects array holes without mutating the committed base", () => {
const tracker = track({ values: [1, 2] });
const change = tracker.beginChange();
expect(() => delete change.state.values[0]).toThrow(/holes/);
change.abort();
expect(tracker.value.values).toEqual([1, 2]);
});
});

View file

@ -0,0 +1,99 @@
import { expect, it } from "vitest";
import { applyImmutable, type Draft, type Op, track } from "../../src/delta/cow/index.ts";
type Item = { id: number; text: string; score: number };
type Document = {
items: Item[];
text: string;
meta: { revision: number; label?: string };
};
type MutableDocument = Document | Draft<Document>;
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
const random =
(seed: number): (() => number) =>
() => {
seed = (seed + 0x6d2b79f5) | 0;
let value = Math.imul(seed ^ (seed >>> 15), 1 | seed);
value = (value + Math.imul(value ^ (value >>> 7), 61 | value)) ^ value;
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
};
const mutate = (document: MutableDocument, choice: number, value: number): void => {
const item = (): Item => ({ id: value, text: `item-${value}`, score: value % 7 });
switch (choice) {
case 0:
document.text += `-${value}`;
break;
case 1:
document.text = `${document.text.slice(Math.min(2, document.text.length))}${value}`;
break;
case 2:
document.items.push(item());
break;
case 3:
document.items.unshift(item());
break;
case 4:
if (document.items.length > 0) document.items.shift();
break;
case 5:
if (document.items.length > 0) document.items.pop();
break;
case 6: {
const index = document.items.length === 0 ? 0 : value % (document.items.length + 1);
document.items.splice(index, document.items.length === 0 ? 0 : value % 2, item());
break;
}
case 7:
document.items.reverse();
break;
case 8:
document.items.sort((left, right) => left.id - right.id);
break;
case 9:
if (document.items.length > 0) document.items[value % document.items.length]!.score = value;
break;
case 10:
document.meta.revision += 1;
document.meta.label = `revision-${value}`;
break;
case 11:
delete document.meta.label;
break;
case 12:
if (document.items.length > 1) document.items[1] = clone(document.items[0]!);
break;
default:
for (let index = 0; index < Math.min(2, document.items.length); index++) document.items[index] = item();
}
};
it("converges across randomized prepared revisions", () => {
for (let seed = 1; seed <= 100; seed++) {
const rng = random(seed);
const initial: Document = {
items: Array.from({ length: 4 }, (_, id) => ({ id, text: `item-${id}`, score: 0 })),
text: "start",
meta: { revision: 0 },
};
const tracker = track(initial);
const expected = clone(initial);
let replica = clone(tracker.value);
for (let step = 0; step < 100; step++) {
const choice = Math.floor(rng() * 14);
const value = seed * 1_000 + step;
mutate(expected, choice, value);
const change = tracker.beginChange();
mutate(change.state, choice, value);
const prepared = change.prepare();
const operations: readonly Op[] = prepared.ops;
replica = applyImmutable(replica, operations);
tracker.adopt(prepared);
expect(tracker.value, `state seed ${seed} step ${step} choice ${choice}`).toEqual(expected);
expect(replica, `replica seed ${seed} step ${step} choice ${choice}`).toEqual(expected);
}
}
});

View file

@ -0,0 +1,28 @@
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
describe("delta change retention across GC jobs", () => {
it.each([
"aborted-payload",
"unadopted-prepared",
"draft-proxies",
"settled-lifecycle",
"obsolete-revisions",
"retained-settled-prepared",
"retained-settled-change",
"lifecycle-churn",
])(
"validates %s retention semantics",
(scenario) => {
const child = spawnSync(
process.execPath,
["--expose-gc", fileURLToPath(new URL("./retention.worker.ts", import.meta.url)), scenario],
{ encoding: "utf8", timeout: 60_000 },
);
expect(child.error, child.stderr).toBeUndefined();
expect(child.status, `${child.stdout}\n${child.stderr}`).toBe(0);
},
65_000,
);
});

View file

@ -0,0 +1,161 @@
import assert from "node:assert/strict";
import { setImmediate } from "node:timers/promises";
import { type Tracker, track } from "../../src/delta/cow/index.ts";
const retainedSettledObjects: object[] = [];
const retainedLargePrepared: object[] = [];
const retainedSettledChanges: object[] = [];
async function collect(ref: WeakRef<object>): Promise<void> {
assert.ok(global.gc, "worker requires --expose-gc");
for (let attempt = 0; attempt < 30; attempt++) {
await setImmediate();
global.gc();
if (ref.deref() === undefined) return;
}
assert.fail("released change data is still retained");
}
function abortedPayload(tracker: Tracker<{ payload: { rows: { value: number }[] } | null }>): WeakRef<object> {
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const reference = new WeakRef(change.state.payload.rows);
change.abort();
return reference;
}
function unadoptedPrepared(tracker: Tracker<{ payload: { rows: { value: number }[] } | null }>): WeakRef<object> {
const change = tracker.beginChange();
change.state.payload = { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) };
const prepared = change.prepare();
return new WeakRef(prepared.value.payload!.rows);
}
function draftProxies(tracker: Tracker<{ payload: { rows: { value: number }[] } | null }>): WeakRef<object> {
const change = tracker.beginChange();
change.state.payload = { rows: [{ value: 1 }] };
const reference = new WeakRef(change.state.payload.rows[0]!);
change.abort();
return reference;
}
async function assertRetained(reference: WeakRef<object>): Promise<void> {
assert.ok(global.gc, "worker requires --expose-gc");
for (let attempt = 0; attempt < 5; attempt++) {
await setImmediate();
global.gc();
}
assert.notEqual(reference.deref(), undefined, "retained Prepared must retain its immutable revisions");
}
function settledLifecycle(): { tracker: WeakRef<object>; future: WeakRef<object> } {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
const change = tracker.beginChange();
change.state.payload = { rows: [{ value: 1 }] };
const retainedPrepared = change.prepare();
tracker.adopt(retainedPrepared);
retainedSettledObjects.push(change, retainedPrepared);
const futurePrepared = tracker.prepareReplace({
payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) },
});
tracker.adopt(futurePrepared);
return {
tracker: new WeakRef(tracker),
future: new WeakRef(futurePrepared.value.payload!.rows),
};
}
const scenario = process.argv[2];
if (scenario === "settled-lifecycle") {
const references = settledLifecycle();
await collect(references.tracker);
await collect(references.future);
} else if (scenario === "retained-settled-prepared") {
const tracker = track({ payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) } });
const retain = (): { base: WeakRef<object>; value: WeakRef<object> } => {
const change = tracker.beginChange();
change.state.payload.rows[0]!.value = -1;
const prepared = change.prepare();
const references = {
base: new WeakRef(prepared.base.payload.rows),
value: new WeakRef(prepared.value.payload.rows),
};
tracker.adopt(prepared);
retainedLargePrepared.push(prepared);
return references;
};
const references = retain();
const replace = (): void => {
const replacement = tracker.prepareReplace({ payload: { rows: [{ value: 1 }] } });
tracker.adopt(replacement);
};
replace();
await assertRetained(references.base);
await assertRetained(references.value);
retainedLargePrepared.length = 0;
await collect(references.base);
await collect(references.value);
} else if (scenario === "retained-settled-change") {
const tracker = track({ payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) } });
const settleChange = (): { base: WeakRef<object>; value: WeakRef<object> } => {
const change = tracker.beginChange();
change.state.payload.rows[0]!.value = -1;
const prepared = change.prepare();
const references = {
base: new WeakRef(prepared.base.payload.rows),
value: new WeakRef(prepared.value.payload.rows),
};
tracker.adopt(prepared);
retainedSettledChanges.push(change);
return references;
};
const references = settleChange();
const replace = (): void => {
const replacement = tracker.prepareReplace({ payload: { rows: [{ value: 1 }] } });
tracker.adopt(replacement);
};
replace();
await collect(references.base);
await collect(references.value);
assert.equal(retainedSettledChanges.length, 1);
} else if (scenario === "lifecycle-churn") {
const tracker = track({ value: 0 });
for (let index = 0; index < 100_000; index++) {
const change = tracker.beginChange();
change.state.value = index;
change.abort();
}
const prepared = tracker.prepareReplace({ value: 1 });
tracker.adopt(prepared);
assert.deepEqual(tracker.value, { value: 1 });
} else if (scenario === "obsolete-revisions") {
const tracker = track({ payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) } });
const oldRoot = new WeakRef(tracker.value.payload.rows);
const replace = (value: number): void => {
const prepared = tracker.prepareReplace({ payload: { rows: [{ value }] } });
tracker.adopt(prepared);
};
for (let value = 0; value < 100; value++) replace(value);
await collect(oldRoot);
assert.deepEqual(tracker.value, { payload: { rows: [{ value: 99 }] } });
} else {
const tracker = track<{ payload: { rows: { value: number }[] } | null }>({ payload: null });
let reference: WeakRef<object>;
switch (scenario) {
case "aborted-payload":
reference = abortedPayload(tracker);
break;
case "unadopted-prepared":
reference = unadoptedPrepared(tracker);
break;
case "draft-proxies":
reference = draftProxies(tracker);
break;
default:
throw new Error(`unknown retention scenario: ${scenario}`);
}
await collect(reference);
assert.deepEqual(tracker.value, { payload: null });
}
console.log(JSON.stringify({ scenario, passed: true }));

View file

@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { track } from "../../src/delta/cow/index.ts";
describe("trusted immutable revision ownership", () => {
it("takes O(1) ownership of an alias-free strict-JSON root", () => {
const input = { left: { value: 1 }, right: { value: 2 } };
const tracker = track(input);
expect(tracker.value).toBe(input);
expect(tracker.value.left).toBe(input.left);
});
it("copies changed branches while sharing untouched branches", () => {
const tracker = track({ changed: { value: 1 }, retained: { value: 2 } });
const base = tracker.value;
const change = tracker.beginChange();
change.state.changed.value = 3;
const prepared = change.prepare();
expect(prepared.value).not.toBe(base);
expect(prepared.value.changed).not.toBe(base.changed);
expect(prepared.value.retained).toBe(base.retained);
tracker.adopt(prepared);
expect(tracker.value).toBe(prepared.value);
});
it("makes repeated placements independent", () => {
const tracker = track({ values: [] as { value: number }[] });
const change = tracker.beginChange();
const shared = { value: 1 };
change.state.values.push(shared, shared);
const prepared = change.prepare();
expect(prepared.value.values).toEqual([{ value: 1 }, { value: 1 }]);
expect(prepared.value.values[0]).not.toBe(prepared.value.values[1]);
});
});