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

233 lines
7.9 KiB
TypeScript

import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import { type Ctx, createHost, type Facet, isChordError, local, method } from "../src/index.ts";
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
async function until(check: () => boolean): Promise<void> {
for (let attempt = 0; attempt < 200 && !check(); attempt++) await tick();
assert.ok(check(), "condition not reached");
}
function latch<T>() {
let open: (value: T) => void = () => {};
const promise = new Promise<T>((resolve) => {
open = resolve;
});
return { promise, open };
}
const Notes = local("t.notes", {
version: 1,
input: Type.Object({}),
members: {
notes: Type.Array(Type.String()),
count: Type.Number(),
has: method(Type.String(), Type.Boolean()),
add: method(Type.String()),
},
});
const NotesSchema = Type.Array(Type.String());
describe("state init", () => {
it("runs after the resources are open and before the object goes live: nothing sees another value", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const loaded = latch<string[]>();
const log: string[] = [];
host.serve(Notes, (_input, _principal, self) => {
const db = self.resource("db", {
version: 1,
open: () => {
log.push("open");
return { load: () => loaded.promise };
},
});
const notes = self.state("notes", NotesSchema, {
init: async () => {
log.push("init");
return await db.value.load();
},
});
const count = self.derive([notes], (list) => list.length);
self.on("notes", notes, (list) => {
log.push(`listener ${list.join(",")}`);
});
self.task(async () => {
log.push(`task ${notes.value.join(",")}`);
});
return {
notes,
count,
has: (note) => notes.value.includes(note),
add: (note, ctx) => {
notes.change(ctx, (draft) => {
draft.push(note);
});
},
};
});
const ref = host.hold(Notes, {});
const seen: unknown[] = [];
ref.notes.subscribe((value) => seen.push(value));
// A call made while init runs waits until the object is live, then reads the loaded value.
const has = host.root((ctx) => ref.has("a", ctx));
await until(() => log.includes("init"));
assert.equal(ref.status, "loading");
loaded.open(["a", "b"]);
assert.equal(await has, true);
assert.equal(ref.count.value, 2);
await until(() => log.length === 4);
assert.deepEqual(log, ["open", "init", "listener a,b", "task a,b"]);
// Before live, a reference's member has no value (undefined); its first value is the loaded one.
assert.deepEqual(
seen.filter((value) => value !== undefined),
[["a", "b"]],
);
});
it("an init cell cannot be read inside the factory", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
host.serve(Notes, (_input, _principal, self) => {
const notes = self.state("notes", NotesSchema, { init: () => [] });
const count = self.derive([notes], (list) => list.length);
notes.value;
return { notes, count, has: () => false, add: () => {} };
});
await assert.rejects(host.hold(Notes, {}).ready, (error) => isChordError(error) && error.code === "not_ready");
});
it("a throwing or invalid init fails the activation and closes its resources; the next get tries again", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
let result: () => unknown = () => {
throw new Error("db down");
};
const closed: string[] = [];
host.serve(Notes, (_input, _principal, self) => {
self.resource("db", { version: 1, open: () => "db", close: (db) => void closed.push(db) });
const notes = self.state("notes", NotesSchema, { init: () => result() as string[] });
const count = self.derive([notes], (list) => list.length);
return { notes, count, has: () => false, add: () => {} };
});
await assert.rejects(host.hold(Notes, {}).ready, /db down/);
assert.deepEqual(closed, ["db"]);
result = () => [1];
await assert.rejects(host.hold(Notes, {}).ready, (error) => isChordError(error) && error.code === "invalid");
assert.deepEqual(closed, ["db", "db"]);
result = () => ["ok"];
const ref = host.hold(Notes, {});
await ref.ready;
assert.deepEqual(ref.notes.value, ["ok"]);
});
it("init runs in a root: it can call other objects", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Source = local("t.source", {
version: 1,
input: Type.Object({}),
members: { all: method(undefined, NotesSchema) },
});
host.serve(Source, () => ({ all: () => ["from source"] }));
host.serve(Notes, (_input, _principal, self) => {
const notes = self.state("notes", NotesSchema, {
init: async (ctx: Ctx) => (await ctx.get(Source, {})).all(ctx),
});
const count = self.derive([notes], (list) => list.length);
return { notes, count, has: () => false, add: () => {} };
});
const ref = host.hold(Notes, {});
await ref.ready;
assert.deepEqual(ref.notes.value, ["from source"]);
});
describe("reload", () => {
const facet = (spec: {
readonly inits: string[];
readonly extra?: () => string[];
readonly countVersion?: number;
}): Facet => ({
name: "notes",
setup: (api) =>
api.serve(Notes, (_input, _principal, self) => {
const notes = self.state("notes", NotesSchema, {
init: () => {
spec.inits.push("notes");
return ["loaded"];
},
});
const counter = self.state("counter", Type.Number(), 0, { version: spec.countVersion ?? 1 });
const extra = spec.extra;
const more =
extra === undefined
? undefined
: self.state("more", NotesSchema, {
init: () => {
spec.inits.push("more");
return extra();
},
});
const count = self.derive(more === undefined ? [notes, counter] : [notes, counter, more], (...lists) =>
lists.reduce<number>((sum, list) => sum + (typeof list === "number" ? list : list.length), 0),
);
return {
notes,
count,
has: (note) => notes.value.includes(note),
add: (note, ctx) => {
notes.change(ctx, (draft) => {
draft.push(note);
});
},
};
}),
});
it("keeps an existing cell without running init; a cell the reload adds runs init while it prepares", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const inits: string[] = [];
await host.install(facet({ inits }));
const ref = host.hold(Notes, {});
await ref.ready;
await host.root((ctx) => ref.add("written", ctx));
await host.reload([facet({ inits, extra: () => ["x", "y"] })]);
assert.deepEqual(inits, ["notes", "more"]);
assert.deepEqual(ref.notes.value, ["loaded", "written"]);
// The new derivation counts the kept cell and the added one: 2 + 0 + 2.
assert.equal(ref.count.value, 4);
});
it("a failing init of an added cell refuses the reload; the old code keeps running", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const inits: string[] = [];
await host.install(facet({ inits }));
const ref = host.hold(Notes, {});
await ref.ready;
await assert.rejects(
host.reload([
facet({
inits,
extra: () => {
throw new Error("cannot load more");
},
}),
]),
/cannot load more/,
);
assert.equal(ref.count.value, 1);
assert.equal(await host.root((ctx) => ref.has("loaded", ctx)), true);
});
it("in a gated reload, an added cell runs init at the gate", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const inits: string[] = [];
await host.install(facet({ inits }));
const ref = host.hold(Notes, {});
await ref.ready;
const report = await host.reload([facet({ inits, countVersion: 2, extra: () => ["x"] })]);
assert.equal(report.gated.length, 1);
assert.deepEqual(inits, ["notes", "more"]);
assert.equal(ref.count.value, 2);
});
});
});