Files
dev/third_party/pi/packages/chord/test/replicated-state-delivery.test.ts
alexion 6081e3e325 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
2026-10-15 17:58:48 -04:00

249 lines
8.7 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import { BACKGROUND_CONTEXT, withCancel } from "../src/context/index.ts";
import {
type Context,
type ReplicatedState,
type ReplicatedStateDelivery,
type ReplicatedStateSourceFrame,
replicatedState,
} from "../src/index.ts";
type Value = { value: number };
type Kind = "mutable" | "attached";
function deferred() {
let resolve!: () => void;
let reject!: (error: Error) => void;
const promise = new Promise<void>((accept, fail) => {
resolve = accept;
reject = fail;
});
return { promise, resolve, reject };
}
function fixture(kind: Kind, onError: (error: Error) => void = () => {}) {
let state: ReplicatedState<Value>;
let publish: (value: Value, context: Context) => void;
if (kind === "mutable") {
const mutable = replicatedState({ value: 0 });
state = mutable;
publish = (value, context) => mutable.replace(context, value);
} else {
let receive!: (frame: ReplicatedStateSourceFrame<Value>) => void;
state = replicatedState<Value>(
{
attach: () => ({
snapshot: { value: { value: 0 }, cursor: 0 },
activate: (listener) => {
receive = listener;
},
dispose() {},
}),
},
{ onError },
);
publish = (value, context) =>
receive({ value, cursor: value.value, ops: [["s", ["value"], value.value]], context });
}
return { state, publish };
}
describe.each<Kind>(["mutable", "attached"])("%s public delivery", (kind) => {
test("awaits hydration and each update independently of other subscribers", async () => {
const { state, publish } = fixture(kind);
const hydration = deferred();
const update = deferred();
const events: string[] = [];
const fast: number[] = [];
state.subscribe(async (value) => {
events.push(`start:${value.value}`);
if (value.value === 0) await hydration.promise;
if (value.value === 1) await update.promise;
events.push(`end:${value.value}`);
});
state.subscribe((value) => fast.push(value.value));
publish({ value: 1 }, BACKGROUND_CONTEXT);
publish({ value: 2 }, BACKGROUND_CONTEXT);
expect(events).toEqual(["start:0"]);
expect(fast).toEqual([0, 1, 2]);
expect(state.value).toEqual({ value: 2 });
hydration.resolve();
await vi.waitFor(() => expect(events).toEqual(["start:0", "end:0", "start:1"]));
update.resolve();
await vi.waitFor(() => expect(events).toEqual(["start:0", "end:0", "start:1", "end:1", "start:2", "end:2"]));
});
test.each([100, 101, 102, 201, 202])("bounds %i pending deliveries", async (count) => {
const { state, publish } = fixture(kind);
const hydration = deferred();
const received: number[] = [];
state.subscribe((value) => {
received.push(value.value);
if (value.value === 0) return hydration.promise;
});
for (let value = 1; value <= count; value += 1) publish({ value }, BACKGROUND_CONTEXT);
expect(received).toEqual([0]);
hydration.resolve();
await hydration.promise;
const first = Math.floor((count - 1) / 100) * 100 + 1;
expect(received).toEqual([0, ...Array.from({ length: count - first + 1 }, (_, index) => first + index)]);
});
test("excludes a running update from overflow and retains exact value/context/delivery", async () => {
const { state, publish } = fixture(kind);
const gate = deferred();
const context = withCancel(BACKGROUND_CONTEXT).context;
const received: { value: Value; context: Context; delivery: ReplicatedStateDelivery }[] = [];
state.subscribe((value, context, delivery) => {
received.push({ value, context, delivery });
if (value.value === 1) return gate.promise;
});
for (let value = 1; value < 102; value += 1) publish({ value }, BACKGROUND_CONTEXT);
const newest = { value: 102 };
publish(newest, context);
const adopted = state.value;
publish({ value: 103 }, BACKGROUND_CONTEXT);
expect(received.map(({ value }) => value.value)).toEqual([0, 1]);
gate.resolve();
await gate.promise;
expect(received.map(({ value }) => value.value)).toEqual([0, 1, 102, 103]);
expect(received[2]?.value).toBe(adopted);
expect(received[2]?.context).toBe(context);
expect(received[2]?.delivery).toEqual({ kind: "update", sequence: 102 });
});
test("serializes reentrant hydration and update callbacks", () => {
const { state, publish } = fixture(kind);
const events: string[] = [];
state.subscribe((value) => {
events.push(`start:${value.value}`);
if (value.value < 2) publish({ value: value.value + 1 }, BACKGROUND_CONTEXT);
events.push(`end:${value.value}`);
});
expect(events).toEqual(["start:0", "end:0", "start:1", "end:1", "start:2", "end:2"]);
});
test("treats two subscriptions of the same callback independently", async () => {
const { state, publish } = fixture(kind);
const gate = deferred();
const received: number[] = [];
const listener = (value: Value) => {
received.push(value.value);
if (value.value === 0) return gate.promise;
};
const stopFirst = state.subscribe(listener);
const stopSecond = state.subscribe(listener);
publish({ value: 1 }, BACKGROUND_CONTEXT);
stopFirst();
stopFirst();
gate.resolve();
await gate.promise;
expect(received).toEqual([0, 0, 1]);
stopSecond();
});
test("unsubscribe drops queued callbacks without joining or aborting the running callback", async () => {
const { state, publish } = fixture(kind);
const gate = deferred();
const { context } = withCancel(BACKGROUND_CONTEXT);
const received: number[] = [];
let completed = false;
const stop = state.subscribe(async (value) => {
received.push(value.value);
if (value.value === 1) {
await gate.promise;
completed = true;
}
});
await Promise.resolve();
publish({ value: 1 }, context);
publish({ value: 2 }, context);
expect(stop()).toBeUndefined();
publish({ value: 3 }, context);
expect(context.abortSignal?.aborted).toBe(false);
expect(completed).toBe(false);
gate.resolve();
await vi.waitFor(() => expect(completed).toBe(true));
expect(received).toEqual([0, 1]);
});
});
describe("attached listener errors", () => {
const kind: Kind = "attached";
test("isolates a synchronous hydration failure without removing the subscription", () => {
const errors: Error[] = [];
const { state, publish } = fixture(kind, (error) => errors.push(error));
const received: number[] = [];
const failure = new Error("sync hydration");
expect(() =>
state.subscribe((value) => {
received.push(value.value);
if (value.value === 0) throw failure;
}),
).not.toThrow();
publish({ value: 1 }, BACKGROUND_CONTEXT);
expect(errors).toEqual([failure]);
expect(received).toEqual([0, 1]);
});
test("observes hydration rejection, synchronous throw, and update rejection while continuing delivery", async () => {
const errors: Error[] = [];
const { state, publish } = fixture(kind, (error) => errors.push(error));
const gate = deferred();
const received: number[] = [];
const fast: number[] = [];
state.subscribe((value) => {
received.push(value.value);
if (value.value === 0) return gate.promise;
if (value.value === 1) throw new Error("sync update");
if (value.value === 2) return Promise.reject(new Error("async update"));
});
state.subscribe((value) => fast.push(value.value));
for (let value = 1; value <= 3; value += 1) publish({ value }, BACKGROUND_CONTEXT);
gate.reject(new Error("async hydration"));
await vi.waitFor(() => expect(received).toEqual([0, 1, 2, 3]));
expect(errors.map((error) => error.message)).toEqual(["async hydration", "sync update", "async update"]);
expect(fast).toEqual([0, 1, 2, 3]);
});
test("still observes a callback rejection after unsubscribe", async () => {
const errors: Error[] = [];
const { state, publish } = fixture(kind, (error) => errors.push(error));
const gate = deferred();
const received: number[] = [];
const stop = state.subscribe((value) => {
received.push(value.value);
return gate.promise;
});
publish({ value: 1 }, BACKGROUND_CONTEXT);
stop();
const failure = new Error("stopped callback");
gate.reject(failure);
await vi.waitFor(() => expect(errors).toEqual([failure]));
expect(received).toEqual([0]);
});
});
test("mutable state reports rejected callbacks through its default error reporter", async () => {
const reports: (() => void)[] = [];
const spy = vi.spyOn(globalThis, "queueMicrotask").mockImplementation((report) => {
reports.push(report);
});
try {
const state = replicatedState({ value: 0 });
const failure = new Error("async mutable hydration");
const received: number[] = [];
state.subscribe((value) => {
received.push(value.value);
if (value.value === 0) return Promise.reject(failure);
});
expect(() => state.replace(BACKGROUND_CONTEXT, { value: 1 })).not.toThrow();
await Promise.resolve();
expect(received).toEqual([0, 1]);
expect(reports).toHaveLength(1);
expect(reports[0]).toThrow(failure);
} finally {
spy.mockRestore();
}
});