Files
dev/third_party/pi/packages/chord/test/state.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

261 lines
8.6 KiB
TypeScript

import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import { applyImmutable } from "../src/delta/index.ts";
import { type Ctx, createHost, isChordError, local, method, type StateEvent } from "../src/index.ts";
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
function emitter<E>() {
const listeners = new Set<(event: E) => void>();
return {
emit: (event: E) => {
for (const fn of listeners) fn(event);
},
subscribe: (fn: (event: E) => void) => {
listeners.add(fn);
return () => listeners.delete(fn);
},
get size() {
return listeners.size;
},
};
}
const Item = Type.Object({ id: Type.Number(), label: Type.String() });
const Items = Type.Object({ items: Type.Array(Item), count: Type.Number() });
const List = local("t.list", {
version: 1,
input: Type.Object({}),
members: {
list: Items,
labels: Type.Array(Type.String()),
push: method(Item),
set: method(Items),
},
});
function serveList(host: ReturnType<typeof createHost>) {
host.serve(List, (_input, _principal, self) => {
const listState = self.state("list", Items, { items: [], count: 0 });
const labels = self.derive([listState], (list) => list.items.map((item) => item.label));
return {
list: listState,
labels,
push: (item: { id: number; label: string }, ctx: Ctx) => {
listState.change(ctx, (draft) => {
draft.items.push(item);
draft.count += 1;
});
},
set: (value: { items: { id: number; label: string }[]; count: number }, ctx: Ctx) => {
listState.replace(ctx, value);
},
};
});
}
describe("state cells", () => {
it("publishes snapshots then exact ops; derivations follow", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
serveList(host);
const ref = host.hold(List, {});
const events: StateEvent<unknown>[] = [];
const labels: StateEvent<unknown>[] = [];
ref.list.events((event) => events.push(event));
ref.labels.events((event) => labels.push(event));
await ref.push({ id: 1, label: "a" });
// Subscribed while loading: the snapshot once live, then exact ops.
assert.deepEqual(events[0], { type: "snap", value: { items: [], count: 0 } });
const ops = events[1]?.type === "ops" ? events[1].ops.map((op) => JSON.stringify(op)).sort() : [];
assert.deepEqual(ops, ['["p",["items"],0,0,[{"id":1,"label":"a"}]]', '["s",["count"],1]']);
assert.deepEqual(events[1]?.value, { items: [{ id: 1, label: "a" }], count: 1 });
assert.deepEqual(ref.labels.value, ["a"]);
assert.deepEqual(
labels.map((event) => event.type),
["snap", "ops"],
);
await ref.set({ items: [], count: 0 });
assert.deepEqual(ref.labels.value, []);
});
it("supports primitive cells and value-returning changes", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Counter = local("t.prim", {
version: 1,
input: Type.Object({}),
members: { n: Type.Number(), inc: method(undefined, Type.Number()) },
});
host.serve(Counter, (_input, _principal, self) => {
const nState = self.state("n", Type.Number(), 0);
return {
n: nState,
inc: (ctx: Ctx) => {
nState.change(ctx, (n) => n + 1);
return nState.value;
},
};
});
const ref = host.hold(Counter, {});
const values: unknown[] = [];
await ref.ready;
const events: StateEvent<unknown>[] = [];
ref.n.events((event) => events.push(event));
ref.n.subscribe((value) => values.push(value));
assert.equal(await ref.inc(), 1);
assert.equal(await ref.inc(), 2);
assert.deepEqual(values, [0, 1, 2]);
assert.deepEqual(events, [
{ type: "snap", value: 0 },
{ type: "ops", ops: [["r", 1]], value: 1 },
{ type: "ops", ops: [["r", 2]], value: 2 },
]);
});
it("rejects a returned value that does not fit the schema, even outside dev mode", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Box = local("t.box", {
version: 1,
input: Type.Object({}),
members: { put: method() },
});
host.serve(Box, (_input, _principal, self) => {
const list = self.state("list", Type.Array(Type.Number()), []);
return {
put: (ctx: Ctx) => {
// The classic slip: an expression-bodied arrow returns push()'s length.
list.change(ctx, (draft) => draft.push(1) as never);
},
};
});
await assert.rejects(host.hold(Box, {}).put(), (error) => isChordError(error, "invalid"));
});
it("validates initial values always and writes only in dev mode", async () => {
const Bad = local("t.bad-initial", { version: 1, input: Type.Object({}) });
const host = createHost({ self: { kind: "host" }, onError: () => {} });
host.serve(Bad, (_input, _principal, self) => {
self.state("x", Type.Object({ n: Type.Number() }), { n: "no" } as never);
return {};
});
await assert.rejects(host.hold(Bad, {}).ready, /initial value of state x/);
const lax = createHost({ self: { kind: "host" }, onError: () => {} });
serveList(lax);
await lax.hold(List, {}).set({ items: [], count: "x" } as never);
const dev = createHost({ self: { kind: "host" }, dev: true, onError: () => {} });
serveList(dev);
const ref = dev.hold(List, {});
await assert.rejects(
ref.set({ items: [], count: "x" } as never),
(error) => isChordError(error, "invalid") && error.reason === "state",
);
assert.deepEqual(ref.list.value, { items: [], count: 0 });
});
it("rejects writes from inside a factory and placeholder use before activation", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Writer = local("t.writer", { version: 1, input: Type.Object({}) });
let captured: Ctx | undefined;
await host.root((ctx) => {
captured = ctx;
});
host.serve(Writer, (_input, _principal, self) => {
const x = self.state("x", Type.Object({}), {});
const r = self.resource("r", { version: 1, open: () => 1 });
assert.throws(
() => r.value,
(error) => isChordError(error, "not_ready"),
);
x.replace(captured!, {});
return {};
});
await assert.rejects(host.hold(Writer, {}).ready, /read_only/);
});
it("listeners run each event in a fresh root; tasks get an abortable ctx", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const events = emitter<number>();
const Watcher = local("t.watcher", {
version: 1,
input: Type.Object({}),
members: { seen: Type.Object({ sum: Type.Number() }) },
});
const roots = new Set<string>();
let taskAborted = false;
host.serve(Watcher, (_input, _principal, self) => {
const seen = self.state("seen", Type.Object({ sum: Type.Number() }), { sum: 0 });
const sourceResource = self.resource("source", { version: 1, open: () => events });
self.on(
"onEvent",
sourceResource,
(source) => source,
(value, ctx) => {
roots.add(ctx.rootId);
seen.change(ctx, (draft) => {
draft.sum += value;
});
},
);
self.task(async (ctx) => {
await new Promise<void>((resolve) => ctx.signal.addEventListener("abort", () => resolve()));
taskAborted = true;
});
return { seen };
});
const ref = host.hold(Watcher, {});
await ref.ready;
events.emit(2);
events.emit(3);
await tick();
assert.deepEqual(ref.seen.value, { sum: 5 });
assert.equal(roots.size, 2);
ref.release();
await tick();
await tick();
assert.equal(events.size, 0);
assert.ok(taskAborted);
});
it("re-entrant writes publish in adoption order; derivations never go stale (Sol #3)", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Echo = local("t.reentrant", {
version: 1,
input: Type.Object({}),
members: { n: Type.Number(), d: Type.Number(), set: method(Type.Number()) },
});
host.serve(Echo, (_input, _principal, self) => {
const nState = self.state("n", Type.Number(), 0);
const d = self.derive([nState], (n) => n * 2);
// Subscribed first: re-enters the write while revision 1 is being published.
self.on("bump", nState, (n, ctx) => {
if (n === 1) nState.replace(ctx, 2);
});
return {
n: nState,
d,
set: (value: number, ctx: Ctx) => {
nState.replace(ctx, value);
},
};
});
const ref = host.hold(Echo, {});
await ref.ready;
const values: unknown[] = [];
let replica: unknown;
ref.n.events((event) => {
replica = event.type === "ops" ? applyImmutable(replica, event.ops) : event.value;
values.push(replica);
});
const doubled: unknown[] = [];
ref.d.subscribe((value) => doubled.push(value));
await ref.set(1);
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(values, [0, 1, 2]);
assert.equal(ref.d.value, 4);
assert.deepEqual(doubled, [0, 2, 4]);
});
});