feat(chord): make immutable delta tracker canonical

Replace the alternate tracker implementations with the optimized immutable overlay, validate draft placements as strict JSON, and document the ownership contract. Consolidate tracker tests and benchmarks around the canonical implementation.
This commit is contained in:
Mario Zechner 2026-09-24 15:06:54 +02:00
parent 5674690966
commit d5cba1d97c
54 changed files with 5639 additions and 7253 deletions

View file

@ -19,7 +19,6 @@ export interface Tracker<T extends object> {
class LegacyTracker<T extends object> implements Tracker<T> {
private readonly tracker: ChordTracker<T>;
private change: Change<T> | undefined;
private wrappers = new WeakMap<object, object>();
private forceBase = true;
constructor(initial: T) {
@ -28,7 +27,7 @@ class LegacyTracker<T extends object> implements Tracker<T> {
get state(): T {
this.change ??= this.tracker.beginChange();
return this.wrap(this.change.state as T);
return this.change.state as T;
}
get target(): T {
@ -44,7 +43,6 @@ class LegacyTracker<T extends object> implements Tracker<T> {
if (this.change !== undefined) {
const change = this.change;
this.change = undefined;
this.wrappers = new WeakMap();
prepared = change.prepare();
// Pico3 adopts before storage. A storage failure faults the owning Session.
this.tracker.adopt(prepared);
@ -59,30 +57,6 @@ class LegacyTracker<T extends object> implements Tracker<T> {
rebase(): void {
this.forceBase = true;
}
private wrap<V extends object>(draft: V): V {
const cached = this.wrappers.get(draft);
if (cached !== undefined) return cached as V;
const tracker = this;
const proxy = new Proxy(draft, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
if (typeof value === "function") {
return (...args: unknown[]) => {
const result = Reflect.apply(value, target, args);
return typeof result === "object" && result !== null ? tracker.wrap(result) : result;
};
}
return typeof value === "object" && value !== null ? tracker.wrap(value) : value;
},
set(target, key, value, receiver) {
if (value === undefined && !Array.isArray(target)) return Reflect.deleteProperty(target, key);
return Reflect.set(target, key, value, receiver);
},
});
this.wrappers.set(draft, proxy);
return proxy;
}
}
export function track<T extends object>(initial: T): Tracker<T> {

View file

@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import { applyImmutable } from "@earendil-works/chord/delta";
import { test } from "vitest";
import { track } from "../../../src/harness/pico3/legacy-tracker.ts";
test("preserves draft-sourced descendant edits without exposing tracker metadata", () => {
const tracker = track({
a: { child: { value: 1 } },
b: null as { child: { value: number } } | null,
rows: [{ child: { value: 1 } }, { child: { value: 2 } }],
});
let replica = applyImmutable<typeof tracker.target>(undefined, tracker.flush());
tracker.state.a.child.value = 2;
tracker.state.b = tracker.state.a;
tracker.state.rows[1] = tracker.state.rows[0]!;
tracker.state.rows[1]!.child.value = 3;
const operations = tracker.flush();
replica = applyImmutable(replica, operations);
assert.deepEqual(tracker.target, {
a: { child: { value: 2 } },
b: { child: { value: 2 } },
rows: [{ child: { value: 1 } }, { child: { value: 3 } }],
});
assert.deepEqual(replica, tracker.target);
assert.notEqual(tracker.target.a, tracker.target.b);
assert.notEqual(tracker.target.a.child, tracker.target.b?.child);
assert.notEqual(tracker.target.rows[0], tracker.target.rows[1]);
assert.notEqual(tracker.target.rows[0]!.child, tracker.target.rows[1]!.child);
});

View file

@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import { isDeepStrictEqual } from "node:util";
import type { Context } from "@earendil-works/chord";
import { apply } from "@earendil-works/chord/delta";
import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
import { onTestFinished, test } from "vitest";
import type { ConversationHandle } from "../../../src/harness/pico3/harness.ts";
@ -175,23 +177,20 @@ test("a self-head commit rewrites one transcript entry with matching entry/head
onTestFinished(() => env.close());
await env.root.write({ kind: "before" }, ctx);
const watch = await specWatch(env.root);
const before = structuredClone(watch.view);
let envelope: Envelope | undefined;
watch.start((next) => {
envelope = next;
});
await env.root.reset("fresh", ctx);
assert.ok(envelope);
const entryOps = envelope.ops.filter((op) => op[0] !== "r" && op[1][0] === "entries");
assert.deepEqual(
entryOps
.map((op) => {
if (op[0] !== "s") throw new Error(`Unexpected transcript operation ${op[0]}`);
assert.equal(op[1][1], 0);
return op[1][2];
})
.sort(),
["head", "id", "kind", "model"],
);
const after = apply(before, envelope.ops);
const previousEntry = watch.view.entries[0] as unknown as Record<string, unknown>;
const nextEntry = after.entries[0] as unknown as Record<string, unknown>;
const changedKeys = [...new Set([...Object.keys(previousEntry), ...Object.keys(nextEntry)])]
.filter((key) => !isDeepStrictEqual(previousEntry[key], nextEntry[key]))
.sort();
assert.deepEqual(changedKeys, ["head", "id", "kind", "model"]);
assert.deepEqual(
envelope.events.map((event) => event.type),
["head.moved", "entry.added"],

View file

@ -32,12 +32,12 @@ The design has a few connected pieces:
stable facade while a provider disconnects or is replaced.
- **Replicated state** exposes authoritative state to local and remote
connected consumers. Producers publish atomic copy-on-write transactions with
connected consumers. Producers publish atomic overlay transactions with
`change(context, callback)`; consumers receive complete immutable values. Draft
proxies exist only during the callback and are revoked afterward. Chord compares
the previous and next immutable revisions to produce one decoded operation batch,
while each remote client/state stream owns independent path-codec state. Replicas
become unready on disconnect or replacement until they are rehydrated.
proxies exist only during the callback and become unusable afterward. Preparation
materializes a structurally shared immutable candidate and one exact decoded
operation batch, while each remote client/state stream owns independent path-codec
state. Replicas become unready on disconnect or replacement until rehydrated.
- **Delta tracking** records and coalesces operations over tracked plain JSON.
It preserves common string and array operations, supports durable base
@ -89,26 +89,41 @@ outer protocol.
## Tracking JSON deltas
Import the standalone delta primitive from `@earendil-works/chord/delta`:
Import the standalone transactional tracker from `@earendil-works/chord/delta`:
```ts
import { apply, track } from "@earendil-works/chord/delta";
import { applyImmutable, track } from "@earendil-works/chord/delta";
const changes = track({ output: "", count: 0 });
changes.flush(); // opening base batch
changes.state.output += "done\n";
changes.state.count += 1;
const tracker = track({ output: "", count: 0 });
const change = tracker.beginChange();
change.state.output += "done\n";
change.state.count += 1;
const prepared = change.prepare();
const ops = changes.flush();
const replica = apply({ output: "", count: 0 }, ops);
tracker.adopt(prepared);
const replica = applyImmutable(prepared.base, prepared.ops);
```
The first flush is always a complete base batch. Later flushes contain path-based
changes. `applyImmutable()` applies those batches while preserving prior replica
revisions. Replicated state instead uses transaction-scoped copy-on-write drafts:
`tracker.value` is always the latest adopted immutable revision. Preparation does
not change authority; adoption validates the preparation and swaps the root pointer.
Assigned containers are copied by value and unchanged subtrees may be shared between
revisions.
The tracker uses trusted immutable ownership rather than defensive copying or
freezing. `track(initial)`, `prepareReplace(value)`, `replicatedState(initial)`, and
`replace(context, value)` take ownership of alias-free strict-JSON roots without
walking them. Callers must not mutate transferred or published data. Values placed
through drafts are already copied, so that walk also rejects non-strict JSON before
the draft changes. Published values are not frozen. In-process loopback consumers
may share their containers with the provider; mutating a consumed value violates
the contract and can corrupt authority. Clone or serialize at any mutable trust
boundary.
Replicated state provides the same model through a callback:
```ts
const status = env.replicatedState({ output: "", count: 0 });
const initial = { output: "", count: 0 };
const status = env.replicatedState(initial); // transfers ownership of initial
status.change(context, (draft) => {
draft.output += "done\n";
draft.count += 1;
@ -116,16 +131,14 @@ status.change(context, (draft) => {
```
A successful `change()` publishes exactly one atomic revision. If its callback
throws, the original value and sequence remain unchanged. Draft handles are revoked
when the callback returns and assigned containers are copied by value. Unchanged
subtrees are shared between immutable revisions. Chord derives string append and
front-truncate operations, array splices and permutations, sets, and deletes from
the two revisions; a large delta falls back to a complete snapshot. Remote
connection plumbing encodes each batch independently for every client/state
pairing. `replace(context, value)` publishes a detached complete value directly.
throws, the original value and sequence remain unchanged. Draft handles become
unusable when the callback returns. Chord emits string append and front-truncate
operations, array splices and permutations, sets, and deletes; large edit sets may
fold into a complete replacement. Remote connection plumbing encodes each batch
independently for every client/state pairing.
The standalone [Delta guide](src/delta/README.md) documents the lower-level mutable
tracker, which remains available separately from replicated state.
The standalone [Delta guide](src/delta/README.md) defines the complete ownership,
lifecycle, operation, and replica contracts.
## Bundling and loading facets

View file

@ -92,6 +92,10 @@ export function replicatedState<T>(
source: ReplicatedStateSource<T>,
options?: ReplicatedStateSourceOptions,
): AttachedReplicatedState<T>;
/**
* Create authoritative state by taking immutable ownership of an alias-free strict-JSON root.
* The caller must not mutate `initial` after this call.
*/
export function replicatedState<T extends object>(initial: T): MutableReplicatedState<T>;
export function replicatedState(
initialOrSource: object | ReplicatedStateSource<unknown>,

View file

@ -1,408 +1,177 @@
# Chord Delta
Chord Delta synchronizes JSON values from an authoritative producer to an
ordered replica. It is available from `@earendil-works/chord/delta`.
Chord Delta produces immutable JSON revisions and exact operation batches for
ordered replicas. Import it from `@earendil-works/chord/delta`.
A change is represented by an `Op`: a JSON tuple for replacing, setting,
deleting, updating a string, splicing an array, or permuting an array. Producers
use `track()`; replicas use `apply()` or `applyImmutable()`.
Immutability is an ownership contract. Nothing is frozen or defensively copied,
so an illegal mutation is not detected. It silently corrupts state.
```ts
import { apply, track } from "@earendil-works/chord/delta";
import { applyImmutable, track } from "@earendil-works/chord/delta";
const tracker = track({ output: "", entries: [] as string[] });
let replica = apply(undefined, tracker.flush());
const initial = { output: "", entries: [] as { id: number }[] };
const tracker = track(initial); // `initial` is transferred: never mutate it again
tracker.state.output += "done\n";
tracker.state.entries.push("result");
replica = apply(replica, tracker.flush());
const change = tracker.beginChange();
change.state.output += "done\n"; // the draft is mutable only while the change is open
change.state.entries.push({ id: 1 }); // placed values are cloned; the caller keeps its object
const prepared = change.prepare(); // draft handles are unusable from here on
// tracker.value, prepared.base, prepared.value, and prepared.ops (including paths
// and payloads) are immutable by contract.
tracker.adopt(prepared); // tracker.value === prepared.value
// Shares containers with prepared.base and the op payloads: never mutate it either.
const replica = applyImmutable(prepared.base, prepared.ops);
```
The first `flush()` returns one operation containing the complete value. Each
later flush returns operations whose application transforms the previously
published value into the current value. It returns `[]` when no tracked mutation
is pending, but a mutation window that restores its starting value may still
produce a redundant batch.
## Mutation rights
`applyImmutable()` copies only containers along changed paths and shares
unchanged subtrees. It does not mutate, clone, or freeze either complete input.
Chord's replicated-state producers use a separate transaction-scoped
copy-on-write draft and derive these operation batches from immutable revisions;
consumers still observe complete immutable values.
| Value | May it be mutated? |
| --- | --- |
| Root passed to `track()`, `prepareReplace()`, `replicatedState()`, or `replace()` | No. Ownership moved to the tracker. |
| `change.state` and handles read from it | Yes, only while that change is open. After `prepare()`, `abort()`, or another adoption, every use throws. Writes through a handle whose element was removed from the draft are ignored. |
| External value after assigning or inserting it into a draft | Yes. The draft stored a validated clone. |
| `tracker.value`, `prepared.base`, retained older revisions | No. |
| `prepared.value`, `prepared.ops`, op tuples, paths, permutations, payloads | No. Payloads may be the same objects as parts of `prepared.value`. |
| `applyImmutable()` inputs and result | No. The result shares containers with both inputs. |
| Values from replicated state (`value`, listener values, loopback consumers) | No. In-process consumers may share the provider's containers. |
| Mutable replica passed to `apply()` | Only through `apply()`, with batches it owns exclusively. Code that edits it between batches breaks convergence. |
| Batch passed to `apply()` | Consumed. `apply()` adopts payload containers into the replica. Use a detached copy for exactly one replica and never touch it again. |
## Sending or storing changes
## Replaying batches
`flush()` produces decoded `Op[]` with complete paths. This is convenient for
local use but repeats long paths on the wire or disk.
`encoder()` compresses those paths and returns `WireOp[]`. `decoder()` validates
the encoded tuples, restores complete paths, and returns the `Op[]` required by
`apply()`:
Immutable replay is the preferred in-process path. One batch can fan out to any
number of immutable replicas:
```ts
import { apply, decoder, encoder, track } from "@earendil-works/chord/delta";
const tracker = track({ output: "" });
const enc = encoder(); // producer side
const dec = decoder(); // consumer side
let replica: { output: string } | undefined;
const send = () => {
const ops = tracker.flush();
const wire = enc.encode(ops); // serialize or store WireOp[] here
const received = dec.decode(wire);
replica = apply(replica, received);
};
let replica = tracker.value; // shared with authority; never mutated
// For each adopted batch, in order:
replica = applyImmutable(replica, prepared.ops); // replica must equal prepared.base
```
Encoding is optional for local application. Never pass `WireOp[]` directly to
`apply()`.
An encoder and decoder are stateful. Use one pair for each ordered stream. The
encoder assigns numeric IDs to paths used across batches; the decoder remembers
the corresponding definitions. A complete-value operation resets both path
dictionaries, so replay can begin at that batch with a fresh decoder.
Path omission is local to one batch. Numeric path IDs may span batches. Each
independently hydrated replicated-state stream needs its own encoder and decoder.
Do not share a pair between state members or subscriptions, even when their
batches use the same ordered transport connection.
## Operation vocabulary
A path is an array of object keys and array indices:
Mutable replay needs a detached starting root and a detached copy of every batch
for every replica. Never apply one in-memory batch to two mutable replicas:
```ts
["operation", "message", "content", 0, "text"]
import { apply } from "@earendil-works/chord/delta";
const detach = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
let replica = detach(tracker.value); // exclusively owned starting root
// For each adopted batch, in order:
replica = apply(replica, detach(prepared.ops)); // fresh copy for this replica only
// Only apply() may change `replica`; do not edit it in application code.
```
### Decoded `Op`
Parsing a separately serialized message per replica is also a valid detachment.
`decode()` does not copy payloads, so decoding one in-memory `WireOp[]` for two
mutable replicas makes them alias each other.
`track().flush()` returns these tuples, and `apply()` accepts them:
## Do and don't
- Do build a fresh root and then transfer it; validate untrusted roots at your ingestion boundary.
- Do keep structural sharing between revisions, for example `replace(ctx, { ...state.value, changed })`.
- Do clone or serialize before handing any published value to code that may mutate it.
- Do settle every change with `prepare()` or `abort()`.
- Don't reuse one container at two places in a transferred root.
- Don't mutate anything the tracker owns, published, or received from a batch.
- Don't keep draft handles past their change or place a settled handle.
- Don't infer meaning from op shape. Only the resulting value is contractual.
## Roots and placements
Full roots are trusted. `track()` and `prepareReplace()` take ownership in O(1)
and never walk the tree. Roots must be alias-free, acyclic strict JSON: dense
plain arrays, plain or null-prototype objects with enumerable own data
properties, strings, booleans, finite numbers, and `null`. Violations have
unspecified behavior. For example, a container shared by two keys can make
`prepared.value` diverge from what replicas compute from `prepared.ops`.
Draft placements are copied, and that copy walk also validates. Placements come
from property writes, index writes, `push`, `unshift`, `splice`, `fill`, and
`copyWithin`. A non-strict value throws `TypeError` before the draft changes;
for example, `push(valid, invalid)` inserts nothing. Rejected values include:
cycles, accessors (never invoked), symbol keys, class instances, sparse or
non-plain arrays, functions, bigint, `NaN`, infinities, and `undefined`.
Null-prototype objects are accepted and preserved. Placing a draft handle clones
its current content. Placing one value at several paths yields independent
containers.
`undefined` rules:
- `draft.obj.key = undefined` deletes `key`.
- `arr[i] = undefined`, `push(undefined)`, `unshift`, `splice`, and `fill` with `undefined` throw.
- `undefined` nested anywhere inside a placed object or array throws. It is not dropped like `JSON.stringify` does.
Arrays stay dense. Writing past the next index and deleting an element throw.
Growing `length` inserts `null`; shrinking it removes elements.
## Lifecycle
`tracker.value` is the latest adopted revision. `beginChange()` opens an overlay
draft over it and never modifies it. A draft may stay open across `await`.
`prepare()` materializes the candidate and ops and does not change authority.
`adopt()` checks the preparation and swaps the root pointer.
Several changes may be open or prepared from one revision. Adopting one makes all
others stale: open drafts become unusable and their `prepare()` throws, and
prepared competitors are rejected by `adopt()`. `adopt()` also rejects foreign,
aborted, and already consumed preparations. `Change.abort()` or
`Prepared.abort()` after `prepare()` prevents adoption. The candidate stays
readable.
No-op batches:
- Empty `ops` means `prepared.value === prepared.base`. Writes restored to their original value and deeply equal container assignments usually normalize to empty.
- Equality ignores key order and prototype. A deeply equal assignment keeps the previous revision's order and prototype, even if the draft showed the new ones.
- Structural array edits may emit a nonempty exact batch even when the result is deeply equal.
- Adopting a no-op still advances `tracker.revision` and stales competitors. Replicated state does not publish no-ops.
`prepareReplace(value)` is a whole-root operation, not a diff. If `value` is
deeply equal to the current root, `ops` is empty and the current root is kept
(the comparison may traverse both trees). Otherwise `ops` is `[["r", value]]`
and `prepared.value === value`, so replicas receive the complete root.
Array mutators: `push`, `pop`, `shift`, `unshift`, `splice`, `reverse`, `sort`,
`fill`, and `copyWithin`. Held handles follow elements through reindexing.
## Operations
Paths contain object keys and non-negative integer array indices.
| Tuple | Meaning |
| --- | --- |
| `["r", value]` | Replace the complete value. |
| `["s", path, value]` | Set a property or array element. |
| `["d", path]` | Delete an object property. |
| `["s", path, value]` | Set an object property or array element. |
| `["d", path]` | Delete an object property or remove an array element. |
| `["a", path, text]` | Append to a string. |
| `["t", path, count]` | Remove UTF-16 code units from a string's front. |
| `["t", path, count]` | Remove `count` UTF-16 code units from a string's front. |
| `["p", path, index, remove, items]` | Splice an array. |
| `["m", path, permutation]` | Reorder an array so `new[i] = old[permutation[i]]`. |
Except for `r`, every decoded operation carries its complete path. `s`, `d`,
`a`, and `t` cannot address the root. `p` and `m` may address a root array.
### Encoded `WireOp`
A `PathRef` is either an inline path or a non-negative numeric path ID.
`WireOp` supports the following tuples:
| Tuple | Meaning |
| --- | --- |
| `["r", value]` | Complete replacement; identical to decoded form. |
| `["#", id, path]` | Define a numeric path ID. |
| `["s", pathRef, value]` | Set with an inline or interned path. |
| `["s", value]` | Set using the previous path in this batch. |
| `["d", pathRef]` | Delete with an inline or interned path. |
| `["d"]` | Delete using the previous path. |
| `["a", pathRef, text]` | Append with an inline or interned path. |
| `["a", text]` | Append using the previous path. |
| `["t", pathRef, count]` | Front-truncate with an inline or interned path. |
| `["t", count]` | Front-truncate using the previous path. |
| `["p", pathRef, index, remove, items]` | Splice with an inline or interned path. |
| `["p", index, remove, items]` | Splice using the previous path. |
| `["m", pathRef, permutation]` | Reorder with an inline or interned path. |
| `["m", permutation]` | Reorder using the previous path. |
For example, adjacent decoded operations on one path:
```ts
[
["t", ["output"], 200],
["a", ["output"], "next chunk"],
]
```
encode to:
```ts
[
["t", ["output"], 200],
["a", "next chunk"], // reuses ["output"]
]
```
When `output` is used again in a later batch, the encoder defines an ID on its
second explicit use:
```ts
[
["#", 0, ["output"]],
["a", 0, "more"],
]
```
Later batches can use `0` directly until a complete-value operation resets the
dictionary.
## Producing changes
Read and mutate `tracker.state` as a normal object:
```ts
tracker.state.status = "running";
tracker.state.settings.theme = "dark";
tracker.state.messages.push(message);
delete tracker.state.retry;
```
Operations are coalesced within a flush window when doing so is cheap and safe:
```ts
tracker.state.status = "starting";
tracker.state.status = "running";
tracker.flush(); // one set to "running"
```
The operation sequence is not canonical. Equivalent changes may use different
verbs, and mutations that cancel can still produce a nonempty batch. Consumers
must depend on the resulting value, not the exact tuples or their minimality.
Replacing an object or array is valid. Delta compares its properties and elements
with the outgoing value at assignment time:
```ts
tracker.state.settings = {
...plainSettings,
theme: "dark",
};
```
Unchanged properties produce no operations. Changed nested strings and arrays
still use string and splice operations.
### Strings
Appending text produces an `a` operation:
```ts
tracker.state.output += "next line\n";
```
Moving a bounded text window forward produces `t` followed by `a` when the
previous suffix matches the new prefix:
```ts
tracker.state.output = tracker.state.output.slice(200) + nextChunk;
```
An unrelated replacement produces `s`.
### Arrays
Use normal array methods:
```ts
tracker.state.messages.push(first);
tracker.state.messages.push(second);
tracker.state.messages.splice(3, 1, replacement);
```
Adjacent `push()` calls are normally coalesced into one tail `p`. Intervening
operations may keep them separate to preserve ordering. Changes to older
elements remain separate, and changes to newly pushed elements may be folded
into the pushed values when no structural operation intervenes.
Front or middle insertion and removal are recorded directly. Edits before and
after an index-changing operation remain ordered against the array generation
they addressed. Sorting, reversing, `fill()`, and `copyWithin()` emit a snapshot
of the affected array; repeated whole-array mutators can therefore produce a
redundant snapshot even when their combined result restores the prior value.
Sparse arrays are unsupported. Writing beyond the next index throws. Increasing
`length` creates explicit `null` elements; decreasing it removes elements.
`fill()` and `copyWithin()` keep normal JavaScript reference semantics; an object
they place at several indices is published at each.
### Large mutation windows
#### Build once, assign once
Every object or array assignment is diffed immediately. Do not repeatedly assign
large intermediate values before one flush:
```ts
// Avoid: traverses every intermediate tree.
for (const frame of frames) tracker.state.view = render(frame);
// Prefer: only the final tree crosses the tracked boundary.
const nextView = frames.reduce((view, frame) => renderInto(view, frame), initialView);
tracker.state.view = nextView;
```
For a few changes, mutate the leaves directly:
```ts
for (const update of updates) {
tracker.state.view.rows[update.index]!.status = update.status;
}
```
#### Do not cancel whole-array mutators
`sort()`, `reverse()`, `fill()`, and `copyWithin()` record snapshots. Cancelling
them still publishes the final snapshot:
```ts
// Avoid: final value is unchanged, but a snapshot may still be sent.
tracker.state.items.reverse();
tracker.state.items.reverse();
// Prefer: decide before mutating tracked state.
if (needsReverse) tracker.state.items.reverse();
```
#### Publish large inserts and edit sets in chunks
Pending inserted values are cloned for replica ownership. Very large unflushed
pushes therefore temporarily retain both the live values and their operation
payloads. Long operation/path histories may also collapse to a complete base
batch, increasing snapshot and wire cost.
```ts
for (const chunk of chunks(items, 1_000)) {
tracker.state.items.push(...chunk);
replica = apply(replica, tracker.flush()); // send each batch in a real producer
}
```
Use the same pattern for large sets of unrelated edits: apply a bounded chunk,
publish it, then continue.
### Optional properties
Optional object properties use absence. They do not require `null`:
```ts
type Settings = { label?: string; count: number };
const tracker = track<Settings>({ count: 0 });
tracker.state.label = "active";
tracker.state.label = undefined; // produces d
// `delete tracker.state.label` is equivalent
```
`undefined` is accepted only as assignment syntax for deleting an object
property. It is not a JSON value. Initial and assigned objects cannot contain own
`undefined` values, and array elements cannot be `undefined`. Use `null` when an
array position or explicit empty value must remain present.
## State ownership
The object passed to `track()` becomes tracker-owned, as does any object later
assigned into state or inserted into an array. Mutate through `tracker.state`:
```ts
const item = { status: "new" };
tracker.state.item = item;
tracker.state.item.status = "ready"; // tracked
item.status = "broken"; // NOT tracked: silently diverges from the replica
```
A reference retained from `tracker.state` stays correct across operations that
renumber it, and across the removal of the element it points at:
```ts
const held = tracker.state.items[2];
tracker.state.items.unshift(other);
held.name = "edited"; // publishes items[3].name
tracker.state.items.splice(3, 1);
held.name = "gone"; // element is no longer in the tree: mutated, nothing published
```
One object may occupy several paths. Each live path is published:
```ts
tracker.state.a = tracker.state.items[0];
tracker.state.items[0].k = 1; // publishes both a.k and items[0].k
```
Tracked state must be a mutable JSON tree:
- strings, booleans, finite numbers, `null`, arrays, and plain objects;
- no cycles;
- no sparse arrays, accessors, frozen objects, symbols, classes, functions,
`Map`, or `Set`.
## Proxy lifetime and large reads
Proxy caches are weak. Reading a subtree does not permanently retain its proxies
just because the underlying plain objects remain in the document. A proxy still
held by application code keeps its identity; held descendants retain the ancestor
tracking metadata needed to follow array reindexing. Explicit alias locations are
remembered separately from the lifetime of their public proxies.
Collection is automatic, not a `flush()` side effect. JavaScript keeps newly
created or dereferenced `WeakRef` targets alive until the current job ends, and
finalizer cleanup can run later. A synchronous traversal can therefore still have
a substantial allocation peak. Retained-memory measurements must allow event-loop
turns as well as GC; a synchronous `gc()` immediately after the traversal is not
sufficient to measure weak-cache reclamation.
This does not eliminate proxy construction/trap costs or full comparisons on
container assignment. `tracker.target` is available for read-only bulk inspection
without creating proxies. Never mutate through it; all tracked mutations must go
through `tracker.state` or a proxy obtained from it.
See the [delta investigation findings](../../../durable/docs/chord-delta-findings.md)
for the full-traversal regression, measured trade-offs, and reproduction commands.
## Tracker lifecycle
```ts
tracker.flush(); // publish changes since the previous flush
tracker.rebase(); // make the next flush a complete replacement
tracker.discard(); // accept current changes without publishing them
tracker.state = replacement; // replace the root; next flush is complete
```
`discard()` intentionally prevents current changes from reaching existing
replicas. Use it only when those replicas do not need the discarded changes.
`flush()` guarantees convergence, not a minimal or canonical diff. Any nonempty
batch advances replicated-state sequence numbers and notifies subscribers, even
if applying it leaves the value deeply equal to the previous revision. To bound
pending operation and path history, a sufficiently long mutation window may
collapse to a complete base batch automatically. This bounds accumulated log
metadata, not payload bytes or peak allocation, and trades one full-value
snapshot for an additional recovery point.
`apply()` adopts object and array payloads from its input batch. Do not freeze a
batch before applying it, and do not apply one in-memory batch to multiple
mutable replicas unless each replica owns that batch. A serialized and decoded
batch is already detached. `applyImmutable()` instead treats its previous value
and operation payloads as immutable, so one batch can safely fan out in-process.
A `decode()`, `apply()`, or `applyImmutable()` error terminates that stream.
Discard its decoder and replica, then recover from a later base batch. `apply()`
is not transactional; operations before the failing operation may already have
changed the replica.
## Limits
- Delta assumes one authoritative writer and ordered delivery. Sequence numbers,
gap detection, retries, and persistence policy belong to the surrounding
protocol or storage format.
- Object identity is not replicated. One object at several paths publishes each
path separately, and a replica holds a distinct value at each.
- Object key insertion order is not replicated. Do not compare or hash replicas
using serialized key order.
- Array operations that change indices may publish a wider array region, as
described under Arrays.
- Object-valued keys named `__proto__`, `constructor`, or `prototype` can be read
and serialized, but cannot be mutated through that key. Replace the nearest
ordinarily named parent instead.
| `["m", path, permutation]` | Reorder an array: `new[i] = old[permutation[i]]`. |
Batches are exact but not canonical. The same change may use different tuples,
and large edit sets may fold into a region splice, an ancestor `s`, or `r`.
Reserved keys: the tracker never emits `__proto__`, `constructor`, or
`prototype` as a path segment. A mutation at or below such a key is folded into a
set of the nearest safe ancestor, or `r` at the root. `apply()`,
`applyImmutable()`, and `decoder()` reject those segments with `UnsafePathError`.
The appliers write values as own data properties, never through a prototype setter.
Appliers check op shape and path safety but not payload strictness. Chord's
replicated-state replicas validate each resulting revision.
`encoder()` interns repeated paths into `WireOp` tuples; `decoder()` validates
and restores `Op` tuples. Use one encoder/decoder pair per ordered state stream.
Path IDs span batches and an `r` resets both dictionaries. After a decode or
apply error, discard the decoder and replica and recover from a later `r`.
## Limits and footguns
- No freezing. Mutating any immutable value corrupts state silently.
- Loopback sharing: in-process service consumers may receive the provider's containers. A consumer mutation corrupts authority and makes remote replicas diverge.
- A change inside a large flat array copies that array's pointer storage for the new revision.
- Placement validation costs time and memory on bulk inserts. In a benchmark unshifting 100k three-field objects, it added about 19 ms and 20 MiB of transient heap.
- External proxies as placements, argument coercion callbacks that mutate the draft, non-primitive array indices, and non-numeric comparator results are out of contract. So are sort comparators that mutate, prepare, abort, or adopt. Behavior is unspecified.
- Object identity is not replicated. Each path is an independent value placement.
- One tracker is one revision sequence. Delivery order and persistence belong to the surrounding protocol.

View file

@ -1,4 +1,4 @@
import type { JsonValue } from "../../types.ts";
import type { JsonValue } from "../types.ts";
import {
apply as applyMutable,
assertValidOp,
@ -7,7 +7,7 @@ import {
PathError,
type Seg,
UnsafePathError,
} from "../index.ts";
} from "./index.ts";
type JsonContainer = JsonValue[] | Record<string, JsonValue>;

View file

@ -0,0 +1,128 @@
import type { JsonValue } from "../types.ts";
import type { Op, Path, Seg } from "./index.ts";
type JsonContainer = JsonValue[] | Record<string, JsonValue>;
type CopiedPath = { root: JsonValue; target: JsonContainer };
const DATA_DESCRIPTOR: PropertyDescriptor = {
value: undefined,
writable: true,
enumerable: true,
configurable: true,
};
/** Apply self-produced trusted operations while copying each touched container once. */
export function applyImmutableTrusted<T>(target: T, operations: readonly Op[]): T {
let root = target as unknown as JsonValue;
const owned = new WeakSet<object>();
for (const operation of operations) {
if (operation[0] === "r") {
root = operation[1];
continue;
}
if (operation[0] === "p" || operation[0] === "m") {
const copied = copyPath(root, operation[1], operation[1].length, owned);
root = copied.root;
if (!Array.isArray(copied.target)) throw new TypeError("Trusted array operation target is not an array");
if (operation[0] === "p") spliceTrusted(copied.target, operation[2], operation[3], operation[4]);
else permuteTrusted(copied.target, operation[2]);
continue;
}
const path = operation[1];
const copied = copyPath(root, path, path.length - 1, owned);
root = copied.root;
const key = path[path.length - 1]!;
switch (operation[0]) {
case "s":
defineData(copied.target, key, operation[2]);
break;
case "d":
if (Array.isArray(copied.target)) {
if (typeof key !== "number") throw new TypeError("Trusted array deletion key is not numeric");
spliceTrusted(copied.target, key, 1, []);
} else Reflect.deleteProperty(copied.target, key);
break;
case "a": {
const current = read(copied.target, key);
if (typeof current !== "string") throw new TypeError("Trusted append target is not a string");
defineData(copied.target, key, `${current}${operation[2]}`);
break;
}
case "t": {
const current = read(copied.target, key);
if (typeof current !== "string") throw new TypeError("Trusted truncate target is not a string");
defineData(copied.target, key, current.slice(operation[2]));
break;
}
}
}
return root as unknown as T;
}
function copyPath(root: JsonValue, path: Path, length: number, owned: WeakSet<object>): CopiedPath {
if (!isContainer(root)) throw new TypeError("Trusted operation root is not a container");
let copiedRoot = root;
if (!owned.has(root)) {
copiedRoot = shallowCopy(root);
owned.add(copiedRoot);
}
let destination = copiedRoot as JsonContainer;
for (let index = 0; index < length; index++) {
const segment = path[index]!;
const child = read(destination, segment);
if (!isContainer(child)) throw new TypeError("Trusted operation path is not a container");
if (owned.has(child)) {
destination = child;
continue;
}
const copy = shallowCopy(child);
defineData(destination, segment, copy);
owned.add(copy);
destination = copy;
}
return { root: copiedRoot, target: destination };
}
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 read(target: JsonContainer, key: Seg): JsonValue {
return (target as Record<Seg, JsonValue>)[key]!;
}
function defineData(target: JsonContainer, key: Seg, value: JsonValue): void {
DATA_DESCRIPTOR.value = value;
Object.defineProperty(target, key, DATA_DESCRIPTOR);
DATA_DESCRIPTOR.value = undefined;
}
function spliceTrusted(target: JsonValue[], start: number, remove: number, items: readonly JsonValue[]): void {
const oldLength = target.length;
const delta = items.length - remove;
if (delta > 0) {
target.length = oldLength + delta;
target.copyWithin(start + items.length, start + remove, oldLength);
} else if (delta < 0) {
target.copyWithin(start + items.length, start + remove, oldLength);
target.length = oldLength + delta;
}
for (let index = 0; index < items.length; index++) defineData(target, start + index, items[index]!);
}
function permuteTrusted(target: JsonValue[], permutation: readonly number[]): void {
if (target.length !== permutation.length) throw new TypeError("Trusted permutation length mismatch");
const previous = target.slice();
for (let index = 0; index < permutation.length; index++) target[index] = previous[permutation[index]!]!;
}
function isContainer(value: unknown): value is JsonContainer {
return value !== null && typeof value === "object";
}

View file

@ -1,24 +0,0 @@
# 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

@ -1,12 +0,0 @@
# 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

@ -1,562 +0,0 @@
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

@ -1,717 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,225 +0,0 @@
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]}`);
}
});
}

View file

@ -8,614 +8,3 @@ export type Draft<T, Depth extends readonly unknown[] = []> = Depth["length"] ex
: T extends object
? { -readonly [Key in keyof T]: Draft<T[Key], [...Depth, unknown]> }
: T;
export type ProduceMetadata<T> = {
value: T;
owned: WeakSet<object>;
};
type Container = Record<string, unknown> | unknown[];
type DraftState = {
base: Container;
context: DraftContext;
copy: Container | undefined;
methods: Map<string, unknown> | undefined;
proxy: object;
target: unknown[] | undefined;
};
type DraftContext = {
active: boolean;
created: DraftState[];
owned: WeakSet<object>;
proxies: WeakMap<object, DraftState>;
states: WeakMap<object, DraftState>;
};
const RELEASED_BASE: Container = Object.freeze({});
const ARRAY_MUTATORS = new Set(["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin"]);
const MAX_NATIVE_ARRAY_INSERT_ITEMS = 10_000;
export type DraftTransaction<T extends object> = {
readonly state: Draft<T>;
finish(): ProduceMetadata<T>;
abort(): void;
};
/** Create a revocable copy-on-write draft whose lifetime is controlled by its owner. */
export function createDraftTransaction<T extends object>(base: T): DraftTransaction<T> {
const context: DraftContext = {
active: true,
created: [],
owned: new WeakSet<object>(),
proxies: new WeakMap<object, DraftState>(),
states: new WeakMap<object, DraftState>(),
};
const root = getState(context, base as Container);
const release = (): void => {
assertActive(context);
context.active = false;
for (const state of context.created) {
state.base = RELEASED_BASE;
state.copy = undefined;
state.methods = undefined;
state.proxy = RELEASED_BASE;
state.target = undefined;
}
context.created.length = 0;
context.proxies = new WeakMap();
context.states = new WeakMap();
};
return {
state: root.proxy as Draft<T>,
finish() {
assertActive(context);
try {
return {
value: finalize(root, new Set<DraftState>(), new WeakMap<DraftState, Container>()) as T,
owned: context.owned,
};
} finally {
release();
}
},
abort: release,
};
}
/** @internal Compatibility helper for the existing replicated-state service. */
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 before propagating an error.
}
throw error;
}
}
/** @internal Compatibility helper for the existing replicated-state service. */
export function produce<T extends object>(base: T, recipe: (draft: Draft<T>) => void): T {
return produceWithMetadata(base, recipe).value;
}
function getState(context: DraftContext, base: Container): DraftState {
const existing = context.states.get(base);
if (existing !== undefined) return existing;
const target = Array.isArray(base) ? [] : undefined;
if (target !== undefined) Reflect.set(target, "length", base.length);
const state: DraftState = {
base,
context,
copy: undefined,
methods: undefined,
proxy: target ?? {},
target,
};
state.proxy = new Proxy(target ?? state, createHandler(state));
context.states.set(base, state);
context.proxies.set(state.proxy, state);
context.created.push(state);
return state;
}
type DraftHandler = ProxyHandler<object> & { state: DraftState };
const handlerPrototype: ProxyHandler<object> = {
deleteProperty(this: DraftHandler, _target, property): boolean {
const state = this.state;
assertStringWrite(property);
const current = currentValue(state);
if (Array.isArray(current)) throw new TypeError("Draft arrays cannot contain holes");
if (!Object.hasOwn(current, property)) return true;
const deleted = Reflect.deleteProperty(ensureCopy(state), property);
syncArrayTarget(state);
return deleted;
},
defineProperty(this: DraftHandler): never {
assertActive(this.state.context);
throw new TypeError("Defining draft properties is not supported");
},
get(this: DraftHandler, _target, property): unknown {
const state = this.state;
const current = currentValue(state);
const value = Reflect.get(current, property, state.proxy);
if (Array.isArray(current) && typeof property === "string" && ARRAY_MUTATORS.has(property)) {
return arrayMutator(state, property, value);
}
return draftValue(state.context, value);
},
getOwnPropertyDescriptor(this: DraftHandler, _target, property): PropertyDescriptor | undefined {
const state = this.state;
const current = currentValue(state);
const descriptor = Reflect.getOwnPropertyDescriptor(current, property);
if (descriptor === undefined) return undefined;
if (Array.isArray(current) && property === "length") {
syncArrayTarget(state);
return Reflect.getOwnPropertyDescriptor(state.target!, property);
}
return {
configurable: true,
enumerable: descriptor.enumerable,
value: "value" in descriptor ? draftValue(state.context, descriptor.value) : undefined,
writable: true,
};
},
getPrototypeOf(this: DraftHandler): object | null {
assertActive(this.state.context);
return Object.getPrototypeOf(this.state.base);
},
has(this: DraftHandler, _target, property): boolean {
return Reflect.has(currentValue(this.state), property);
},
isExtensible(this: DraftHandler): boolean {
assertActive(this.state.context);
return true;
},
ownKeys(this: DraftHandler): ArrayLike<string | symbol> {
return Reflect.ownKeys(currentValue(this.state));
},
preventExtensions(this: DraftHandler): never {
assertActive(this.state.context);
throw new TypeError("Drafts cannot be made non-extensible");
},
set(this: DraftHandler, _target, property, value): boolean {
const state = this.state;
assertStringWrite(property);
const current = currentValue(state);
let stored: unknown;
if (isContainer(value)) {
stored = cloneAssigned(value, state.context, new Set<object>());
} else {
assertJsonPrimitive(value);
stored = value;
}
if (Object.hasOwn(current, property) && Object.is(Reflect.get(current, property), stored)) return true;
const copy = ensureCopy(state);
if (Array.isArray(copy) && property === "length") {
const written = Reflect.set(copy, property, stored);
syncArrayTarget(state);
return written;
}
Object.defineProperty(copy, property, {
value: stored,
writable: true,
enumerable: true,
configurable: true,
});
syncArrayTarget(state);
return true;
},
setPrototypeOf(this: DraftHandler): never {
assertActive(this.state.context);
throw new TypeError("Changing a draft prototype is not supported");
},
};
function createHandler(state: DraftState): ProxyHandler<object> {
const handler = Object.create(handlerPrototype) as DraftHandler;
handler.state = state;
return handler;
}
function arrayMutator(state: DraftState, property: string, value: unknown): unknown {
assertActive(state.context);
const cached = state.methods?.get(property);
if (cached !== undefined) return cached;
if (typeof value !== "function") return draftValue(state.context, value);
const wrapped = function (this: unknown, ...args: unknown[]): unknown {
assertActive(state.context);
const receiver = isContainer(this) ? state.context.proxies.get(this) : undefined;
if (receiver === undefined) return Reflect.apply(value, this, args);
const target = ensureCopy(receiver);
if (!Array.isArray(target)) throw new TypeError("Array mutator receiver is not an array draft");
try {
return mutateArray(receiver, target, property, args);
} finally {
syncArrayTarget(receiver);
}
};
if (state.methods === undefined) state.methods = new Map();
state.methods.set(property, wrapped);
return wrapped;
}
function mutateArray(state: DraftState, target: unknown[], property: string, args: unknown[]): unknown {
const { context } = state;
switch (property) {
case "push": {
const items = cloneArrayItems(args, context);
for (const item of items) defineArrayValue(target, target.length, item);
return target.length;
}
case "pop": {
if (target.length === 0) return undefined;
const index = target.length - 1;
const removed = target[index];
Reflect.deleteProperty(target, String(index));
target.length = index;
return draftValue(context, removed);
}
case "shift":
return draftValue(context, Reflect.apply(Array.prototype.shift, target, []));
case "unshift": {
const items = cloneArrayItems(args, context);
if (
items.length <= MAX_NATIVE_ARRAY_INSERT_ITEMS &&
!hasInheritedGrowthIndex(target, target.length + items.length)
) {
return Reflect.apply(Array.prototype.unshift, target, items);
}
spliceArray(target, items, 0, 0, context);
return target.length;
}
case "splice": {
const length = target.length;
const start = args.length === 0 ? 0 : clampArrayIndex(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 items = cloneArrayItems(args.slice(2), context);
if (
items.length <= MAX_NATIVE_ARRAY_INSERT_ITEMS &&
!mayRunCoercionCode(args[0]) &&
!mayRunCoercionCode(args[1]) &&
!hasInheritedGrowthIndex(target, length - remove + items.length)
) {
return Reflect.apply(Array.prototype.splice, target, [start, remove, ...items]).map((item) =>
draftValue(context, item),
);
}
return spliceArray(target, items, start, remove, context, length);
}
case "sort": {
const comparator = args[0];
if (comparator !== undefined && typeof comparator !== "function") {
Reflect.apply(Array.prototype.sort, target, args);
} else {
Reflect.apply(Array.prototype.sort, target, [
(left: unknown, right: unknown): number => {
const draftedLeft = draftValue(context, left);
const draftedRight = draftValue(context, right);
if (typeof comparator === "function") {
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;
},
]);
}
return state.proxy;
}
case "reverse":
Reflect.apply(Array.prototype.reverse, target, []);
return state.proxy;
case "fill": {
const length = target.length;
const start = args.length > 1 ? clampArrayIndex(toIntegerOrInfinity(args[1]), length) : 0;
const end =
args.length > 2 && args[2] !== undefined ? clampArrayIndex(toIntegerOrInfinity(args[2]), length) : length;
if (end <= start) return state.proxy;
const supplied = args[0];
for (let index = start; index < end; index++) {
const item = isContainer(supplied)
? cloneAssigned(supplied, context, new Set<object>())
: assertJsonPrimitive(supplied);
defineArrayValue(target, index, item);
}
return state.proxy;
}
case "copyWithin": {
const length = target.length;
const to = clampArrayIndex(toIntegerOrInfinity(args[0]), length);
const from = clampArrayIndex(toIntegerOrInfinity(args[1]), length);
const end =
args.length > 2 && args[2] !== undefined ? clampArrayIndex(toIntegerOrInfinity(args[2]), length) : length;
const count = Math.min(Math.max(end - from, 0), length - to);
const source = target.slice(from, from + count);
for (let offset = 0; offset < count; offset++) {
const item = source[offset];
const itemState = isContainer(item) ? context.states.get(item) : undefined;
const current = (itemState?.proxy as Container | undefined) ?? item;
const copy = isContainer(current)
? cloneAssigned(current, context, new Set<object>())
: assertJsonPrimitive(current);
defineArrayValue(target, to + offset, copy);
}
return state.proxy;
}
}
throw new TypeError(`Unsupported array mutator: ${property}`);
}
function cloneArrayItems(items: readonly unknown[], context: DraftContext): unknown[] {
return items.map((item) => {
if (isContainer(item)) return cloneAssigned(item, context, new Set<object>());
assertJsonPrimitive(item);
return item;
});
}
function hasInheritedGrowthIndex(target: unknown[], nextLength: number): boolean {
for (let index = target.length; index < nextLength; index++) {
if (index in target) return true;
}
return false;
}
function mayRunCoercionCode(value: unknown): boolean {
return (typeof value === "object" && value !== null) || typeof value === "function";
}
function spliceArray(
target: unknown[],
items: readonly unknown[],
index: number,
remove: number,
context: DraftContext,
length = target.length,
): unknown[] {
const removed = new Array<unknown>(remove);
for (let offset = 0; offset < remove; offset++) {
const source = index + offset;
if (!(source in target)) continue;
defineArrayValue(removed, offset, draftValue(context, target[source]));
}
if (items.length < remove) {
for (let source = index + remove; source < length; source++) {
defineArrayValue(target, source - remove + items.length, target[source]);
}
} else if (items.length > remove) {
for (let source = length - 1; source >= index + remove; source--) {
defineArrayValue(target, source - remove + items.length, target[source]);
}
}
for (let offset = 0; offset < items.length; offset++) defineArrayValue(target, index + offset, items[offset]);
target.length = length - remove + items.length;
return removed;
}
function defineArrayValue(target: unknown[], index: number, value: unknown): void {
Object.defineProperty(target, index, {
value,
writable: true,
enumerable: true,
configurable: true,
});
}
function toIntegerOrInfinity(value: unknown): number {
const number = +(value as unknown as number);
if (Number.isNaN(number) || number === 0) return 0;
return Number.isFinite(number) ? Math.trunc(number) : number;
}
function clampArrayIndex(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 currentValue(state: DraftState): Container {
assertActive(state.context);
return state.copy ?? state.base;
}
function ensureCopy(state: DraftState): Container {
if (state.copy !== undefined) return state.copy;
if (state.context.owned.has(state.base)) {
state.copy = state.base;
} else {
state.copy = shallowOwnedCopy(state.base, state.context);
}
return state.copy;
}
function shallowOwnedCopy(value: Container, context: DraftContext): Container {
if (Array.isArray(value)) {
const copy = value.slice();
context.owned.add(copy);
return copy;
}
const copy = Object.create(Object.getPrototypeOf(value)) as Record<string, unknown>;
for (const key of Object.keys(value)) {
Object.defineProperty(copy, key, {
value: value[key],
writable: true,
enumerable: true,
configurable: true,
});
}
context.owned.add(copy);
return copy;
}
function cloneAssigned(value: Container, context: DraftContext, ancestors: Set<object>): Container {
if (ancestors.has(value)) throw new TypeError("Assigned JSON values cannot contain cycles");
ancestors.add(value);
try {
if (Array.isArray(value)) {
assertDenseArray(value);
const clone: unknown[] = [];
context.owned.add(clone);
for (let index = 0; index < value.length; index++) {
clone.push(cloneAssignedValue(Reflect.get(value, String(index)), context, ancestors));
}
return clone;
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Assigned JSON containers must be plain objects or arrays");
}
const clone = Object.create(prototype) as Record<string, unknown>;
context.owned.add(clone);
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Assigned JSON objects cannot have symbol properties");
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Assigned JSON objects must contain enumerable data properties");
}
Object.defineProperty(clone, key, {
value: cloneAssignedValue(descriptor.value, context, ancestors),
writable: true,
enumerable: true,
configurable: true,
});
}
return clone;
} finally {
ancestors.delete(value);
}
}
function cloneAssignedValue(value: unknown, context: DraftContext, ancestors: Set<object>): unknown {
if (isContainer(value)) return cloneAssigned(value, context, ancestors);
assertJsonPrimitive(value);
return value;
}
function assertJsonPrimitive(value: unknown): null | boolean | number | string {
if (value === undefined) throw new TypeError("Assigned values cannot be undefined");
if (
value !== null &&
typeof value !== "string" &&
typeof value !== "boolean" &&
!(typeof value === "number" && Number.isFinite(value))
) {
throw new TypeError("Assigned values must be strict JSON values");
}
return value;
}
function draftValue(context: DraftContext, value: unknown): unknown {
if (!isContainer(value)) return value;
return getState(context, value).proxy;
}
function isContainer(value: unknown): value is Container {
return typeof value === "object" && value !== null;
}
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return (
((typeof value === "object" && value !== null) || typeof value === "function") &&
typeof (value as { then?: unknown }).then === "function"
);
}
function assertActive(context: DraftContext): void {
if (!context.active) throw new TypeError("Cannot use a draft outside its change callback");
}
function assertStringWrite(property: string | symbol): asserts property is string {
if (typeof property === "symbol") throw new TypeError("Symbol writes are not supported");
}
function syncArrayTarget(state: DraftState): void {
const current = currentValue(state);
if (Array.isArray(current)) Reflect.set(state.target!, "length", current.length);
}
function finalize(
state: DraftState,
finalizing: Set<DraftState>,
finalized: WeakMap<DraftState, Container>,
): Container {
const cached = finalized.get(state);
if (cached !== undefined) return cached;
if (finalizing.has(state)) throw new TypeError("Cyclic draft state is not supported");
finalizing.add(state);
try {
if (state.copy !== undefined && shallowEqual(state.base, state.copy)) state.copy = undefined;
const current = currentValue(state);
let result = current;
for (const key of Object.keys(current)) {
const value = Reflect.get(current, key);
if (!isContainer(value)) continue;
const child = state.context.states.get(value);
if (child === undefined) continue;
const finalizedChild = finalize(child, finalizing, finalized);
if (finalizedChild === value) continue;
if (result === state.base) result = shallowOwnedCopy(state.base, state.context);
Object.defineProperty(result, key, {
value: finalizedChild,
writable: true,
enumerable: true,
configurable: true,
});
}
if (state.copy !== undefined && Array.isArray(result)) assertDenseArray(result);
finalized.set(state, result);
return result;
} finally {
finalizing.delete(state);
}
}
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 assertDenseArray(value: unknown[]): void {
const keys = Reflect.ownKeys(value);
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string")) {
throw new TypeError("Draft arrays must be dense and contain only indexed entries");
}
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (
descriptor === undefined ||
!descriptor.enumerable ||
!("value" in descriptor) ||
descriptor.value === undefined
) {
throw new TypeError("Draft arrays must contain enumerable indexed data properties with defined values");
}
}
}

View file

@ -0,0 +1,75 @@
import type { JsonValue } from "../types.ts";
type JsonContainer = JsonValue[] | Record<string, JsonValue>;
const isContainer = (value: JsonValue): value is JsonContainer => typeof value === "object" && value !== null;
/** Validates immutable replica revisions while skipping containers validated in earlier revisions. */
export class JsonRevisionValidator {
readonly #validated = new WeakSet<object>();
validate<T extends JsonValue>(value: T): T {
return this.#validate(value, new Set<object>(), new WeakSet<object>()) as T;
}
#validate(value: JsonValue, ancestors: Set<object>, finished: WeakSet<object>): JsonValue {
if (!isContainer(value) || this.#validated.has(value)) return isContainer(value) ? value : assertPrimitive(value);
if (finished.has(value)) return value;
if (ancestors.has(value)) throw new TypeError("Replicated state cannot contain cycles");
ancestors.add(value);
try {
if (Array.isArray(value)) {
assertDenseArray(value);
for (let index = 0; index < value.length; index++) this.#validate(value[index]!, ancestors, finished);
} else {
assertPlainObject(value);
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Replicated state cannot contain symbol properties");
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Replicated state objects must contain enumerable data properties");
}
this.#validate(descriptor.value as JsonValue, ancestors, finished);
}
}
finished.add(value);
this.#validated.add(value);
return value;
} finally {
ancestors.delete(value);
}
}
}
function assertPrimitive(value: unknown): null | boolean | number | string {
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number" && Number.isFinite(value)) return value;
throw new TypeError("Replicated state values must be strict JSON");
}
function assertPlainObject(value: object): void {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Replicated state containers must be plain objects or arrays");
}
}
function assertDenseArray(value: readonly unknown[]): void {
const keys = Reflect.ownKeys(value);
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string")) {
throw new TypeError("Replicated state arrays must be dense and contain only indexed entries");
}
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (
descriptor === undefined ||
!descriptor.enumerable ||
!("value" in descriptor) ||
descriptor.value === undefined
) {
throw new TypeError(
"Replicated state arrays must contain enumerable indexed data properties with defined values",
);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,179 +0,0 @@
import type { JsonValue } from "../types.ts";
type JsonContainer = JsonValue[] | Record<string, JsonValue>;
const isContainer = (value: unknown): value is JsonContainer => typeof value === "object" && value !== null;
/** Owns and freezes immutable JSON revisions while reusing already-owned subtrees. */
export class JsonRevisionStore {
readonly #owned = new WeakSet<object>();
import<T extends object>(value: T): T {
return this.#clone(value, new Set<object>()) as T;
}
commit<T extends object>(value: T, transactionOwned: WeakSet<object>): T {
return this.#finish(value as JsonContainer, transactionOwned, new Set<object>(), new WeakSet<object>()) as T;
}
adopt<T extends JsonValue>(value: T): T {
return this.#adopt(value, new Set<object>(), new WeakSet<object>()) as T;
}
#clone(value: unknown, ancestors: Set<object>): JsonValue {
if (!isContainer(value)) return assertPrimitive(value);
if (ancestors.has(value)) throw new TypeError("Replicated state cannot contain cycles");
ancestors.add(value);
try {
if (Array.isArray(value)) {
assertDenseArray(value);
const result = new Array<JsonValue>(value.length);
for (let index = 0; index < value.length; index++) {
Object.defineProperty(result, index, {
value: this.#clone(value[index], ancestors),
writable: true,
enumerable: true,
configurable: true,
});
}
this.#owned.add(result);
return Object.freeze(result) as unknown as JsonValue;
}
assertPlainObject(value);
const result = Object.create(Object.getPrototypeOf(value)) as Record<string, JsonValue>;
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Replicated state cannot contain symbol properties");
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Replicated state objects must contain enumerable data properties");
}
Object.defineProperty(result, key, {
value: this.#clone(descriptor.value, ancestors),
writable: true,
enumerable: true,
configurable: true,
});
}
this.#owned.add(result);
return Object.freeze(result) as unknown as JsonValue;
} finally {
ancestors.delete(value);
}
}
#adopt(value: JsonValue, ancestors: Set<object>, finished: WeakSet<object>): JsonValue {
if (!isContainer(value) || this.#owned.has(value)) return isContainer(value) ? value : assertPrimitive(value);
if (finished.has(value)) return value;
if (ancestors.has(value)) throw new TypeError("Replicated state cannot contain cycles");
ancestors.add(value);
try {
if (Array.isArray(value)) {
assertDenseArray(value);
for (let index = 0; index < value.length; index++) {
const child = this.#adopt(value[index]!, ancestors, finished);
if (child !== value[index]) defineValue(value, String(index), child as JsonContainer);
}
} else {
assertPlainObject(value);
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Replicated state cannot contain symbol properties");
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Replicated state objects must contain enumerable data properties");
}
const child = this.#adopt(descriptor.value as JsonValue, ancestors, finished);
if (child !== descriptor.value && isContainer(child)) defineValue(value, key, child);
}
}
finished.add(value);
this.#owned.add(value);
return Object.freeze(value) as unknown as JsonValue;
} finally {
ancestors.delete(value);
}
}
#finish(
value: JsonContainer,
transactionOwned: WeakSet<object>,
ancestors: Set<object>,
placements: WeakSet<object>,
): JsonContainer {
if (ancestors.has(value)) throw new TypeError("Replicated state cannot contain cycles");
if (placements.has(value)) return this.#clone(value, new Set<object>()) as JsonContainer;
placements.add(value);
if (this.#owned.has(value)) return value;
if (!transactionOwned.has(value)) return this.#clone(value, ancestors) as JsonContainer;
ancestors.add(value);
try {
if (Array.isArray(value)) {
assertDenseArray(value);
for (let index = 0; index < value.length; index++) {
const child = value[index];
if (!isContainer(child)) {
assertPrimitive(child);
continue;
}
const next = this.#finish(child, transactionOwned, ancestors, placements);
if (next !== child) defineValue(value, String(index), next);
}
} else {
assertPlainObject(value);
for (const key of Reflect.ownKeys(value)) {
if (typeof key === "symbol") throw new TypeError("Replicated state cannot contain symbol properties");
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
throw new TypeError("Replicated state objects must contain enumerable data properties");
}
if (!isContainer(descriptor.value)) {
assertPrimitive(descriptor.value);
continue;
}
const next = this.#finish(descriptor.value, transactionOwned, ancestors, placements);
if (next !== descriptor.value) defineValue(value, key, next);
}
}
this.#owned.add(value);
return Object.freeze(value) as unknown as JsonContainer;
} finally {
ancestors.delete(value);
}
}
}
function defineValue(target: object, key: string, value: JsonContainer): void {
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
}
function assertPrimitive(value: unknown): null | boolean | number | string {
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number" && Number.isFinite(value)) return value;
throw new TypeError("Replicated state values must be strict JSON");
}
function assertPlainObject(value: object): void {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Replicated state containers must be plain objects or arrays");
}
}
function assertDenseArray(value: readonly unknown[]): void {
const keys = Reflect.ownKeys(value);
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string")) {
throw new TypeError("Replicated state arrays must be dense and contain only indexed entries");
}
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (
descriptor === undefined ||
!descriptor.enumerable ||
!("value" in descriptor) ||
descriptor.value === undefined
) {
throw new TypeError(
"Replicated state arrays must contain enumerable indexed data properties with defined values",
);
}
}
}

View file

@ -1,6 +1,6 @@
import { BACKGROUND_CONTEXT } from "../context/index.ts";
import { applyImmutable, isBase, type Op, type Prepared, type Tracker, track } from "../delta/index.ts";
import { JsonRevisionStore } from "../delta/value.ts";
import { JsonRevisionValidator } from "../delta/revision-validator.ts";
import type {
AttachedReplicatedState,
Context,
@ -265,7 +265,7 @@ export function attachReplicatedStateSource<T>(
export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements ReplicatedState<T> {
readonly #listeners = new Set<StateListener<T>>();
readonly #reportError: (error: Error) => void;
readonly #store = new JsonRevisionStore();
readonly #validator = new JsonRevisionValidator();
#value: T | undefined;
#sequence: number | undefined;
@ -292,7 +292,7 @@ export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements
let next: T;
try {
if (!isBase(ops)) throw new Error("Replicated state snapshot is not a base operation batch");
next = this.#store.adopt(applyImmutable<T>(undefined, ops));
next = this.#validator.validate(applyImmutable<T>(undefined, ops));
} catch (error) {
this.clear();
throw error;
@ -312,7 +312,7 @@ export class ReplicatedStateReplica<T extends JsonValue = JsonValue> implements
}
let next: T;
try {
next = this.#store.adopt(applyImmutable(this.#value, ops));
next = this.#validator.validate(applyImmutable(this.#value, ops));
} catch (error) {
this.clear();
throw error;

View file

@ -41,20 +41,26 @@ export interface ReplicatedStateDelivery {
}
export interface ReplicatedState<T> {
/** Immutable value, or undefined until hydration. Later updates do not mutate previously returned values. */
/**
* Contract-immutable value, or undefined until hydration. It is not frozen and may share containers with an
* in-process provider; consumers must not mutate it. Later updates do not mutate previously returned values.
*/
readonly value: T | undefined;
/** Listener values are immutable and may structurally share unchanged data with other revisions. */
/** Listener values follow the same immutable ownership contract and may share unchanged revision data. */
subscribe(listener: (value: T, context: Context, delivery: ReplicatedStateDelivery) => void): () => void;
}
export interface MutableReplicatedState<T extends object> extends ReplicatedState<T> {
readonly value: T;
/**
* Atomically publish one synchronous copy-on-write mutation. Draft handles are revoked when the callback returns.
* Async callbacks and non-JSON assignments, including `undefined`, throw without changing the value.
* Atomically publish one synchronous overlay mutation. Draft handles are unusable after the callback returns.
* Values placed through the draft are cloned by value; assigning `undefined` to an object property deletes it.
*/
change(context: Context, mutate: (draft: Draft<T>) => void): void;
/** Atomically replace the complete value with a detached immutable JSON snapshot. */
/**
* Atomically take immutable ownership of an alias-free strict-JSON replacement.
* The caller must not mutate the transferred root after this call.
*/
replace(context: Context, value: T): void;
}
@ -263,7 +269,10 @@ export interface FacetEnvironment {
provide<T>(service: Service<T>, implementation: NoInfer<T>): void;
/** Declare ownership of a multi-instance service and return its deferred spawning capability. */
provideMany<T>(service: Service<T>): ServiceSpawner<T>;
/** Create initialized mutable state suitable for exposing through a service implementation. */
/**
* Create initialized mutable state by taking immutable ownership of an alias-free strict-JSON root.
* The caller must not mutate `initial` after this call.
*/
replicatedState<T extends object>(initial: T): MutableReplicatedState<T>;
/** Give the facet ownership of a resource cleanup function. */
own(disposal: () => void | Promise<void>): void;

View file

@ -1,142 +0,0 @@
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

@ -1,134 +0,0 @@
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

@ -1,142 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,106 +0,0 @@
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,210 @@
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 = "tracker";
type Scenario =
| "import"
| "committed-read"
| "draft-read"
| "sparse"
| "queue"
| "sort"
| "dense"
| "unshift"
| "watch-authority-control"
| "watch-live-borrowed"
| "watch-acquire-shared"
| "watch-acquire-detached"
| "watch-acquire-staggered"
| "watch-lag"
| "watch-catch-up"
| "watch-catch-up-coalesced"
| "watch-live-immutable";
type Result = {
mode: Mode;
scenario: Scenario;
size: number;
trial: number;
queueOperations: number;
watchProfile: string | null;
watchCount: number;
watchUpdates: number;
queuedBatchReferences: number;
importMs: number;
mutateMs: number;
prepareMs: number;
serializeMs: number;
adoptMs: number;
acquireMs: number;
enqueueMs: number;
deliveryMs: number;
scenarioWorkMs: number;
opBytes: number;
operationCount: number;
checksum: number;
readyMiB: number;
sampledAcquireMiB: number;
sampledTransientMiB: number;
retainedMiB: number;
retainedOverReadyMiB: number;
postAcquireRetainedMiB: number;
postLagRetainedMiB: number;
postDeliveryRetainedMiB: number;
releasedRetainedMiB: number;
maxRssMiB: number;
};
type Failure = { mode: Mode; scenario: Scenario; trial: number; stderr: string };
const ordinaryScenarios: readonly Scenario[] = [
"import",
"committed-read",
"draft-read",
"sparse",
"queue",
"sort",
"dense",
"unshift",
];
const watchScenarios: readonly Scenario[] = [
"watch-authority-control",
"watch-live-borrowed",
"watch-acquire-shared",
"watch-acquire-detached",
"watch-acquire-staggered",
"watch-lag",
"watch-catch-up",
"watch-catch-up-coalesced",
"watch-live-immutable",
];
const modes: readonly Mode[] = ["tracker"];
const quick = process.argv.includes("--quick");
const watchOnly = process.argv.includes("--watch-only");
const ordinaryOnly = process.argv.includes("--ordinary-only");
if (watchOnly && ordinaryOnly) throw new Error("Choose at most one scenario filter");
const scenarios = watchOnly
? watchScenarios
: ordinaryOnly
? ordinaryScenarios
: [...ordinaryScenarios, ...watchScenarios];
const size = quick ? 10_000 : 100_000;
const trials = quick ? 1 : 3;
const watchCount = quick ? 4 : 8;
const watchUpdates = quick ? 20 : 100;
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),
String(watchCount),
String(watchUpdates),
],
{ 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 median = (values: readonly number[]): number | null => {
if (values.length === 0) return null;
const ordered = [...values].sort((left, right) => left - right);
return ordered[Math.floor(ordered.length / 2)]!;
};
const range = (values: readonly number[]): readonly [number, number] | null =>
values.length === 0 ? null : [Math.min(...values), Math.max(...values)];
const metricNames = [
"importMs",
"mutateMs",
"prepareMs",
"serializeMs",
"adoptMs",
"acquireMs",
"enqueueMs",
"deliveryMs",
"scenarioWorkMs",
"opBytes",
"operationCount",
"readyMiB",
"sampledAcquireMiB",
"sampledTransientMiB",
"retainedMiB",
"retainedOverReadyMiB",
"postAcquireRetainedMiB",
"postLagRetainedMiB",
"postDeliveryRetainedMiB",
"releasedRetainedMiB",
"maxRssMiB",
] as const satisfies readonly (keyof Result)[];
const summary = scenarios.flatMap((scenario) =>
modes.map((mode) => {
const selected = results.filter((result) => result.mode === mode && result.scenario === scenario);
const averages: Record<string, number | null> = {};
const medians: Record<string, number | null> = {};
const ranges: Record<string, readonly [number, number] | null> = {};
for (const metric of metricNames) {
const values = selected.map((result) => result[metric]);
averages[metric] = average(values);
medians[metric] = median(values);
ranges[metric] = range(values);
}
return {
mode,
scenario,
watchProfile: selected[0]?.watchProfile ?? null,
watchCount: selected[0]?.watchCount ?? 0,
watchUpdates: selected[0]?.watchUpdates ?? 0,
averages,
medians,
ranges,
};
}),
);
const output = join(tmpdir(), "chord-delta-benchmark.json");
writeFileSync(
output,
JSON.stringify(
{ node: process.version, size, trials, watchCount, watchUpdates, results, failures, summary },
null,
2,
),
);
console.log(
JSON.stringify({
output,
node: process.version,
size,
trials,
watchCount,
watchUpdates,
failures: failures.length,
summary,
}),
);

View file

@ -0,0 +1,436 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { setImmediate } from "node:timers/promises";
import type { Draft } from "../../src/delta/draft.ts";
import { applyImmutable, type Op, track } from "../../src/delta/index.ts";
type Mode = "tracker";
type Scenario =
| "import"
| "committed-read"
| "draft-read"
| "sparse"
| "queue"
| "sort"
| "dense"
| "unshift"
| "watch-authority-control"
| "watch-live-borrowed"
| "watch-acquire-shared"
| "watch-acquire-detached"
| "watch-acquire-staggered"
| "watch-lag"
| "watch-catch-up"
| "watch-catch-up-coalesced"
| "watch-live-immutable";
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 };
type Frame = { readonly sequence: number; readonly ops: readonly Op[] };
type WatchReplica = {
value: Document;
readonly baseSequence: number;
sequence: number;
readonly queue: Frame[];
};
const MiB = 1024 * 1024;
const MODES = new Set<Mode>(["tracker"]);
const SCENARIOS = new Set<Scenario>([
"import",
"committed-read",
"draft-read",
"sparse",
"queue",
"sort",
"dense",
"unshift",
"watch-authority-control",
"watch-live-borrowed",
"watch-acquire-shared",
"watch-acquire-detached",
"watch-acquire-staggered",
"watch-lag",
"watch-catch-up",
"watch-catch-up-coalesced",
"watch-live-immutable",
]);
const WATCH_SCENARIOS = new Set<Scenario>([
"watch-authority-control",
"watch-live-borrowed",
"watch-acquire-shared",
"watch-acquire-detached",
"watch-acquire-staggered",
"watch-lag",
"watch-catch-up",
"watch-catch-up-coalesced",
"watch-live-immutable",
]);
const mode = process.argv[2] as Mode;
const scenario = process.argv[3] as Scenario;
if (!MODES.has(mode)) throw new Error(`Unknown benchmark mode ${mode}`);
if (!SCENARIOS.has(scenario)) throw new Error(`Unknown benchmark scenario ${scenario}`);
const size = Number(process.argv[4] ?? 100_000);
const trial = Number(process.argv[5] ?? 0);
const requestedWatchCount = Number(process.argv[6] ?? 8);
const requestedWatchUpdates = Number(process.argv[7] ?? 100);
if (!Number.isSafeInteger(requestedWatchCount) || requestedWatchCount < 1) throw new RangeError("Invalid watch count");
if (!Number.isSafeInteger(requestedWatchUpdates) || requestedWatchUpdates < 0)
throw new RangeError("Invalid watch update count");
function fixture(count: number): Document {
return {
rows: Array.from({ length: count }, (_, id) => ({ id, value: id, payload: `row-${id}` })),
};
}
function cloneTrusted<T>(value: T): T {
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map((child) => cloneTrusted(child)) as T;
const result = Object.create(Object.getPrototypeOf(value)) as Record<string, unknown>;
for (const key of Object.keys(value)) {
Object.defineProperty(result, key, {
value: cloneTrusted((value as Record<string, unknown>)[key]),
writable: true,
enumerable: true,
configurable: true,
});
}
return result as T;
}
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 = track(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 acquireMs = 0;
let enqueueMs = 0;
let deliveryMs = 0;
let opBytes = 0;
let operationCount = 0;
let checksum = 0;
let authoritySequence = 0;
let postAcquireRetainedMiB = 0;
let postLagRetainedMiB = 0;
let postDeliveryRetainedMiB = 0;
let sampledAcquireMiB = 0;
let watchers: WatchReplica[] = [];
let borrowedListeners: Array<(value: Document, sequence: number) => number> = [];
const queueOperations = Math.min(size, 1_000);
const watchCount = WATCH_SCENARIOS.has(scenario) ? requestedWatchCount : 0;
const watchUpdateTarget = WATCH_SCENARIOS.has(scenario) ? requestedWatchUpdates : 0;
const samplePeak = (): void => {
peakHeap = Math.max(peakHeap, process.memoryUsage().heapUsed);
};
const retainedFromBaseline = async (): Promise<number> => {
await gc();
return (process.memoryUsage().heapUsed - baselineHeap) / MiB;
};
const createWatch = (value: Document): WatchReplica => ({
value,
baseSequence: authoritySequence,
sequence: authoritySequence,
queue: [],
});
const acquireSharedWatches = (): void => {
const beforeHeap = process.memoryUsage().heapUsed;
const start = performance.now();
const base = tracker.value;
watchers = Array.from({ length: watchCount }, () => createWatch(base));
acquireMs += performance.now() - start;
sampledAcquireMiB = Math.max(sampledAcquireMiB, (process.memoryUsage().heapUsed - beforeHeap) / MiB);
for (const watcher of watchers) assert.equal(watcher.value, tracker.value);
samplePeak();
};
const acquireDetachedWatches = (): void => {
const beforeHeap = process.memoryUsage().heapUsed;
const start = performance.now();
watchers = Array.from({ length: watchCount }, () => createWatch(cloneTrusted(tracker.value)));
acquireMs += performance.now() - start;
sampledAcquireMiB = Math.max(sampledAcquireMiB, (process.memoryUsage().heapUsed - beforeHeap) / MiB);
for (let left = 0; left < watchers.length; left++) {
assert.notEqual(watchers[left]!.value, tracker.value);
for (let right = left + 1; right < watchers.length; right++) {
assert.notEqual(watchers[left]!.value, watchers[right]!.value);
}
}
samplePeak();
};
const acquireOneWatch = (): void => {
const beforeHeap = process.memoryUsage().heapUsed;
const start = performance.now();
watchers.push(createWatch(tracker.value));
acquireMs += performance.now() - start;
sampledAcquireMiB = Math.max(sampledAcquireMiB, (process.memoryUsage().heapUsed - beforeHeap) / MiB);
samplePeak();
};
const commitWatchUpdate = (update: number): Frame => {
let change: ChangeLike | undefined = tracker.beginChange();
const row = ((update + 1) * 997) % size;
const mutateStart = performance.now();
change.state.rows[row]!.value = -update - 1;
mutateMs += performance.now() - mutateStart;
samplePeak();
const prepareStart = performance.now();
let prepared: PreparedLike | undefined = change.prepare();
prepareMs += performance.now() - prepareStart;
samplePeak();
const operations = prepared.ops;
operationCount += operations.length;
const adoptStart = performance.now();
tracker.adopt(prepared);
adoptMs += performance.now() - adoptStart;
samplePeak();
change = undefined;
prepared = undefined;
authoritySequence += 1;
return { sequence: authoritySequence, ops: operations };
};
const enqueueFrame = (frame: Frame): void => {
const start = performance.now();
for (const watcher of watchers) {
assert.equal(frame.sequence, watcher.sequence + watcher.queue.length + 1);
watcher.queue.push(frame);
}
enqueueMs += performance.now() - start;
};
const applyFrame = (watcher: WatchReplica, frame: Frame): void => {
assert.equal(frame.sequence, watcher.sequence + 1);
watcher.value = applyImmutable(watcher.value, frame.ops);
watcher.sequence = frame.sequence;
};
const drainWatchQueues = (measure: boolean): void => {
const start = performance.now();
for (const watcher of watchers) {
for (const frame of watcher.queue) applyFrame(watcher, frame);
watcher.queue.length = 0;
samplePeak();
}
if (measure) deliveryMs += performance.now() - start;
};
const drainWatchQueuesCoalesced = (): void => {
const start = performance.now();
for (const watcher of watchers) {
const operations = watcher.queue.flatMap((frame) => frame.ops);
watcher.value = applyImmutable(watcher.value, operations);
watcher.sequence = watcher.queue.at(-1)?.sequence ?? watcher.sequence;
watcher.queue.length = 0;
samplePeak();
}
deliveryMs += performance.now() - start;
};
const assertWatchConvergence = (): void => {
for (const watcher of watchers) {
assert.equal(watcher.sequence, authoritySequence);
assert.deepEqual(watcher.value, tracker.value);
}
};
if (scenario === "committed-read") {
const start = performance.now();
checksum = sum(tracker.value);
mutateMs = performance.now() - start;
samplePeak();
} else if (scenario === "watch-authority-control") {
for (let update = 0; update < watchUpdateTarget; update++) commitWatchUpdate(update);
postDeliveryRetainedMiB = await retainedFromBaseline();
checksum = sum(tracker.value);
} else if (scenario === "watch-live-borrowed") {
const start = performance.now();
borrowedListeners = Array.from({ length: watchCount }, () => (value, sequence) => value.rows[0]!.value + sequence);
for (const listener of borrowedListeners) checksum += listener(tracker.value, authoritySequence);
acquireMs = performance.now() - start;
postAcquireRetainedMiB = await retainedFromBaseline();
for (let update = 0; update < watchUpdateTarget; update++) {
commitWatchUpdate(update);
const deliveryStart = performance.now();
for (const listener of borrowedListeners) checksum += listener(tracker.value, authoritySequence);
deliveryMs += performance.now() - deliveryStart;
samplePeak();
}
postDeliveryRetainedMiB = await retainedFromBaseline();
} else if (scenario === "watch-acquire-shared" || scenario === "watch-acquire-detached") {
if (scenario === "watch-acquire-shared") acquireSharedWatches();
else acquireDetachedWatches();
postAcquireRetainedMiB = await retainedFromBaseline();
checksum = sum(watchers[0]!.value) + watchers.length;
} else if (scenario === "watch-acquire-staggered") {
for (let watcher = 0; watcher < watchCount; watcher++) {
acquireOneWatch();
if (watcher + 1 === watchCount) break;
const frame = commitWatchUpdate(watcher);
enqueueFrame(frame);
}
for (let watcher = 0; watcher < watchers.length; watcher++) {
assert.equal(watchers[watcher]!.baseSequence, watcher);
assert.equal(watchers[watcher]!.queue.length, watchCount - watcher - 1);
}
postAcquireRetainedMiB = await retainedFromBaseline();
drainWatchQueues(false);
assertWatchConvergence();
postDeliveryRetainedMiB = await retainedFromBaseline();
checksum = sum(watchers[0]!.value) + sum(watchers.at(-1)!.value);
} else if (
scenario === "watch-lag" ||
scenario === "watch-catch-up" ||
scenario === "watch-catch-up-coalesced" ||
scenario === "watch-live-immutable"
) {
acquireSharedWatches();
postAcquireRetainedMiB = await retainedFromBaseline();
for (let update = 0; update < watchUpdateTarget; update++) {
const frame = commitWatchUpdate(update);
if (scenario === "watch-live-immutable") {
const start = performance.now();
for (const watcher of watchers) applyFrame(watcher, frame);
deliveryMs += performance.now() - start;
samplePeak();
} else enqueueFrame(frame);
}
postLagRetainedMiB = await retainedFromBaseline();
if (scenario === "watch-catch-up-coalesced") drainWatchQueuesCoalesced();
else if (scenario !== "watch-live-immutable") drainWatchQueues(scenario === "watch-catch-up");
assertWatchConvergence();
postDeliveryRetainedMiB = await retainedFromBaseline();
checksum = sum(watchers[0]!.value) + sum(tracker.value);
} 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;
}
default:
throw new Error(`Unhandled benchmark scenario ${scenario}`);
}
mutateMs = performance.now() - start;
samplePeak();
const prepareStart = performance.now();
let prepared: PreparedLike | undefined = change.prepare();
prepareMs = performance.now() - prepareStart;
samplePeak();
const serializeStart = performance.now();
const serialized = JSON.stringify(prepared.ops);
serializeMs = performance.now() - serializeStart;
opBytes = Buffer.byteLength(serialized);
operationCount = prepared.ops.length;
samplePeak();
const adoptStart = performance.now();
tracker.adopt(prepared);
adoptMs = performance.now() - adoptStart;
change = undefined;
prepared = undefined;
}
const scenarioWorkMs = mutateMs + prepareMs + serializeMs + adoptMs + acquireMs + enqueueMs + deliveryMs;
samplePeak();
await gc();
const retainedHeap = process.memoryUsage().heapUsed;
const retainedMiB = (retainedHeap - baselineHeap) / MiB;
const retainedOverReadyMiB = (retainedHeap - readyHeap) / MiB;
const watcherRoots = watchers.length;
const queuedBatchReferences = watchers.reduce((total, watcher) => total + watcher.queue.length, 0);
const listenerCount = borrowedListeners.length;
watchers = [];
borrowedListeners = [];
await gc();
const releasedRetainedMiB = (process.memoryUsage().heapUsed - baselineHeap) / MiB;
console.log(
JSON.stringify({
mode,
scenario,
size,
trial,
queueOperations,
watchProfile: WATCH_SCENARIOS.has(scenario) ? "local-sparse-array-leaf" : null,
watchCount: watcherRoots + listenerCount,
watchUpdates: authoritySequence,
queuedBatchReferences,
importMs,
mutateMs,
prepareMs,
serializeMs,
adoptMs,
acquireMs,
enqueueMs,
deliveryMs,
scenarioWorkMs,
opBytes,
operationCount,
checksum,
readyMiB: (readyHeap - baselineHeap) / MiB,
sampledAcquireMiB,
sampledTransientMiB: (peakHeap - readyHeap) / MiB,
retainedMiB,
retainedOverReadyMiB,
postAcquireRetainedMiB,
postLagRetainedMiB,
postDeliveryRetainedMiB,
releasedRetainedMiB,
maxRssMiB: process.resourceUsage().maxRSS / 1024,
}),
);

View file

@ -0,0 +1,337 @@
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";
// Provisional Pico5 benchmark schemas. Results are architecture experiments,
// not compatibility commitments for public or persisted ConversationView data.
const PROVISIONAL_SCHEMA = "pico5.conversation-view.provisional.v0";
type Mode = "tracker";
type Scenario =
| "projection-control"
| "normal"
| "fanout"
| "delayed-start-lag"
| "churn-reconnect"
| "overflow-reset"
| "branch-head-replacement";
type Distribution = {
count: number;
totalMs: number;
medianMs: number;
p95Ms: number;
p99Ms: number;
minMs: number;
maxMs: number;
};
type PhaseName =
| "mutation"
| "prepare"
| "adopt"
| "synthesize"
| "projectApply"
| "project"
| "enqueue"
| "sessionLine"
| "delivery"
| "opSerialize"
| "resetSerialize"
| "conversationAcquire"
| "documentAcquire"
| "conversationSerialize"
| "documentSerialize"
| "resetClone";
type Result = {
mode: Mode;
scenario: Scenario;
trial: number;
provisionalSchema: string;
headlineEntries: number;
commitTarget: number;
initialAggregateBytes: number;
conversationWatchCount: number;
documentWatchCount: number;
queueCapacity: number | null;
queueCapacityUnit: "pending-operations";
deliveryModel: "async-serialized-ops-only-no-consumer-apply";
conversationSequence: number;
documentSequences: Record<string, number>;
publishedAggregateBatches: number;
publishedDocumentBatches: number;
conversationRegistrations: number;
documentRegistrations: number;
conversationBaseBytes: number;
documentBaseBytes: number;
noOpCommitCount: number;
unmountedCommitCount: number;
resetCount: number;
resetBytes: number;
deliveryFrameReferences: number;
deliveryOperationReferences: number;
aggregateOperationCount: number;
aggregateOperationBytes: number;
documentOperationCount: number;
documentOperationBytes: number;
peakQueueReferences: number;
peakQueuedOperationReferences: number;
peakPendingOperationsPerWatch: number;
peakUniqueQueuedBatches: number;
finalQueueReferences: number;
finalQueuedOperationReferences: number;
finalUniqueQueuedBatches: number;
phaseDistributions: Record<PhaseName, Distribution>;
authorityReadyMiB: number;
postAcquireRetainedMiB: number;
postWorkRetainedMiB: number;
postVerificationRetainedMiB: number;
releaseHeapMiB: number;
releaseTotalMiB: number;
peakHeapMiB: number;
peakTotalHeapMiB: number;
maxRssMiB: number;
checksum: number;
};
type Failure = { mode: Mode; scenario: Scenario; trial: number; stderr: string };
type NumericResultKey =
| "initialAggregateBytes"
| "conversationSequence"
| "publishedAggregateBatches"
| "publishedDocumentBatches"
| "conversationRegistrations"
| "documentRegistrations"
| "conversationBaseBytes"
| "documentBaseBytes"
| "noOpCommitCount"
| "unmountedCommitCount"
| "resetCount"
| "resetBytes"
| "deliveryFrameReferences"
| "deliveryOperationReferences"
| "aggregateOperationCount"
| "aggregateOperationBytes"
| "documentOperationCount"
| "documentOperationBytes"
| "peakQueueReferences"
| "peakQueuedOperationReferences"
| "peakPendingOperationsPerWatch"
| "peakUniqueQueuedBatches"
| "finalQueueReferences"
| "finalQueuedOperationReferences"
| "finalUniqueQueuedBatches"
| "authorityReadyMiB"
| "postAcquireRetainedMiB"
| "postWorkRetainedMiB"
| "postVerificationRetainedMiB"
| "releaseHeapMiB"
| "releaseTotalMiB"
| "peakHeapMiB"
| "peakTotalHeapMiB"
| "maxRssMiB";
type SummaryValue = { median: number | null; range: readonly [number, number] | null };
const modes: readonly Mode[] = ["tracker"];
const scenarios: readonly Scenario[] = [
"projection-control",
"normal",
"fanout",
"delayed-start-lag",
"churn-reconnect",
"overflow-reset",
"branch-head-replacement",
];
const phaseNames: readonly PhaseName[] = [
"mutation",
"prepare",
"adopt",
"synthesize",
"projectApply",
"project",
"enqueue",
"sessionLine",
"delivery",
"opSerialize",
"resetSerialize",
"conversationAcquire",
"documentAcquire",
"conversationSerialize",
"documentSerialize",
"resetClone",
];
const numericMetrics: readonly NumericResultKey[] = [
"initialAggregateBytes",
"conversationSequence",
"publishedAggregateBatches",
"publishedDocumentBatches",
"conversationRegistrations",
"documentRegistrations",
"conversationBaseBytes",
"documentBaseBytes",
"noOpCommitCount",
"unmountedCommitCount",
"resetCount",
"resetBytes",
"deliveryFrameReferences",
"deliveryOperationReferences",
"aggregateOperationCount",
"aggregateOperationBytes",
"documentOperationCount",
"documentOperationBytes",
"peakQueueReferences",
"peakQueuedOperationReferences",
"peakPendingOperationsPerWatch",
"peakUniqueQueuedBatches",
"finalQueueReferences",
"finalQueuedOperationReferences",
"finalUniqueQueuedBatches",
"authorityReadyMiB",
"postAcquireRetainedMiB",
"postWorkRetainedMiB",
"postVerificationRetainedMiB",
"releaseHeapMiB",
"releaseTotalMiB",
"peakHeapMiB",
"peakTotalHeapMiB",
"maxRssMiB",
];
function optionNumber(name: string, fallback: number): number {
const prefix = `--${name}=`;
const raw = process.argv.find((argument) => argument.startsWith(prefix))?.slice(prefix.length);
if (raw === undefined) return fallback;
const value = Number(raw);
if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`Invalid --${name}`);
return value;
}
function median(values: readonly number[]): number | null {
if (values.length === 0) return null;
const ordered = [...values].sort((left, right) => left - right);
return ordered[Math.floor(ordered.length / 2)]!;
}
function range(values: readonly number[]): readonly [number, number] | null {
return values.length === 0 ? null : [Math.min(...values), Math.max(...values)];
}
function summarize(values: readonly number[]): SummaryValue {
return { median: median(values), range: range(values) };
}
const quick = process.argv.includes("--quick");
const selectedScenario = process.argv
.find((argument) => argument.startsWith("--scenario="))
?.slice("--scenario=".length);
if (selectedScenario !== undefined && !(scenarios as readonly string[]).includes(selectedScenario)) {
throw new Error(`Unknown scenario ${selectedScenario}`);
}
const selectedScenarios = selectedScenario === undefined ? scenarios : [selectedScenario as Scenario];
const headlineEntries = optionNumber("entries", quick ? 128 : 512);
const commits = optionNumber("commits", quick ? 120 : 1_000);
const trials = optionNumber("trials", quick ? 1 : 3);
const queueOperationCapacity = quick ? 64 : 256;
const root = fileURLToPath(new URL("../../../..", import.meta.url));
const worker = fileURLToPath(new URL("./conversation-view-benchmark.worker.ts", import.meta.url));
const results: Result[] = [];
const failures: Failure[] = [];
for (let trial = 0; trial < trials; trial++) {
for (const scenario of selectedScenarios) {
const offset = trial % modes.length;
const trialModes = [...modes.slice(offset), ...modes.slice(0, offset)];
for (const mode of trialModes) {
const child = spawnSync(
process.execPath,
[
"--expose-gc",
"--max-old-space-size=4096",
worker,
mode,
scenario,
String(headlineEntries),
String(commits),
String(trial),
String(queueOperationCapacity),
],
{ cwd: root, encoding: "utf8", timeout: 300_000, maxBuffer: 20 * 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));
}
}
}
const summary = selectedScenarios.flatMap((scenario) =>
modes.map((mode) => {
const selected = results.filter((result) => result.scenario === scenario && result.mode === mode);
const metrics: Record<string, SummaryValue> = {};
for (const metric of numericMetrics) metrics[metric] = summarize(selected.map((result) => result[metric]));
const phases: Record<
string,
{ medianMs: SummaryValue; p95Ms: SummaryValue; p99Ms: SummaryValue; totalMs: SummaryValue }
> = {};
for (const phase of phaseNames) {
phases[phase] = {
medianMs: summarize(selected.map((result) => result.phaseDistributions[phase].medianMs)),
p95Ms: summarize(selected.map((result) => result.phaseDistributions[phase].p95Ms)),
p99Ms: summarize(selected.map((result) => result.phaseDistributions[phase].p99Ms)),
totalMs: summarize(selected.map((result) => result.phaseDistributions[phase].totalMs)),
};
}
return {
mode,
scenario,
provisionalSchema: PROVISIONAL_SCHEMA,
conversationWatchCount: selected[0]?.conversationWatchCount ?? 0,
documentWatchCount: selected[0]?.documentWatchCount ?? 0,
queueCapacity: selected[0]?.queueCapacity ?? null,
queueCapacityUnit: "pending-operations",
deliveryModel: "async-serialized-ops-only-no-consumer-apply",
metrics,
phases,
};
}),
);
const output = join(tmpdir(), "chord-conversation-view-benchmark.json");
writeFileSync(
output,
JSON.stringify(
{
node: process.version,
provisionalSchema: PROVISIONAL_SCHEMA,
profile: "local-pico5-provisional-conversation-view",
headlineEntries,
commits,
trials,
queueOperationCapacity,
results,
failures,
summary,
},
null,
2,
),
);
console.log(
JSON.stringify({
output,
node: process.version,
provisionalSchema: PROVISIONAL_SCHEMA,
headlineEntries,
commits,
trials,
queueOperationCapacity,
failures: failures.length,
summary,
}),
);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,142 @@
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 = "tracker";
type Scenario = "idle-control" | "sparse-burst" | "staggered-revisions" | "lagging-watches";
type Result = {
mode: Mode;
scenario: Scenario;
trial: number;
size: number;
updates: number;
watchCount: number;
workMs: number;
acquisitionMs: number;
gcCount: number;
gcMs: number;
readyHeapMiB: number;
readyRssMiB: number;
peakOverReadyHeapMiB: number;
peakOverProcessBaselineHeapMiB: number;
peakOverReadyRssMiB: number;
retainedOverReadyHeapMiB: number;
retainedOverProcessBaselineHeapMiB: number;
retainedOverReadyRssMiB: number;
snapshotRetainedHeapMiB: number;
afterSnapshotReleaseOverReadyHeapMiB: number;
afterSnapshotReleaseOverProcessBaselineHeapMiB: number;
releasedOverProcessBaselineHeapMiB: number;
releasedOverProcessBaselineRssMiB: number;
maxRssMiB: number;
checksum: number;
};
type Failure = { mode: Mode; scenario: Scenario; trial: number; stderr: string };
const modes: readonly Mode[] = ["tracker"];
const scenarios: readonly Scenario[] = ["idle-control", "sparse-burst", "staggered-revisions", "lagging-watches"];
const metrics = [
"workMs",
"acquisitionMs",
"gcCount",
"gcMs",
"readyHeapMiB",
"readyRssMiB",
"peakOverReadyHeapMiB",
"peakOverProcessBaselineHeapMiB",
"peakOverReadyRssMiB",
"retainedOverReadyHeapMiB",
"retainedOverProcessBaselineHeapMiB",
"retainedOverReadyRssMiB",
"snapshotRetainedHeapMiB",
"afterSnapshotReleaseOverReadyHeapMiB",
"afterSnapshotReleaseOverProcessBaselineHeapMiB",
"releasedOverProcessBaselineHeapMiB",
"releasedOverProcessBaselineRssMiB",
"maxRssMiB",
] as const satisfies readonly (keyof Result)[];
function median(values: readonly number[]): number | null {
if (values.length === 0) return null;
const ordered = [...values].sort((left, right) => left - right);
return ordered[Math.floor(ordered.length / 2)]!;
}
function range(values: readonly number[]): readonly [number, number] | null {
return values.length === 0 ? null : [Math.min(...values), Math.max(...values)];
}
const quick = process.argv.includes("--quick");
const size = quick ? 10_000 : 100_000;
const updates = quick ? 20 : 100;
const watchCount = quick ? 4 : 8;
const trials = quick ? 1 : 3;
const root = fileURLToPath(new URL("../../../..", import.meta.url));
const worker = fileURLToPath(new URL("./memory-benchmark.worker.ts", import.meta.url));
const results: Result[] = [];
const failures: Failure[] = [];
for (let trial = 0; trial < trials; trial++) {
for (const scenario of scenarios) {
const offset = trial % modes.length;
const trialModes = [...modes.slice(offset), ...modes.slice(0, offset)];
for (const mode of trialModes) {
const child = spawnSync(
process.execPath,
[
"--expose-gc",
"--no-flush-bytecode",
"--max-old-space-size=4096",
worker,
mode,
scenario,
String(size),
String(updates),
String(watchCount),
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));
continue;
}
const result = JSON.parse(child.stdout) as Result;
results.push(result);
console.log(JSON.stringify(result));
}
}
}
const summary = scenarios.flatMap((scenario) =>
modes.map((mode) => {
const selected = results.filter((result) => result.scenario === scenario && result.mode === mode);
const values: Record<string, { median: number | null; range: readonly [number, number] | null }> = {};
for (const metric of metrics) {
const samples = selected.map((result) => result[metric]);
values[metric] = { median: median(samples), range: range(samples) };
}
return { scenario, mode, metrics: values };
}),
);
const output = join(tmpdir(), "chord-delta-memory-benchmark.json");
writeFileSync(
output,
JSON.stringify({ node: process.version, size, updates, watchCount, trials, results, failures, summary }, null, 2),
);
console.log(
JSON.stringify({
output,
node: process.version,
size,
updates,
watchCount,
trials,
failures: failures.length,
summary,
}),
);

View file

@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import { PerformanceObserver, performance } from "node:perf_hooks";
import { setImmediate } from "node:timers/promises";
import type { Draft } from "../../src/delta/draft.ts";
import { type Op, track } from "../../src/delta/index.ts";
const MiB = 1024 * 1024;
type Mode = "tracker";
type Scenario = "idle-control" | "sparse-burst" | "staggered-revisions" | "lagging-watches";
type Row = { id: number; value: number; payload: string };
type Document = { rows: Row[] };
type PreparedLike = { readonly value: 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 MODES = new Set<Mode>(["tracker"]);
const SCENARIOS = new Set<Scenario>(["idle-control", "sparse-burst", "staggered-revisions", "lagging-watches"]);
const mode = process.argv[2] as Mode;
const scenario = process.argv[3] as Scenario;
const size = Number(process.argv[4] ?? 100_000);
const updates = Number(process.argv[5] ?? 100);
const watchCount = Number(process.argv[6] ?? 8);
const trial = Number(process.argv[7] ?? 0);
if (!MODES.has(mode)) throw new Error(`Unknown mode ${mode}`);
if (!SCENARIOS.has(scenario)) throw new Error(`Unknown scenario ${scenario}`);
if (!Number.isSafeInteger(size) || size < 1) throw new RangeError("Invalid size");
if (!Number.isSafeInteger(updates) || updates < 1) throw new RangeError("Invalid update count");
if (!Number.isSafeInteger(watchCount) || watchCount < 1) throw new RangeError("Invalid watch count");
function fixture(count: number): Document {
return { rows: Array.from({ length: count }, (_, id) => ({ id, value: id, payload: `row-${id}` })) };
}
async function collect(): Promise<void> {
assert.ok(global.gc, "memory benchmark requires --expose-gc");
for (let attempt = 0; attempt < 3; attempt++) {
await setImmediate();
global.gc();
}
}
function createTracker(value: Document): TrackerLike {
return track(value) as TrackerLike;
}
function commitUpdate(tracker: TrackerLike, update: number): void {
const change = tracker.beginChange();
const row = ((update + 1) * 997) % size;
change.state.rows[row]!.value = -update - 1;
const prepared = change.prepare();
tracker.adopt(prepared);
}
function acquireSnapshot(tracker: TrackerLike): Document {
return tracker.value;
}
let warmup: TrackerLike | undefined = createTracker(fixture(16));
const warmupChange = warmup.beginChange();
warmupChange.state.rows[0]!.value = -1;
const warmupPrepared = warmupChange.prepare();
warmup.adopt(warmupPrepared);
warmup = undefined;
await collect();
const processBaseline = process.memoryUsage();
let input: Document | undefined = fixture(size);
let tracker: TrackerLike | undefined = createTracker(input);
input = undefined;
await collect();
const ready = process.memoryUsage();
let peakHeap = ready.heapUsed;
let peakRss = ready.rss;
let acquisitionMs = 0;
let snapshots: Document[] = [];
let gcCount = 0;
let gcMs = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
gcCount += 1;
gcMs += entry.duration;
}
});
observer.observe({ entryTypes: ["gc"] });
const samplePeak = (): void => {
const memory = process.memoryUsage();
peakHeap = Math.max(peakHeap, memory.heapUsed);
peakRss = Math.max(peakRss, memory.rss);
};
const acquire = (): void => {
const start = performance.now();
snapshots.push(acquireSnapshot(tracker!));
acquisitionMs += performance.now() - start;
samplePeak();
};
const workStart = performance.now();
if (scenario === "idle-control") {
for (let update = 0; update < updates; update++) samplePeak();
} else if (scenario === "sparse-burst") {
for (let update = 0; update < updates; update++) {
commitUpdate(tracker, update);
samplePeak();
}
} else if (scenario === "staggered-revisions") {
for (let watcher = 0; watcher < watchCount; watcher++) {
acquire();
if (watcher + 1 < watchCount) commitUpdate(tracker, watcher);
samplePeak();
}
} else {
for (let watcher = 0; watcher < watchCount; watcher++) acquire();
for (let update = 0; update < updates; update++) {
commitUpdate(tracker, update);
samplePeak();
}
}
const workMs = performance.now() - workStart;
for (let attempt = 0; attempt < 3; attempt++) await setImmediate();
observer.disconnect();
samplePeak();
await collect();
const retained = process.memoryUsage();
const checksum = tracker.value.rows[0]!.value + tracker.value.rows.at(-1)!.value + snapshots.length;
snapshots = [];
await collect();
const afterSnapshotRelease = process.memoryUsage();
tracker = undefined;
await collect();
const released = process.memoryUsage();
console.log(
JSON.stringify({
mode,
scenario,
trial,
size,
updates: scenario === "staggered-revisions" ? watchCount - 1 : scenario === "idle-control" ? 0 : updates,
watchCount: scenario === "staggered-revisions" || scenario === "lagging-watches" ? watchCount : 0,
workMs,
acquisitionMs,
gcCount,
gcMs,
readyHeapMiB: (ready.heapUsed - processBaseline.heapUsed) / MiB,
readyRssMiB: (ready.rss - processBaseline.rss) / MiB,
peakOverReadyHeapMiB: (peakHeap - ready.heapUsed) / MiB,
peakOverProcessBaselineHeapMiB: (peakHeap - processBaseline.heapUsed) / MiB,
peakOverReadyRssMiB: (peakRss - ready.rss) / MiB,
retainedOverReadyHeapMiB: (retained.heapUsed - ready.heapUsed) / MiB,
retainedOverProcessBaselineHeapMiB: (retained.heapUsed - processBaseline.heapUsed) / MiB,
retainedOverReadyRssMiB: (retained.rss - ready.rss) / MiB,
snapshotRetainedHeapMiB: (retained.heapUsed - afterSnapshotRelease.heapUsed) / MiB,
afterSnapshotReleaseOverReadyHeapMiB: (afterSnapshotRelease.heapUsed - ready.heapUsed) / MiB,
afterSnapshotReleaseOverProcessBaselineHeapMiB: (afterSnapshotRelease.heapUsed - processBaseline.heapUsed) / MiB,
releasedOverProcessBaselineHeapMiB: (released.heapUsed - processBaseline.heapUsed) / MiB,
releasedOverProcessBaselineRssMiB: (released.rss - processBaseline.rss) / MiB,
maxRssMiB: process.resourceUsage().maxRSS / 1024,
checksum,
}),
);

View file

@ -2,32 +2,42 @@ import { describe, expect, it } from "vitest";
import { apply, track } from "../src/delta/index.ts";
describe("tracker ownership", () => {
it("deeply detaches and freezes the imported revision", () => {
it("takes immutable ownership of the imported revision in O(1)", () => {
const input = {
point: { x: 3, y: 7, pressure: 0.1 },
rows: [{ values: [0, false, null, "text", { n: 1 }] }],
};
const tracker = track(input);
expect(tracker.value).toEqual(input);
expect(tracker.value).not.toBe(input);
expect(tracker.value.point).not.toBe(input.point);
expect(tracker.value.rows[0]!.values[4]).not.toBe(input.rows[0]!.values[4]);
expect(Object.isFrozen(tracker.value.rows[0]!.values)).toBe(true);
input.point.x = 99;
expect(tracker.value.point.x).toBe(3);
expect(tracker.value).toBe(input);
expect(tracker.value.point).toBe(input.point);
expect(tracker.value.rows[0]!.values[4]).toBe(input.rows[0]!.values[4]);
expect(Object.isFrozen(tracker.value.rows[0]!.values)).toBe(false);
});
it("preserves null prototypes and expands aliases", () => {
it("preserves null prototypes in an alias-free owned root", () => {
type Dictionary = { enabled: boolean; child: { n: number } };
const dictionary = Object.assign(Object.create(null) as Dictionary, { enabled: true, child: { n: 1 } });
const shared = { nested: [{ n: 1 }] };
const tracker = track({ dictionary, left: shared, right: shared });
const tracker = track({ dictionary, left: { nested: [{ n: 1 }] }, right: { nested: [{ n: 1 }] } });
expect(Object.getPrototypeOf(tracker.value.dictionary)).toBeNull();
expect(tracker.value.dictionary).not.toBe(dictionary);
expect(tracker.value.dictionary).toBe(dictionary);
expect(tracker.value.left).not.toBe(tracker.value.right);
expect(tracker.value.left.nested[0]).not.toBe(tracker.value.right.nested[0]);
});
it("does not traverse a trusted root while taking ownership", () => {
let reads = 0;
const input = Object.defineProperty({}, "untrustedAccessor", {
enumerable: true,
get() {
reads += 1;
throw new Error("root was traversed");
},
});
const tracker = track(input);
expect(tracker.value).toBe(input);
expect(reads).toBe(0);
});
it("copies assigned and inserted values immediately", () => {
const tracker = track({ rows: [] as { nested: { value: number } }[] });
const assigned = { nested: { value: 1 } };
@ -40,41 +50,4 @@ describe("tracker ownership", () => {
const replica = apply(structuredClone(prepared.base), prepared.ops);
expect(replica).toEqual(prepared.value);
});
it("rejects cycles, accessors, classes, and non-finite numbers", () => {
const cyclic: { self?: object } = {};
cyclic.self = cyclic;
expect(() => track(cyclic)).toThrow(/cycles/);
expect(() => track({ value: Number.NaN })).toThrow(/strict JSON/);
expect(() => track({ value: new Date() })).toThrow(/plain objects/);
const accessor = Object.defineProperty({}, "value", { enumerable: true, get: () => 1 });
expect(() => track(accessor)).toThrow(/data properties/);
});
it("rejects indexed accessors and non-enumerable array entries without invoking getters", () => {
let reads = 0;
const accessor: number[] = [];
Object.defineProperty(accessor, "0", {
enumerable: true,
configurable: true,
get() {
reads += 1;
return 1;
},
});
expect(() => track({ values: accessor })).toThrow(/data properties/);
expect(reads).toBe(0);
const hidden: number[] = [];
Object.defineProperty(hidden, "0", { value: 1, enumerable: false, configurable: true, writable: true });
expect(() => track({ values: hidden })).toThrow(/enumerable/);
const tracker = track({ values: [] as number[] });
const change = tracker.beginChange();
expect(() => {
change.state.values = accessor;
}).toThrow(/data properties/);
expect(reads).toBe(0);
change.abort();
});
});

View file

@ -1,95 +0,0 @@
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

@ -1,145 +0,0 @@
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

@ -1,501 +0,0 @@
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

@ -1,538 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,34 +0,0 @@
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]);
});
});

View file

@ -7,8 +7,8 @@ import {
diffRevisions,
encoder,
type Op,
} from "../../src/delta/cow/index.ts";
import type { JsonValue } from "../../src/types.ts";
} from "../src/delta/index.ts";
import type { JsonValue } from "../src/types.ts";
const expectDiff = (before: JsonValue, after: JsonValue, expected: unknown): void => {
const operations = diffRevisions(before, after);
@ -257,11 +257,11 @@ describe("immutable revision diff", () => {
expect(diffRevisions(before, after)).toEqual([["r", after]]);
});
it("uses normalized array fallback without payload-cost policy", () => {
it("bounds a wide normalized array fallback with a root replacement", () => {
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(operations).toEqual([["r", after]]);
expect(applyImmutable(before, operations)).toEqual(after);
});
});

View file

@ -1,19 +0,0 @@
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"])(
"releases %s",
(scenario) => {
const child = spawnSync(
process.execPath,
["--expose-gc", fileURLToPath(new URL("./delta-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

@ -1,82 +0,0 @@
import assert from "node:assert/strict";
import { setImmediate } from "node:timers/promises";
import { type Tracker, track } from "../src/delta/index.ts";
const retainedSettledObjects: 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;
}
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 {
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

@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
describe("delta change retention across GC jobs", () => {
describe("delta tracker retention across GC jobs", () => {
it.each([
"aborted-payload",
"unadopted-prepared",
@ -11,6 +11,12 @@ describe("delta change retention across GC jobs", () => {
"obsolete-revisions",
"retained-settled-prepared",
"retained-settled-change",
"retained-settled-proxy",
"retained-large-settled-proxy",
"retained-large-placement-proxies",
"stale-unprepared-change",
"same-job-fast-cleanup",
"same-job-folded-ops-cleanup",
"lifecycle-churn",
])(
"validates %s retention semantics",

View file

@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { setImmediate } from "node:timers/promises";
import { type Tracker, track } from "../../src/delta/cow/index.ts";
import { type Tracker, track } from "../../src/delta/index.ts";
const retainedSettledObjects: object[] = [];
const retainedLargePrepared: object[] = [];
@ -119,6 +119,147 @@ if (scenario === "settled-lifecycle") {
await collect(references.base);
await collect(references.value);
assert.equal(retainedSettledChanges.length, 1);
} else if (scenario === "retained-settled-proxy") {
const tracker = track({
child: { value: 0 },
payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) },
});
const obsolete = new WeakRef(tracker.value.payload.rows);
const commit = (): object => {
const change = tracker.beginChange();
const held = change.state.child;
held.value = 1;
const prepared = change.prepare();
tracker.adopt(prepared);
return held;
};
retainedSettledChanges.push(commit());
const replace = (): void => {
const replacement = tracker.prepareReplace({ child: { value: 2 }, payload: { rows: [{ value: 1 }] } });
tracker.adopt(replacement);
};
replace();
await collect(obsolete);
assert.throws(() => (retainedSettledChanges[0] as { value: number }).value, /settled/);
} else if (scenario === "retained-large-settled-proxy") {
const tracker = track({ rows: Array.from({ length: 100_000 }, (_, value) => ({ value })) });
const obsolete = new WeakRef(tracker.value.rows);
const commit = (): object => {
const change = tracker.beginChange();
const held = change.state.rows[0]!;
for (let index = 1; index < 5_000; index++) assert.equal(change.state.rows[index]!.value, index);
held.value = -1;
const prepared = change.prepare();
tracker.adopt(prepared);
return held;
};
retainedSettledChanges.push(commit());
const replace = (): void => {
const replacement = tracker.prepareReplace({ rows: [{ value: 1 }] });
tracker.adopt(replacement);
};
replace();
await collect(obsolete);
assert.throws(() => (retainedSettledChanges[0] as { value: number }).value, /settled/);
} else if (scenario === "retained-large-placement-proxies") {
type Root = {
child: { value: number };
visited: { value: number }[];
first: number[] | null;
second: number[] | null;
rows: Array<number[] | null>;
};
const run = (kind: "write" | "writes" | "insert" | "override"): WeakRef<object> => {
const tracker = track<Root>({
child: { value: 0 },
visited: Array.from({ length: 5_000 }, (_, value) => ({ value })),
first: null,
second: null,
rows: kind === "override" ? [null] : [],
});
const change = tracker.beginChange();
for (const value of change.state.visited) assert.equal(value.value >= 0, true);
let held: object;
if (kind === "write" || kind === "writes") {
held = change.state.child;
change.state.first = Array<number>(50_000).fill(1);
if (kind === "writes") change.state.second = Array<number>(50_000).fill(2);
} else {
held = change.state.rows;
if (kind === "insert") change.state.rows.push(Array<number>(50_000).fill(1));
else change.state.rows[0] = Array<number>(50_000).fill(1);
}
const prepared = change.prepare();
const payload = kind === "write" || kind === "writes" ? prepared.value.first : prepared.value.rows[0];
assert.ok(payload);
const reference = new WeakRef(payload);
tracker.adopt(prepared);
const replacement = tracker.prepareReplace({
child: { value: 1 },
visited: [],
first: null,
second: null,
rows: [],
});
tracker.adopt(replacement);
retainedSettledChanges.push(held);
return reference;
};
const references = [run("write"), run("writes"), run("insert"), run("override")];
for (const reference of references) await collect(reference);
for (const held of retainedSettledChanges) assert.throws(() => Reflect.get(held, "length"), /settled/);
} else if (scenario === "stale-unprepared-change") {
const tracker = track({ payload: { rows: Array.from({ length: 50_000 }, (_, value) => ({ value })) } });
const stale = tracker.beginChange();
stale.state.payload.rows[0]!.value = -1;
const obsolete = new WeakRef(tracker.value.payload.rows);
const replace = (): void => {
const winner = tracker.prepareReplace({ payload: { rows: [{ value: 1 }] } });
tracker.adopt(winner);
};
replace();
assert.throws(() => stale.prepare(), /settled/);
retainedSettledChanges.push(stale);
await collect(obsolete);
assert.equal(retainedSettledChanges.length, 1);
} else if (scenario === "same-job-fast-cleanup") {
assert.ok(global.gc, "worker requires --expose-gc");
const tracker = track({ rows: Array.from({ length: 100_000 }, (_, value) => ({ value })) });
global.gc();
const readyHeap = process.memoryUsage().heapUsed;
const commit = (value: number): void => {
const change = tracker.beginChange();
const draft = change.state;
for (let index = 0; index < 4_094; index++) assert.equal(draft.rows[index]!.value, index);
draft.rows.at(-1)!.value = value;
const prepared = change.prepare();
tracker.adopt(prepared);
};
for (let value = 0; value < 100; value++) commit(-value - 1);
global.gc();
const retainedMiB = (process.memoryUsage().heapUsed - readyHeap) / (1024 * 1024);
assert.ok(retainedMiB < 16, `same-job cleanup retained ${retainedMiB.toFixed(2)} MiB`);
assert.equal(tracker.value.rows.at(-1)!.value, -100);
} else if (scenario === "same-job-folded-ops-cleanup") {
assert.ok(global.gc, "worker requires --expose-gc");
const tracker = track(
Object.fromEntries(Array.from({ length: 5_000 }, (_, index) => [`field${index}`, 0])) as Record<string, number>,
);
global.gc();
const readyHeap = process.memoryUsage().heapUsed;
const commit = (value: number): void => {
const change = tracker.beginChange();
for (let index = 0; index < 5_000; index++) change.state[`field${index}`] = value;
const prepared = change.prepare();
assert.equal(prepared.ops.length, 1);
assert.equal(prepared.ops[0]![0], "r");
tracker.adopt(prepared);
};
for (let value = 1; value <= 100; value++) commit(value);
global.gc();
const retainedMiB = (process.memoryUsage().heapUsed - readyHeap) / (1024 * 1024);
assert.ok(retainedMiB < 8, `same-job folded operations retained ${retainedMiB.toFixed(2)} MiB`);
assert.equal(tracker.value.field0, 100);
} else if (scenario === "lifecycle-churn") {
const tracker = track({ value: 0 });
for (let index = 0; index < 100_000; index++) {

View file

@ -1,7 +1,6 @@
import { isProxy } from "node:util/types";
import { describe, expect, it } from "vitest";
import { type Draft, type Op, track } from "../../src/delta/astra/index.ts";
import { apply, decoder, encoder, type JsonValue } from "../../src/delta/index.ts";
import { apply, type Draft, decoder, encoder, type JsonValue, type Op, track } from "../../src/delta/index.ts";
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
@ -9,20 +8,41 @@ function replay<T>(base: T, operations: readonly Op[]): T {
return apply(clone(base), clone(operations));
}
function expectAliasFree(value: unknown): void {
const seen = new WeakMap<object, string>();
const visit = (current: unknown, path: string): void => {
if (current === null || typeof current !== "object") return;
const previous = seen.get(current);
if (previous !== undefined) throw new Error(`container at ${path} aliases ${previous}`);
seen.set(current, path);
if (Array.isArray(current)) {
for (let index = 0; index < current.length; index++) visit(current[index], `${path}[${index}]`);
return;
}
for (const key of Object.keys(current)) visit((current as Record<string, unknown>)[key], `${path}.${key}`);
};
visit(value, "$root");
}
function settle<T extends object>(tracker: ReturnType<typeof track<T>>, mutate: (draft: Draft<T>) => void): T {
const base = clone(tracker.value);
const baseRoot = tracker.value;
const base = clone(baseRoot);
const change = tracker.beginChange();
mutate(change.state);
const prepared = change.prepare();
const candidate = clone(prepared.value);
expect(isProxy(prepared.value)).toBe(false);
expect(replay(base, prepared.ops)).toEqual(candidate);
expect(baseRoot).toEqual(base);
tracker.adopt(prepared);
expect(baseRoot).toEqual(base);
expect(tracker.value).toBe(prepared.value);
expect(tracker.value).toEqual(candidate);
return candidate;
}
describe("astra transactional overlay lifecycle", () => {
it("borrows the committed root and leaves it untouched until adopt", () => {
describe("delta tracker transactional overlay lifecycle", () => {
it("materializes an immutable next revision and adopts it by pointer swap", () => {
const initial = { count: 1, nested: { text: "a" }, values: [1] };
const tracker = track(initial);
expect(tracker.value).toBe(initial);
@ -32,28 +52,24 @@ describe("astra transactional overlay lifecycle", () => {
change.state.values.push(2);
expect(initial).toEqual({ count: 1, nested: { text: "a" }, values: [1] });
const prepared = change.prepare();
const next = prepared.value;
expect(prepared.baseRevision).toBe(0);
expect(prepared.base).toBe(initial);
expect(prepared.value).toEqual({ count: 2, nested: { text: "ab" }, values: [1, 2] });
expect(next).not.toBe(initial);
expect(isProxy(next)).toBe(false);
expect(next).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(() => {
(prepared.value as { count: number }).count = 9;
}).toThrow(/read-only/);
expect(tracker.value).toBe(initial);
tracker.adopt(prepared);
expect(initial).toEqual({ count: 2, nested: { text: "ab" }, values: [1, 2] });
expect(prepared.value).toBe(tracker.value);
expect(prepared.value).toEqual({ count: 2, nested: { text: "ab" }, values: [1, 2] });
expect(prepared.base).toBe(prepared.value);
expect(initial).toEqual({ count: 1, nested: { text: "a" }, values: [1] });
expect(tracker.value).toBe(next);
expect(prepared.value).toBe(next);
expect(prepared.base).toBe(initial);
expect(() => change.state.count).toThrow(TypeError);
expect(prepared.ops).toEqual([
["s", ["count"], 2],
["a", ["nested", "text"], "b"],
["p", ["values"], 1, 0, [2]],
]);
});
it("keeps a transaction live across await and seals it only at prepare", async () => {
@ -66,7 +82,7 @@ describe("astra transactional overlay lifecycle", () => {
expect(prepared.value).toEqual({ left: 1, nested: { right: 2 } });
expect(() => {
change.state.left = 3;
}).toThrow(/read-only/);
}).toThrow(/settled/);
tracker.adopt(prepared);
});
@ -93,12 +109,31 @@ describe("astra transactional overlay lifecycle", () => {
const held = secondPrepared.value;
tracker.adopt(firstPrepared);
expect(tracker.value.value).toBe(1);
expect(() => held.value).toThrow(TypeError);
expect(held.value).toBe(2);
expect(() => tracker.adopt(secondPrepared)).toThrow(/stale/);
expect(() => tracker.adopt(firstPrepared)).toThrow(/already been used/);
});
it("adopts object edits in place with ordinary fast-layout descriptors", () => {
it("invalidates competing open overlays without changing their base revision", () => {
const initial = { rows: [{ value: 0 }, { value: 1 }] };
const tracker = track(initial);
const staleChange = tracker.beginChange();
const held = staleChange.state.rows[1]!;
held.value = 2;
const winner = tracker.beginChange();
winner.state.rows[0]!.value = 3;
const prepared = winner.prepare();
tracker.adopt(prepared);
expect(initial).toEqual({ rows: [{ value: 0 }, { value: 1 }] });
expect(tracker.value).toEqual({ rows: [{ value: 3 }, { value: 1 }] });
expect(() => held.value).toThrow(/settled/);
expect(() => staleChange.prepare()).toThrow(/settled/);
staleChange.abort();
});
it("adopts object edits as ordinary fast-layout immutable revisions", () => {
const initial = { first: 1, second: 2 } as { first: number; second?: number; third?: number };
const tracker = track(initial);
const change = tracker.beginChange();
@ -107,9 +142,10 @@ describe("astra transactional overlay lifecycle", () => {
change.state.third = 4;
const prepared = change.prepare();
tracker.adopt(prepared);
expect(tracker.value).toBe(initial);
expect(tracker.value).not.toBe(initial);
expect(initial).toEqual({ first: 1, second: 2 });
expect(tracker.value).toEqual({ first: 3, third: 4 });
expect(Object.getOwnPropertyDescriptor(initial, "third")).toEqual({
expect(Object.getOwnPropertyDescriptor(tracker.value, "third")).toEqual({
value: 4,
writable: true,
enumerable: true,
@ -137,9 +173,10 @@ describe("astra transactional overlay lifecycle", () => {
next.state.value = 2;
const nextPrepared = next.prepare();
tracker.adopt(nextPrepared);
// In-place adoption intentionally advances retained references to the mutable committed root.
expect(published).toBe(tracker.value);
expect(prepared.value).toEqual({ value: 2, nested: { count: 1 } });
// Retained immutable publications remain at their original revision.
expect(published).not.toBe(tracker.value);
expect(prepared.value).toEqual({ value: 1, nested: { count: 1 } });
expect(tracker.value).toEqual({ value: 2, nested: { count: 1 } });
});
it("keeps replacement base/value structurally compatible and readable after adoption", () => {
@ -148,22 +185,22 @@ describe("astra transactional overlay lifecycle", () => {
const tracker = track(original);
const prepared = tracker.prepareReplace(replacement);
expect(prepared.base).toBe(original);
expect(prepared.value).not.toBe(replacement);
expect(prepared.value).toBe(replacement);
tracker.adopt(prepared);
expect(prepared.base).toBe(original);
expect(prepared.value).toBe(replacement);
expect(prepared.value).toBe(tracker.value);
});
it("revokes aborted and stale prepared candidate views", () => {
it("keeps aborted and stale materialized candidates readable", () => {
const tracker = track({ value: 0 });
const abortedChange = tracker.beginChange();
abortedChange.state.value = 1;
const aborted = abortedChange.prepare();
const abortedView = aborted.value;
aborted.abort();
expect(() => abortedView.value).toThrow(TypeError);
expect(() => aborted.value.value).toThrow(TypeError);
expect(abortedView.value).toBe(1);
expect(aborted.value.value).toBe(1);
const loserChange = tracker.beginChange();
loserChange.state.value = 2;
@ -171,8 +208,21 @@ describe("astra transactional overlay lifecycle", () => {
const loserView = loser.value;
const winner = tracker.prepareReplace({ value: 3 });
tracker.adopt(winner);
expect(() => loserView.value).toThrow(TypeError);
expect(() => loser.value.value).toThrow(TypeError);
expect(loserView.value).toBe(2);
expect(loser.value.value).toBe(2);
});
it("lets a settled Change abort its prepared result without retaining its context", () => {
const tracker = track({ value: 0, nested: { value: 1 } });
const change = tracker.beginChange();
change.state.value = 1;
const prepared = change.prepare();
const operations = prepared.ops;
change.abort();
change.abort();
expect(prepared.ops).toBe(operations);
expect(prepared.value).toEqual({ value: 1, nested: { value: 1 } });
expect(() => tracker.adopt(prepared)).toThrow(/aborted/);
});
it("rejects foreign and aborted prepared values", () => {
@ -186,26 +236,28 @@ describe("astra transactional overlay lifecycle", () => {
});
it("reads deleted own properties as absent", () => {
Object.defineProperty(Object.prototype, "astraInherited", {
Object.defineProperty(Object.prototype, "trackerInherited", {
value: 7,
writable: true,
configurable: true,
});
try {
const tracker = track({ value: 1 } as { value?: number; astraInherited?: number });
const tracker = track({ value: 1 } as { value?: number; trackerInherited?: number });
const change = tracker.beginChange();
delete change.state.value;
change.state.astraInherited = 1;
delete change.state.astraInherited;
change.state.trackerInherited = 1;
delete change.state.trackerInherited;
expect(change.state.value).toBeUndefined();
expect(change.state.astraInherited).toBeUndefined();
expect(change.state.trackerInherited).toBeUndefined();
const prepared = change.prepare();
expect(prepared.value.value).toBeUndefined();
expect(prepared.value.astraInherited).toBeUndefined();
// Plain committed values expose inherited properties through ordinary
// JavaScript reads; strict-JSON membership remains own-properties only.
expect(prepared.value.trackerInherited).toBe(7);
tracker.adopt(prepared);
expect(Object.keys(tracker.value)).toEqual([]);
} finally {
delete (Object.prototype as Record<string, unknown>).astraInherited;
delete (Object.prototype as Record<string, unknown>).trackerInherited;
}
});
@ -252,24 +304,21 @@ describe("astra transactional overlay lifecycle", () => {
expect(tracker.value).toBe(initial);
});
it("takes O(1) ownership for replacement and detaches its operation payload", () => {
it("takes O(1) immutable ownership for replacement and its operation payload", () => {
const tracker = track({ value: 0, rows: [] as { value: number }[] });
const replacement = { value: 1, rows: [{ value: 2 }] };
const prepared = tracker.prepareReplace(replacement);
expect(prepared.base).toBe(tracker.value);
expect(prepared.value.rows).not.toBe(replacement.rows);
expect(prepared.value).toBe(replacement);
const operations = prepared.ops;
expect(operations).toEqual([["r", replacement]]);
expect((operations[0] as readonly ["r", typeof replacement])[1]).not.toBe(replacement);
expect((operations[0] as readonly ["r", typeof replacement])[1]).toBe(replacement);
tracker.adopt(prepared);
expect(tracker.value).toBe(replacement);
expect(prepared.value).toBe(replacement);
replacement.rows[0]!.value = 9;
expect(operations).toEqual([["r", { value: 1, rows: [{ value: 2 }] }]]);
});
});
describe("astra policy view", () => {
describe("delta tracker policy view", () => {
it("supports native reads, descriptors, keys, iteration, map, and JSON", () => {
const tracker = track({ values: [1, 2, 3], object: { a: 1 } as { a: number; b?: number } });
const change = tracker.beginChange();
@ -286,6 +335,26 @@ describe("astra policy view", () => {
change.abort();
});
it("keeps document keys distinct from object proxy target fields", () => {
const tracker = track({
object: {
context: 1,
base: 2,
parent: 3,
dirty: 4,
target: 5,
proxy: 6,
},
});
const change = tracker.beginChange();
change.state.object.context = 7;
change.state.object.proxy = 8;
expect(Object.keys(change.state.object)).toEqual(["context", "base", "parent", "dirty", "target", "proxy"]);
const prepared = change.prepare();
expect(prepared.value.object).toEqual({ context: 7, base: 2, parent: 3, dirty: 4, target: 5, proxy: 8 });
tracker.adopt(prepared);
});
it("keeps native coercion side effects and string operation forms", () => {
const tracker = track({ text: "abcdefgh", values: [1, 2, 3], marker: 0 });
const change = tracker.beginChange();
@ -351,6 +420,21 @@ describe("astra policy view", () => {
expect(Object.keys(tracker.value.object)).toEqual(["1", "2", "label"]);
});
it("normalizes deeply equal container assignments before direct candidate materialization", () => {
const initial = { child: { a: 1, b: 2 }, count: 0 };
const tracker = track(initial);
const change = tracker.beginChange();
change.state.child = { b: 2, a: 1 };
change.state.count = 1;
const prepared = change.prepare();
const replayed = replay(initial, prepared.ops);
expect(prepared.ops).toEqual([["s", ["count"], 1]]);
expect(Object.keys(prepared.value.child)).toEqual(["a", "b"]);
expect(Object.keys(replayed.child)).toEqual(["a", "b"]);
expect(prepared.value).toEqual(replayed);
tracker.adopt(prepared);
});
it("keeps native integer and string ordering across deletion and re-addition", () => {
const tracker = track({
object: { 1: "one", 2: "two", first: "a", second: "b" } as Record<string, string>,
@ -568,7 +652,7 @@ describe("astra policy view", () => {
});
});
describe("astra by-value placements", () => {
describe("delta tracker by-value placements", () => {
it("clones property, index, push, unshift, splice, fill, and copyWithin placements", () => {
const tracker = track({
property: null as { value: number } | null,
@ -597,6 +681,81 @@ describe("astra by-value placements", () => {
tracker.adopt(prepared);
});
it("rejects non-strict JSON placements before changing the draft", () => {
const initial = {
optional: "remove" as string | undefined,
payload: null as JsonValue,
number: 0,
values: [1, 2] as JsonValue[],
};
const tracker = track(initial);
const change = tracker.beginChange();
const undefinedValue = undefined as unknown as JsonValue;
expect(() => {
change.state.values[0] = undefinedValue;
}).toThrow(/strict JSON/);
expect(() => change.state.values.push({ staged: true }, undefinedValue)).toThrow(/strict JSON/);
expect(() => change.state.values.unshift(undefinedValue)).toThrow(/strict JSON/);
expect(() => change.state.values.splice(1, 0, undefinedValue)).toThrow(/strict JSON/);
expect(() => change.state.values.fill(undefinedValue)).toThrow(/strict JSON/);
expect(() => {
change.state.payload = { nested: undefined } as unknown as JsonValue;
}).toThrow(/strict JSON/);
expect(() => {
change.state.payload = { nested: () => 1 } as unknown as JsonValue;
}).toThrow(/strict JSON/);
expect(() => {
change.state.payload = { nested: Symbol("invalid") } as unknown as JsonValue;
}).toThrow(/strict JSON/);
expect(() => {
change.state.payload = { nested: 1n } as unknown as JsonValue;
}).toThrow(/strict JSON/);
expect(() => {
change.state.number = Number.NaN;
}).toThrow(/strict JSON/);
expect(() => {
change.state.number = Number.POSITIVE_INFINITY;
}).toThrow(/strict JSON/);
expect(() => {
change.state.payload = new Date() as unknown as JsonValue;
}).toThrow(/plain objects/);
const sparse: JsonValue[] = new Array<JsonValue>(1);
expect(() => {
change.state.payload = sparse;
}).toThrow(/dense plain arrays/);
const cyclic: { self?: unknown } = {};
cyclic.self = cyclic;
expect(() => {
change.state.payload = cyclic as JsonValue;
}).toThrow(/cycles/);
let accessorReads = 0;
const accessor = Object.defineProperty({}, "value", {
enumerable: true,
get() {
accessorReads += 1;
return 1;
},
});
expect(() => {
change.state.payload = accessor as JsonValue;
}).toThrow(/data properties/);
expect(accessorReads).toBe(0);
expect(change.state).toEqual(initial);
change.state.optional = undefined;
const dictionary = Object.create(null) as Record<string, JsonValue>;
dictionary.valid = true;
change.state.payload = dictionary;
change.state.values.push({ valid: true });
const prepared = change.prepare();
expect(prepared.value).toEqual({ payload: { valid: true }, number: 0, values: [1, 2, { valid: true }] });
expect(Object.getPrototypeOf(prepared.value.payload)).toBeNull();
expect(replay(initial, prepared.ops)).toEqual(prepared.value);
tracker.adopt(prepared);
});
it("expands repeated source aliases into independent placements", () => {
const tracker = track({
left: null as { nested: { value: number } } | null,
@ -622,6 +781,36 @@ describe("astra by-value placements", () => {
tracker.adopt(prepared);
});
it("deep-clones draft-sourced placements within and across revisions", () => {
const tracker = track({
a: { child: { value: 1 } },
b: null as { child: { value: number } } | null,
rows: [{ child: { value: 1 } }, { child: { value: 2 } }],
});
const first = tracker.beginChange();
first.state.b = first.state.a;
first.state.b.child.value = 3;
first.state.rows[1] = first.state.rows[0]!;
const firstPrepared = first.prepare();
expect(firstPrepared.value.a.child.value).toBe(1);
expect(firstPrepared.value.b?.child.value).toBe(3);
expect(firstPrepared.value.a).not.toBe(firstPrepared.value.b);
expect(firstPrepared.value.a.child).not.toBe(firstPrepared.value.b?.child);
expect(firstPrepared.value.rows[0]).not.toBe(firstPrepared.value.rows[1]);
expect(firstPrepared.value.rows[0]!.child).not.toBe(firstPrepared.value.rows[1]!.child);
expectAliasFree(firstPrepared.value);
tracker.adopt(firstPrepared);
const base = clone(tracker.value);
const second = tracker.beginChange();
second.state.rows[1]!.child.value = 4;
const secondPrepared = second.prepare();
expect(secondPrepared.value.rows.map((row) => row.child.value)).toEqual([1, 4]);
expect(replay(base, secondPrepared.ops)).toEqual(secondPrepared.value);
expectAliasFree(secondPrepared.value);
tracker.adopt(secondPrepared);
});
it("distinguishes raw committed references from draft references", () => {
const tracker = track({
source: { value: 1 },
@ -684,7 +873,7 @@ describe("astra by-value placements", () => {
sparseTracker.adopt(sparsePrepared);
}, 30_000);
it("keeps operation payloads detached from candidate and adopted placement mutation", () => {
it("shares immutable operation placements with the materialized candidate", () => {
const tracker = track({ rows: [] as { value: number }[] });
const change = tracker.beginChange();
change.state.rows.push({ value: 1 });
@ -697,13 +886,15 @@ describe("astra by-value placements", () => {
expect(isProxy(value)).toBe(false);
pending.push(...Object.values(value));
}
const splice = operations[0];
if (splice?.[0] !== "p") throw new Error("expected splice");
expect(splice[4][0]).toBe(prepared.value.rows[0]);
tracker.adopt(prepared);
tracker.value.rows[0]!.value = 9;
expect(operations).toEqual([["p", ["rows"], 0, 0, [{ value: 1 }]]]);
expect(tracker.value).toBe(prepared.value);
});
});
describe("astra piece arrays", () => {
describe("delta tracker piece arrays", () => {
it("keeps adoption independent from mutable detached permutation metadata", () => {
const tracker = track({ values: [3, 1, 2] });
const change = tracker.beginChange();
@ -716,6 +907,31 @@ describe("astra piece arrays", () => {
expect(tracker.value.values).toEqual([1, 2, 3]);
});
it("matches native splice with zero or one argument", () => {
const noArguments = track({ values: [1, 2, 3] });
const noArgumentsChange = noArguments.beginChange();
expect(Reflect.apply(noArgumentsChange.state.values.splice, noArgumentsChange.state.values, [])).toEqual([]);
const noArgumentsPrepared = noArgumentsChange.prepare();
expect(noArgumentsPrepared.value.values).toEqual([1, 2, 3]);
expect(noArgumentsPrepared.ops).toEqual([]);
noArguments.adopt(noArgumentsPrepared);
const oneArgument = track({ values: [1, 2, 3, 4] });
const oneArgumentChange = oneArgument.beginChange();
expect(oneArgumentChange.state.values.splice(1)).toEqual([2, 3, 4]);
const oneArgumentPrepared = oneArgumentChange.prepare();
expect(oneArgumentPrepared.value.values).toEqual([1]);
expect(replay(oneArgument.value, oneArgumentPrepared.ops)).toEqual(oneArgumentPrepared.value);
oneArgument.adopt(oneArgumentPrepared);
const pastEnd = track({ values: [1, 2, 3] });
const pastEndChange = pastEnd.beginChange();
expect(pastEndChange.state.values.splice(3)).toEqual([]);
const pastEndPrepared = pastEndChange.prepare();
expect(pastEndPrepared.value.values).toEqual([1, 2, 3]);
pastEnd.adopt(pastEndPrepared);
});
it("supports all structural mutators and native return values", () => {
const tracker = track({ values: [3, 1, 2] });
const base = clone(tracker.value);
@ -818,21 +1034,34 @@ describe("astra piece arrays", () => {
});
it("normalizes duplicate entries created by structurally reentrant sort callbacks", () => {
const tracker = track({ values: [{ rank: 2 }, { rank: 1 }] });
const tracker = track({
values: [
{ rank: 2, edited: 0, nested: { value: 0 } },
{ rank: 1, edited: 0, nested: { value: 0 } },
],
});
const base = clone(tracker.value);
const change = tracker.beginChange();
let inserted = false;
change.state.values.sort((left, right) => {
left.edited = left.rank * 10;
left.nested.value = left.rank * 100;
right.edited = right.rank * 10;
right.nested.value = right.rank * 100;
if (!inserted) {
inserted = true;
change.state.values.unshift({ rank: 4 });
change.state.values.unshift({ rank: 4, edited: 0, nested: { value: 0 } });
}
return left.rank - right.rank;
});
const prepared = change.prepare();
expect(prepared.value.values.map((value) => value.rank)).toEqual([1, 2, 1]);
expect(prepared.value.values.map((value) => value.edited)).toEqual([10, 20, 10]);
expect(prepared.value.values.map((value) => value.nested.value)).toEqual([100, 200, 100]);
expect(prepared.value.values[0]).not.toBe(prepared.value.values[2]);
expect(prepared.value.values[0]!.nested).not.toBe(prepared.value.values[2]!.nested);
expect(replay(base, prepared.ops)).toEqual(prepared.value);
expectAliasFree(prepared.value);
tracker.adopt(prepared);
});
@ -1025,7 +1254,7 @@ describe("astra piece arrays", () => {
);
});
describe("astra randomized transactions", () => {
describe("delta tracker randomized transactions", () => {
type Document = {
values: { id: number; score: number }[];
text: string;
@ -1091,7 +1320,8 @@ describe("astra randomized transactions", () => {
const tracker = track(initial);
const expected = clone(initial);
for (let transaction = 0; transaction < 25; transaction++) {
const base = clone(tracker.value);
const baseRoot = tracker.value;
const base = clone(baseRoot);
const change = tracker.beginChange();
for (let operation = 0; operation < 5; operation++) {
const choice = Math.floor(rng() * 11);
@ -1101,16 +1331,43 @@ describe("astra randomized transactions", () => {
}
const prepared = change.prepare();
const policy = clone(prepared.value);
expect(isProxy(prepared.value)).toBe(false);
expect(baseRoot, `prepare changed base seed ${seed} transaction ${transaction}`).toEqual(base);
expect(policy, `policy seed ${seed} transaction ${transaction}`).toEqual(expected);
expect(replay(base, prepared.ops), `replay seed ${seed} transaction ${transaction}`).toEqual(policy);
tracker.adopt(prepared);
expect(baseRoot, `adopt changed base seed ${seed} transaction ${transaction}`).toEqual(base);
expect(tracker.value, `adopt seed ${seed} transaction ${transaction}`).toBe(prepared.value);
expect(tracker.value, `adopt seed ${seed} transaction ${transaction}`).toEqual(policy);
expectAliasFree(tracker.value);
}
}
}, 30_000);
});
describe("astra security and storage-style cloning", () => {
describe("delta tracker object emission scaling", () => {
it("orders many reverse-depth object edits through the bounded fallback", () => {
type DeepNode = { value: number; next?: DeepNode };
let initial: DeepNode = { value: 0 };
for (let depth = 0; depth < 512; depth++) initial = { value: 0, next: initial };
const tracker = track(initial);
const change = tracker.beginChange();
const nodes: Array<Draft<DeepNode>> = [];
let node: Draft<DeepNode> | undefined = change.state;
while (node !== undefined) {
nodes.push(node);
node = node.next;
}
for (let index = nodes.length - 1; index >= 0; index--) nodes[index]!.value = index + 1;
const prepared = change.prepare();
expect(prepared.ops).toHaveLength(nodes.length);
expect(replay(initial, prepared.ops)).toEqual(prepared.value);
tracker.adopt(prepared);
expectAliasFree(tracker.value);
});
});
describe("delta tracker security and storage-style cloning", () => {
it("defines own properties without invoking inherited setters", () => {
const tracker = track({} as Record<string, JsonValue>);
Object.defineProperty(Object.prototype, "trap", {

View file

@ -94,7 +94,9 @@ function sources() {
"../src/delta/tracker.ts",
"../src/delta/draft.ts",
"../src/delta/diff.ts",
"../src/delta/value.ts",
"../src/delta/revision-validator.ts",
"../src/delta/apply-immutable-batch.ts",
"../src/delta/apply-immutable-trusted.ts",
"./delta-traversal.bench.ts",
].map((path) => [
path,

View file

@ -19,10 +19,9 @@ 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).toEqual(input);
expect(tracker.value).not.toBe(input);
expect(Object.isFrozen(tracker.value)).toBe(true);
expect(Object.isFrozen(tracker.value.nested)).toBe(true);
expect(tracker.value).toBe(input);
expect(Object.isFrozen(tracker.value)).toBe(false);
expect(Object.isFrozen(tracker.value.nested)).toBe(false);
const change = tracker.beginChange();
change.state.count = 2;
@ -60,16 +59,14 @@ describe("immutable tracker lifecycle", () => {
next.abort();
});
it("releases the tracker and revokes drafts when preparation fails", () => {
const tracker = track({ values: [1, 2] });
it("grows arrays with explicit nulls and revokes drafts after preparation", () => {
const tracker = track({ values: [1, 2] as Array<number | null> });
const change = tracker.beginChange();
change.state.values.length = 4;
expect(() => change.prepare()).toThrow(/dense|holes/);
const prepared = change.prepare();
expect(prepared.value.values).toEqual([1, 2, null, null]);
expect(() => change.state.values).toThrow(TypeError);
expect(() => change.abort()).not.toThrow();
expect(() => change.prepare()).toThrow(/settled/);
const next = tracker.beginChange();
next.abort();
tracker.adopt(prepared);
});
it("normalizes a deep no-op to exact previous identity", () => {
@ -83,13 +80,12 @@ describe("immutable tracker lifecycle", () => {
expect(tracker.value).toBe(prepared.base);
});
it("copies replacement input and applies the same no-op normalization", () => {
it("takes immutable ownership of replacement input and applies no-op normalization", () => {
const tracker = track({ nested: { value: 1 } });
const replacement = { nested: { value: 2 } };
const prepared = tracker.prepareReplace(replacement);
replacement.nested.value = 9;
expect(prepared.value.nested.value).toBe(2);
expect(Object.isFrozen(prepared.value.nested)).toBe(true);
expect(prepared.value).toBe(replacement);
expect(prepared.ops).toEqual([["r", replacement]]);
tracker.adopt(prepared);
const noOp = tracker.prepareReplace({ nested: { value: 2 } });
@ -97,31 +93,31 @@ describe("immutable tracker lifecycle", () => {
expect(noOp.ops).toEqual([]);
});
it("deeply freezes prepared metadata", () => {
it("shares immutable operation placements with the prepared candidate", () => {
const tracker = track({ rows: [] as { id: number }[] });
const change = tracker.beginChange();
change.state.rows.push({ id: 1 });
const prepared = change.prepare();
expect(Object.isFrozen(prepared)).toBe(true);
expect(Object.isFrozen(prepared.ops)).toBe(true);
const splice = prepared.ops[0]!;
expect(Object.isFrozen(splice)).toBe(true);
if (splice[0] !== "p") throw new Error("expected splice");
expect(Object.isFrozen(splice[1])).toBe(true);
expect(Object.isFrozen(splice[4])).toBe(true);
expect(Object.isFrozen(splice[4][0])).toBe(true);
expect(splice[4][0]).toBe(prepared.value.rows[0]);
expect(Object.isFrozen(prepared)).toBe(false);
expect(Object.isFrozen(prepared.ops)).toBe(false);
});
it("rejects foreign, stale, repeated, and overlapping changes", () => {
it("rejects foreign, stale, and repeated preparations while allowing competing changes", () => {
const first = track({ value: 0 });
const second = track({ value: 0 });
const change = first.beginChange();
expect(() => first.beginChange()).toThrow(/active/);
const competing = first.beginChange();
change.state.value = 1;
competing.state.value = 2;
const prepared = change.prepare();
const competingPrepared = competing.prepare();
expect(() => second.adopt(prepared)).toThrow(/different tracker/);
first.adopt(prepared);
expect(() => first.adopt(prepared)).toThrow(/already been used/);
expect(() => first.adopt(competingPrepared)).toThrow(/stale/);
const stale = first.prepareReplace({ value: 2 });
const winner = first.prepareReplace({ value: 3 });
@ -151,14 +147,14 @@ describe("immutable tracker lifecycle", () => {
expect(() => tracker.adopt(competing)).toThrow(/stale/);
});
it("keeps large prepareReplace array edits narrow", () => {
it("emits an owned root replacement without traversing large replacement input", () => {
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);
const replacement = { rows: tracker.value.rows.slice() };
replacement.rows[5_000] = { value: -1, stable: replacement.rows[5_000]!.stable };
const prepared = tracker.prepareReplace(replacement);
expect(prepared.value).toBe(replacement);
expect(prepared.ops).toEqual([["r", replacement]]);
expect(applyImmutable(prepared.base, prepared.ops)).toEqual(prepared.value);
});
});

View file

@ -99,8 +99,7 @@ describe("remote services", () => {
delivered = value;
deliveries.push(delivery.kind);
});
expect(state.value).toEqual(initial);
expect(state.value).not.toBe(initial);
expect(state.value).toBe(initial);
expect(delivered).toBe(state.value);
const hydrated = delivered;

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { applyImmutable, type Draft, track } from "../src/delta/index.ts";
import { applyImmutable, type Draft, type JsonValue, track } from "../src/delta/index.ts";
describe("copy-on-write change drafts", () => {
describe("transactional overlay drafts", () => {
it("copies only changed branches", () => {
const tracker = track({
changed: { count: 1, sibling: { value: "kept" } },
@ -89,15 +89,28 @@ describe("copy-on-write change drafts", () => {
expect(applyImmutable(prepared.base, prepared.ops)).toEqual(prepared.value);
});
it("rejects invalid writes without mutating the committed base", () => {
const tracker = track({ values: [1, 2] });
let change = tracker.beginChange();
expect(() => delete change.state.values[0]).toThrow(/dense|holes/);
change.abort();
change = tracker.beginChange();
it("rejects non-strict JSON placements without mutating the draft or base", () => {
const tracker = track({ number: 0, payload: null as JsonValue, values: [1, 2] as JsonValue[] });
const change = tracker.beginChange();
expect(() => {
change.state.values[0] = undefined as unknown as number;
}).toThrow(/undefined/);
change.state.values[0] = undefined as unknown as JsonValue;
}).toThrow(/strict JSON/);
expect(() => change.state.values.push({ nested: undefined } as unknown as JsonValue)).toThrow(/strict JSON/);
expect(() => {
change.state.number = Number.NaN;
}).toThrow(/strict JSON/);
expect(() => {
change.state.payload = new Date() as unknown as JsonValue;
}).toThrow(/plain objects/);
expect(change.state).toEqual(tracker.value);
change.abort();
expect(tracker.value).toEqual({ number: 0, payload: null, values: [1, 2] });
});
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(/dense|holes/);
change.abort();
expect(tracker.value.values).toEqual([1, 2]);
});

View file

@ -12,6 +12,22 @@ type MutableDocument = Document | Draft<Document>;
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
function expectAliasFree(value: unknown): void {
const seen = new WeakMap<object, string>();
const visit = (current: unknown, path: string): void => {
if (current === null || typeof current !== "object") return;
const previous = seen.get(current);
if (previous !== undefined) throw new Error(`container at ${path} aliases ${previous}`);
seen.set(current, path);
if (Array.isArray(current)) {
for (let index = 0; index < current.length; index++) visit(current[index], `${path}[${index}]`);
return;
}
for (const key of Object.keys(current)) visit((current as Record<string, unknown>)[key], `${path}.${key}`);
};
visit(value, "$root");
}
const random =
(seed: number): (() => number) =>
() => {
@ -85,16 +101,23 @@ it("converges across randomized prepared revisions", () => {
for (let step = 0; step < 100; step++) {
const choice = Math.floor(rng() * 14);
const value = seed * 1_000 + step;
const baseRoot = tracker.value;
const base = clone(baseRoot);
mutate(expected, choice, value);
const change = tracker.beginChange();
mutate(change.state, choice, value);
const prepared = change.prepare();
const operations: readonly Op[] = prepared.ops;
const operations = clone(prepared.ops) as readonly Op[];
expect(baseRoot, `prepare base seed ${seed} step ${step} choice ${choice}`).toEqual(base);
expect(prepared.base).toBe(baseRoot);
replica = applyImmutable(replica, operations);
expect(replica, `replay seed ${seed} step ${step} choice ${choice}`).toEqual(prepared.value);
tracker.adopt(prepared);
expect(baseRoot, `adopt base seed ${seed} step ${step} choice ${choice}`).toEqual(base);
expect(tracker.value).toBe(prepared.value);
expect(tracker.value, `state seed ${seed} step ${step} choice ${choice}`).toEqual(expected);
expect(replica, `replica seed ${seed} step ${step} choice ${choice}`).toEqual(expected);
expect(Object.isFrozen(tracker.value)).toBe(true);
expectAliasFree(tracker.value);
}
}
});

View file

@ -2,27 +2,27 @@ import { describe, expect, it } from "vitest";
import { track } from "../src/delta/index.ts";
describe("immutable revision ownership", () => {
it("imports detached frozen JSON and expands aliases", () => {
const shared = { value: 1 };
const input = { left: shared, right: shared };
it("takes ownership of an alias-free mutable JSON root without freezing", () => {
const input = { left: { value: 1 }, right: { value: 1 } };
const tracker = track(input);
expect(tracker.value).toEqual(input);
expect(tracker.value).not.toBe(input);
expect(tracker.value).toBe(input);
expect(tracker.value.left).not.toBe(tracker.value.right);
expect(Object.isFrozen(tracker.value)).toBe(true);
expect(Object.isFrozen(tracker.value.left)).toBe(true);
expect(Object.isFrozen(tracker.value)).toBe(false);
expect(Object.isFrozen(tracker.value.left)).toBe(false);
});
it("commits transaction copies in place while sharing unchanged branches", () => {
const tracker = track({ changed: { value: 1 }, retained: { value: 2 } });
const base = tracker.value;
const baseSnapshot = structuredClone(base);
const change = tracker.beginChange();
change.state.changed.value = 3;
const prepared = change.prepare();
expect(prepared.value.changed).not.toBe(base.changed);
expect(prepared.value.retained).toBe(base.retained);
expect(Object.isFrozen(prepared.value.changed)).toBe(true);
expect(Object.isFrozen(prepared.value.changed)).toBe(false);
tracker.adopt(prepared);
expect(base).toEqual(baseSnapshot);
expect(tracker.value).toBe(prepared.value);
});

View file

@ -27,7 +27,7 @@ describe("transactional replicated state", () => {
expect(state.value).not.toBe(previous);
expect(state.value.changed).not.toBe(previous.changed);
expect(state.value.retained).toBe(previous.retained);
expect(Object.isFrozen(state.value)).toBe(true);
expect(Object.isFrozen(state.value)).toBe(false);
expect(deliveries).toEqual([0, 1]);
});
@ -140,25 +140,16 @@ describe("transactional replicated state", () => {
expect(state.value.left).not.toBe(state.value.right);
});
it("expands reused owned subtrees into independent placements", () => {
it("takes immutable ownership of alias-free replacements", () => {
const state = replicatedState({ left: { value: 1 }, right: { value: 2 } });
state.replace(BACKGROUND_CONTEXT, { left: state.value.left, right: state.value.left });
const replacement = { left: { ...state.value.left }, right: { ...state.value.left } };
state.replace(BACKGROUND_CONTEXT, replacement);
expect(state.value).toBe(replacement);
expect(state.value.left).not.toBe(state.value.right);
state.change(BACKGROUND_CONTEXT, (draft) => {
draft.left.value = 9;
});
expect(state.value).toEqual({ left: { value: 9 }, right: { value: 1 } });
const overlapping = replicatedState({ parent: { child: { value: 1 } }, other: { value: 2 } });
overlapping.replace(BACKGROUND_CONTEXT, {
parent: overlapping.value.parent,
other: overlapping.value.parent.child,
});
expect(overlapping.value.parent.child).not.toBe(overlapping.value.other);
overlapping.change(BACKGROUND_CONTEXT, (draft) => {
draft.other.value = 9;
});
expect(overlapping.value).toEqual({ parent: { child: { value: 1 } }, other: { value: 9 } });
});
it("preserves compact string, splice, and permutation operations", () => {
@ -185,6 +176,24 @@ describe("transactional replicated state", () => {
]);
});
it("validates replica revisions without freezing shared immutable payloads", () => {
const replica = new ReplicatedStateReplica<{ rows: { value: number }[] }>(() => {});
const initial = { rows: [{ value: 1 }] };
replica.hydrate(0, [["r", initial]], BACKGROUND_CONTEXT);
expect(replica.value).toBe(initial);
expect(Object.isFrozen(initial)).toBe(false);
expect(Object.isFrozen(initial.rows[0])).toBe(false);
const inserted = { value: 2 };
replica.update(1, [["p", ["rows"], 1, 0, [inserted]]], BACKGROUND_CONTEXT);
expect(replica.value).not.toBe(initial);
expect(replica.value?.rows).toEqual([{ value: 1 }, { value: 2 }]);
expect(replica.value?.rows[0]).toBe(initial.rows[0]);
expect(replica.value?.rows[1]).toBe(inserted);
expect(initial.rows).toHaveLength(1);
expect(Object.isFrozen(inserted)).toBe(false);
});
it("clears a replica when an adopted update is invalid", () => {
const errors: Error[] = [];
const replica = new ReplicatedStateReplica<{ value: number }>((error) => errors.push(error));
@ -207,7 +216,7 @@ describe("transactional replicated state", () => {
expect(state.value).toBe(previous);
state.replace(BACKGROUND_CONTEXT, { ...state.value, value: { nested: 2 } });
expect(state.value).toEqual({ value: { nested: 2 }, retained: { nested: 2 } });
expect(state.value.retained).not.toBe(previous.retained);
expect(state.value.retained).toBe(previous.retained);
});
});

View file

@ -323,7 +323,7 @@ implementation or reproduces the historical graph/mutation comparison.
Targeted tests from `packages/chord`:
```sh
node ../../node_modules/vitest/dist/cli.js --run test/delta.test.ts test/delta-clone.test.ts test/delta-retention.test.ts test/services.test.ts test/service-wire.test.ts test/facets.test.ts
node ../../node_modules/vitest/dist/cli.js --run test/delta.test.ts test/delta-clone.test.ts test/delta-tracker/retention.test.ts test/services.test.ts test/service-wire.test.ts test/facets.test.ts
```
Historical source identifiers: