chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
@@ -0,0 +1,351 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { BACKGROUND_CONTEXT } from "../src/context/index.ts";
|
||||
import type { Draft, Op } from "../src/delta/index.ts";
|
||||
import {
|
||||
type Context,
|
||||
type ReplicatedStateSource,
|
||||
type ReplicatedStateSourceAttachment,
|
||||
type ReplicatedStateSourceFrame,
|
||||
replicatedState,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("transactional replicated state", () => {
|
||||
it("publishes one immutable structurally shared revision", () => {
|
||||
const initial = { changed: { value: 1 }, retained: { value: 2 } };
|
||||
const state = replicatedState(initial);
|
||||
const previous = state.value;
|
||||
const deliveries: number[] = [];
|
||||
state.subscribe((_value, _context, delivery) => deliveries.push(delivery.sequence));
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.changed.value = 3;
|
||||
draft.changed.value = 4;
|
||||
});
|
||||
expect(state.value).toEqual({ changed: { value: 4 }, retained: { value: 2 } });
|
||||
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(false);
|
||||
expect(deliveries).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("rolls back callback failures and revokes escaped drafts", () => {
|
||||
const state = replicatedState({ nested: { value: 1 } });
|
||||
const previous = state.value;
|
||||
let escaped: Draft<{ value: number }> | undefined;
|
||||
expect(() =>
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
escaped = draft.nested;
|
||||
draft.nested.value = 2;
|
||||
throw new Error("stop");
|
||||
}),
|
||||
).toThrow("stop");
|
||||
expect(state.value).toBe(previous);
|
||||
expect(() => escaped?.value).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("rejects PromiseLike callbacks and aborts their draft", () => {
|
||||
const state = replicatedState({ value: 0 });
|
||||
const previous = state.value;
|
||||
expect(() =>
|
||||
state.change(BACKGROUND_CONTEXT, ((draft: Draft<{ value: number }>) => {
|
||||
draft.value = 1;
|
||||
return Promise.resolve();
|
||||
}) as (draft: Draft<{ value: number }>) => void),
|
||||
).toThrow(/synchronous/);
|
||||
expect(state.value).toBe(previous);
|
||||
});
|
||||
|
||||
it("rejects nested changes and replacements without losing the outer rollback", () => {
|
||||
const state = replicatedState({ left: 0, right: 0 });
|
||||
expect(() =>
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.left = 1;
|
||||
state.change(BACKGROUND_CONTEXT, (nested) => {
|
||||
nested.right = 2;
|
||||
});
|
||||
}),
|
||||
).toThrow(/reentrantly/);
|
||||
expect(state.value).toEqual({ left: 0, right: 0 });
|
||||
expect(() =>
|
||||
state.change(BACKGROUND_CONTEXT, () => {
|
||||
state.replace(BACKGROUND_CONTEXT, { left: 1, right: 2 });
|
||||
}),
|
||||
).toThrow(/change callback/);
|
||||
expect(state.value).toEqual({ left: 0, right: 0 });
|
||||
});
|
||||
|
||||
it("queues listener-triggered changes in sequence order", () => {
|
||||
const state = replicatedState({ value: 0 });
|
||||
const deliveries: Array<{ value: number; sequence: number }> = [];
|
||||
const lateDeliveries: Array<{ kind: string; sequence: number }> = [];
|
||||
let nested = false;
|
||||
state.subscribe((_value, _context, delivery) => {
|
||||
if (delivery.kind !== "update" || nested) return;
|
||||
nested = true;
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.value = 2;
|
||||
});
|
||||
state.subscribe((_value, _context, delivery) => lateDeliveries.push(delivery));
|
||||
});
|
||||
state.subscribe((value, _context, delivery) => {
|
||||
if (delivery.kind === "update") deliveries.push({ value: value.value, sequence: delivery.sequence });
|
||||
});
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.value = 1;
|
||||
});
|
||||
expect(deliveries).toEqual([
|
||||
{ value: 1, sequence: 1 },
|
||||
{ value: 2, sequence: 2 },
|
||||
]);
|
||||
expect(lateDeliveries).toEqual([{ kind: "hydrate", sequence: 2 }]);
|
||||
});
|
||||
|
||||
it("isolates listener failures after committing the revision", () => {
|
||||
const reports: (() => void)[] = [];
|
||||
const spy = vi.spyOn(globalThis, "queueMicrotask").mockImplementation((report) => {
|
||||
reports.push(report);
|
||||
});
|
||||
try {
|
||||
const state = replicatedState({ value: 0 });
|
||||
const received: number[] = [];
|
||||
state.subscribe((_value, _context, delivery) => {
|
||||
if (delivery.kind === "update") throw new Error("listener failed");
|
||||
});
|
||||
state.subscribe((_value, _context, delivery) => received.push(delivery.sequence));
|
||||
expect(() =>
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.value = 1;
|
||||
}),
|
||||
).not.toThrow();
|
||||
expect(state.value).toEqual({ value: 1 });
|
||||
expect(received).toEqual([0, 1]);
|
||||
expect(reports).toHaveLength(1);
|
||||
expect(reports[0]).toThrow("listener failed");
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("copies assigned values by value", () => {
|
||||
const external = { value: 1 };
|
||||
const state = replicatedState<{ left: { value: number } | null; right: { value: number } | null }>({
|
||||
left: null,
|
||||
right: null,
|
||||
});
|
||||
state.change(BACKGROUND_CONTEXT, (draft) => {
|
||||
draft.left = external;
|
||||
draft.right = external;
|
||||
draft.left.value = 2;
|
||||
});
|
||||
expect(external.value).toBe(1);
|
||||
expect(state.value).toEqual({ left: { value: 2 }, right: { value: 1 } });
|
||||
expect(state.value.left).not.toBe(state.value.right);
|
||||
});
|
||||
|
||||
it("takes immutable ownership of alias-free replacements", () => {
|
||||
const state = replicatedState({ left: { value: 1 }, right: { value: 2 } });
|
||||
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 } });
|
||||
});
|
||||
|
||||
it("replaces atomically and ignores deeply equal replacements", () => {
|
||||
const state = replicatedState({ value: { nested: 1 }, retained: { nested: 2 } });
|
||||
const previous = state.value;
|
||||
state.replace(BACKGROUND_CONTEXT, { value: { nested: 1 }, retained: { nested: 2 } });
|
||||
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).toBe(previous.retained);
|
||||
});
|
||||
});
|
||||
|
||||
type SourceValue = Readonly<{ value: number }>;
|
||||
|
||||
class TestSource<T> implements ReplicatedStateSource<T> {
|
||||
readonly attachments = new Set<TestSourceAttachment<T>>();
|
||||
onAttach: (() => void) | undefined;
|
||||
#value: T;
|
||||
#cursor: number;
|
||||
|
||||
constructor(value: T, cursor = 0) {
|
||||
this.#value = value;
|
||||
this.#cursor = cursor;
|
||||
}
|
||||
|
||||
attach(): ReplicatedStateSourceAttachment<T> {
|
||||
const attachment = new TestSourceAttachment(Object.freeze({ value: this.#value, cursor: this.#cursor }), () =>
|
||||
this.attachments.delete(attachment),
|
||||
);
|
||||
this.attachments.add(attachment);
|
||||
this.onAttach?.();
|
||||
return attachment;
|
||||
}
|
||||
|
||||
commit(value: T, ops: readonly Op[], context: Context = BACKGROUND_CONTEXT, cursor = this.#cursor + 1): void {
|
||||
this.#value = value;
|
||||
this.#cursor = cursor;
|
||||
const frame = Object.freeze({ cursor, value, ops, context });
|
||||
for (const attachment of [...this.attachments]) attachment.publish(frame);
|
||||
}
|
||||
}
|
||||
|
||||
class TestSourceAttachment<T> implements ReplicatedStateSourceAttachment<T> {
|
||||
readonly snapshot: { readonly value: T; readonly cursor: number };
|
||||
readonly #onDispose: () => void;
|
||||
readonly #buffer: ReplicatedStateSourceFrame<T>[] = [];
|
||||
#listener: ((frame: ReplicatedStateSourceFrame<T>) => void) | undefined;
|
||||
#activated = false;
|
||||
#disposed = false;
|
||||
|
||||
constructor(snapshot: { readonly value: T; readonly cursor: number }, onDispose: () => void) {
|
||||
this.snapshot = snapshot;
|
||||
this.#onDispose = onDispose;
|
||||
}
|
||||
|
||||
activate(listener: (frame: ReplicatedStateSourceFrame<T>) => void): void {
|
||||
if (this.#activated) throw new Error("attachment is already active");
|
||||
this.#activated = true;
|
||||
this.#listener = listener;
|
||||
for (const frame of this.#buffer.splice(0)) listener(frame);
|
||||
}
|
||||
|
||||
publish(frame: ReplicatedStateSourceFrame<T>): void {
|
||||
if (this.#disposed) return;
|
||||
if (this.#listener === undefined) this.#buffer.push(frame);
|
||||
else this.#listener(frame);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.#disposed) return;
|
||||
this.#disposed = true;
|
||||
this.#buffer.length = 0;
|
||||
this.#onDispose();
|
||||
}
|
||||
}
|
||||
|
||||
describe("authoritative replicated state sources", () => {
|
||||
it("captures before activation and drains queued commits in order", () => {
|
||||
const initial = Object.freeze({ value: 0 });
|
||||
const first = Object.freeze({ value: 1 });
|
||||
const second = Object.freeze({ value: 2 });
|
||||
const firstOps = Object.freeze([["s", ["value"], 1] as const]);
|
||||
const secondOps = Object.freeze([["s", ["value"], 2] as const]);
|
||||
const source = new TestSource<SourceValue>(initial, 10);
|
||||
source.onAttach = () => {
|
||||
source.commit(first, firstOps);
|
||||
source.commit(second, secondOps);
|
||||
};
|
||||
|
||||
const state = replicatedState(source);
|
||||
const deliveries: Array<{ value: SourceValue; sequence: number }> = [];
|
||||
state.subscribe((value, _context, delivery) => deliveries.push({ value, sequence: delivery.sequence }));
|
||||
expect(state.value).toBe(second);
|
||||
expect(deliveries).toEqual([{ value: second, sequence: 2 }]);
|
||||
});
|
||||
|
||||
it("hydrates at sequence zero when attaching after existing commits", () => {
|
||||
const initial = Object.freeze({ value: 0 });
|
||||
const current = Object.freeze({ value: 1 });
|
||||
const source = new TestSource<SourceValue>(initial, 40);
|
||||
source.commit(current, Object.freeze([["s", ["value"], 1] as const]));
|
||||
const state = replicatedState(source);
|
||||
const deliveries: number[] = [];
|
||||
state.subscribe((_value, _context, delivery) => deliveries.push(delivery.sequence));
|
||||
expect(state.value).toBe(current);
|
||||
expect(deliveries).toEqual([0]);
|
||||
});
|
||||
|
||||
it("publishes exact source values without applying or re-diffing", () => {
|
||||
const source = new TestSource<SourceValue>(Object.freeze({ value: 0 }));
|
||||
const state = replicatedState(source);
|
||||
const next = Object.freeze({ value: 1 });
|
||||
// Deliberately not the operation that produces `next`: Chord must publish the source value as is.
|
||||
const ops = Object.freeze([["s", ["value"], 7] as const]);
|
||||
let publishedValue: SourceValue | undefined;
|
||||
state.subscribe((value, _context, delivery) => {
|
||||
if (delivery.kind === "update") publishedValue = value;
|
||||
});
|
||||
|
||||
source.commit(next, ops);
|
||||
expect(state.value).toBe(next);
|
||||
expect(publishedValue).toBe(next);
|
||||
});
|
||||
|
||||
it("buffers reentrant frames and skips updates covered by a late hydration", () => {
|
||||
const source = new TestSource<SourceValue>(Object.freeze({ value: 0 }));
|
||||
const state = replicatedState(source);
|
||||
const received: number[] = [];
|
||||
const late: Array<{ kind: string; sequence: number; value: number }> = [];
|
||||
let nested = false;
|
||||
state.subscribe((_value, _context, delivery) => {
|
||||
if (delivery.kind !== "update" || delivery.sequence !== 1 || nested) return;
|
||||
nested = true;
|
||||
source.commit(Object.freeze({ value: 2 }), Object.freeze([["s", ["value"], 2] as const]));
|
||||
state.subscribe((value, _context, delivery) => late.push({ ...delivery, value: value.value }));
|
||||
});
|
||||
state.subscribe((value, _context, delivery) => {
|
||||
if (delivery.kind === "update") received.push(value.value);
|
||||
});
|
||||
|
||||
source.commit(Object.freeze({ value: 1 }), Object.freeze([["s", ["value"], 1] as const]));
|
||||
expect(received).toEqual([1, 2]);
|
||||
expect(late).toEqual([{ kind: "hydrate", sequence: 2, value: 2 }]);
|
||||
});
|
||||
|
||||
it("reports listener failures without throwing them into the source", () => {
|
||||
const source = new TestSource<SourceValue>(Object.freeze({ value: 0 }));
|
||||
const errors: Error[] = [];
|
||||
const state = replicatedState(source, { onError: (error) => errors.push(error) });
|
||||
const received: number[] = [];
|
||||
state.subscribe((_value, _context, delivery) => {
|
||||
if (delivery.kind === "update") throw new Error("listener failed");
|
||||
});
|
||||
state.subscribe((value, _context, delivery) => {
|
||||
if (delivery.kind === "update") received.push(value.value);
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
source.commit(Object.freeze({ value: 1 }), Object.freeze([["s", ["value"], 1] as const])),
|
||||
).not.toThrow();
|
||||
source.commit(Object.freeze({ value: 2 }), Object.freeze([["s", ["value"], 2] as const]));
|
||||
expect(received).toEqual([1, 2]);
|
||||
expect(errors.map((error) => error.message)).toEqual(["listener failed", "listener failed"]);
|
||||
});
|
||||
|
||||
it("reports cursor gaps, disposes the broken attachment, and ignores later frames", () => {
|
||||
const source = new TestSource<SourceValue>(Object.freeze({ value: 0 }), 5);
|
||||
const errors: Error[] = [];
|
||||
const state = replicatedState(source, { onError: (error) => errors.push(error) });
|
||||
source.commit(Object.freeze({ value: 2 }), Object.freeze([["s", ["value"], 2] as const]), BACKGROUND_CONTEXT, 7);
|
||||
expect(errors[0]?.message).toContain("expected 6, received 7");
|
||||
expect(source.attachments.size).toBe(0);
|
||||
expect(state.value).toEqual({ value: 0 });
|
||||
source.commit(Object.freeze({ value: 3 }), Object.freeze([["s", ["value"], 3] as const]), BACKGROUND_CONTEXT, 8);
|
||||
expect(state.value).toEqual({ value: 0 });
|
||||
});
|
||||
|
||||
it("keeps attachments independent and disposes each idempotently", () => {
|
||||
const source = new TestSource<SourceValue>(Object.freeze({ value: 0 }));
|
||||
const first = replicatedState(source);
|
||||
const second = replicatedState(source);
|
||||
expect(source.attachments.size).toBe(2);
|
||||
source.commit(Object.freeze({ value: 1 }), Object.freeze([["s", ["value"], 1] as const]));
|
||||
expect(first.value.value).toBe(1);
|
||||
expect(second.value.value).toBe(1);
|
||||
|
||||
first.dispose();
|
||||
first.dispose();
|
||||
expect(source.attachments.size).toBe(1);
|
||||
source.commit(Object.freeze({ value: 2 }), Object.freeze([["s", ["value"], 2] as const]));
|
||||
expect(first.value.value).toBe(1);
|
||||
expect(second.value.value).toBe(2);
|
||||
second.dispose();
|
||||
expect(source.attachments.size).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user