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

302 lines
10 KiB
TypeScript

// Link flow control: state waits while the peer's acknowledgements lag a window behind, coalesced per subscription;
// everything else is sent at once.
import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import {
type Ctx,
createHost,
type Frame,
type Host,
hostNamed,
isChordError,
type Link,
memoryLinkPair,
method,
PROTOCOL_VERSION,
remote,
} from "../src/index.ts";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function until(check: () => boolean, attempts = 200): Promise<void> {
for (let i = 0; i < attempts && !check(); i++) await sleep(2);
assert.ok(check(), "condition not reached");
}
/** A client's end of a link that can hold back its acknowledgements, and records what it receives. */
class AckGate implements Link {
readonly inner: Link;
readonly received: Frame[] = [];
paused = false;
#held: Frame | undefined;
constructor(inner: Link) {
this.inner = inner;
}
get closed(): boolean {
return this.inner.closed;
}
send(frame: Frame): number | undefined {
if (frame.t === "ack" && this.paused) {
this.#held = frame;
return 0;
}
return this.inner.send(frame);
}
release(): void {
this.paused = false;
if (this.#held !== undefined) this.inner.send(this.#held);
this.#held = undefined;
}
receive(handler: (frame: Frame) => void): void {
this.inner.receive((frame) => {
this.received.push(frame);
handler(frame);
});
}
onClose(handler: (reason: string) => void): void {
this.inner.onClose(handler);
}
close(reason?: string): void {
this.inner.close(reason);
}
}
/** A counter on the server, or (`t.flow-worker-counter`) on the worker host `w`. */
const counterOn = (name: string, host: string) =>
remote(name, {
version: 1,
input: Type.Object({}),
host: hostNamed(host),
variants: {
main: {
state: { count: Type.Object({ n: Type.Number() }) },
methods: { add: method(Type.Number(), Type.Number()), wait: method() },
},
},
});
const Counter = counterOn("t.flow-counter", "server");
const WorkerCounter = counterOn("t.flow-worker-counter", "w");
let release = () => {};
const waiting = () =>
new Promise<void>((resolve) => {
const previous = release;
release = () => {
previous();
resolve();
};
});
function serveCounter(host: Host, type = Counter): void {
host.serve(type, (_input, _principal, self) => {
const count = self.state("count", Type.Object({ n: Type.Number() }), { n: 0 });
return {
variant: "main",
count,
add: (delta: number, ctx: Ctx) => {
count.change(ctx, (draft) => {
draft.n += delta;
});
return count.value.n;
},
wait: waiting,
};
});
}
/** A client of `server` (accepted with `window`) whose acknowledgements can be held back. */
function client(server: Host, window: number) {
const [down, up] = memoryLinkPair();
server.accept(down, { principal: { user: "u" }, window });
const gate = new AckGate(up);
const host = createHost({
self: { kind: "host" },
name: "browser",
kind: "client",
upstream: { link: gate, principal: { kind: "server" } },
onError: () => {},
});
return { host, gate };
}
const stateFrames = (frames: readonly Frame[]) =>
frames.filter((frame): frame is Extract<Frame, { t: "state" }> => frame.t === "state");
describe("link flow control", () => {
it("holds state back while the window is full, answers calls meanwhile, and sends one coalesced frame", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
serveCounter(server);
const { host, gate } = client(server, 200);
const counter = host.hold(Counter, {});
const values: unknown[] = [];
counter.count.subscribe((value) => values.push(value));
await until(() => values.at(-1) !== undefined);
gate.paused = true;
const results: number[] = [];
for (let i = 0; i < 40; i++) results.push((await counter.add(1)) as number);
// Every call was answered, though most state is still waiting for an acknowledgement.
assert.equal(results.at(-1), 40);
assert.ok((values.at(-1) as { n: number }).n < 40, "state waits behind the window");
const before = stateFrames(gate.received).length;
gate.release();
await until(() => (values.at(-1) as { n: number }).n === 40);
assert.equal(stateFrames(gate.received).length - before, 1, "the waiting ops arrive as one frame");
await host.close();
await server.close();
});
it("replaces ops that outgrow their slot with the current value", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
serveCounter(server);
const { host, gate } = client(server, 64);
const counter = host.hold(Counter, {});
const values: unknown[] = [];
counter.count.subscribe((value) => values.push(value));
await until(() => values.at(-1) !== undefined);
gate.paused = true;
const before = gate.received.length;
for (let i = 0; i < 100; i++) await counter.add(1);
gate.release();
await until(() => (values.at(-1) as { n: number }).n === 100);
const sent = stateFrames(gate.received.slice(before));
assert.ok(
sent.some((frame) => frame.as === "reset"),
"a reset replaced the ops",
);
assert.ok(sent.length < 10, `few state frames, not one per change (${sent.length})`);
await host.close();
await server.close();
});
it("drops what waits for a reference that is released", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
serveCounter(server);
const { host, gate } = client(server, 64);
const counter = host.hold(Counter, {});
await counter.ready;
const other = host.hold(Counter, {});
await other.ready;
gate.paused = true;
for (let i = 0; i < 10; i++) await counter.add(1);
counter.release();
other.release();
const before = gate.received.length;
gate.release();
await sleep(10);
assert.deepEqual(stateFrames(gate.received.slice(before)), []);
await host.close();
await server.close();
});
it("coalesces relayed state for the origin and asks downstream for a reset when it overflows", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
const toWorker: Frame[] = [];
server.route("w", {
principal: (name) => ({ host: name }),
authorize: () => true,
resolve: async (name) => {
const [down, up] = memoryLinkPair();
const worker = createHost({
self: { kind: "host" },
name,
upstream: { link: up, principal: { kind: "server" } },
onError: () => {},
});
serveCounter(worker, WorkerCounter);
return {
get closed() {
return down.closed;
},
send: (frame: Frame) => {
toWorker.push(frame);
return down.send(frame);
},
receive: (handler) => down.receive(handler),
onClose: (handler) => down.onClose(handler),
close: (reason) => down.close(reason),
} satisfies Link;
},
});
const { host, gate } = client(server, 64);
const counter = host.hold(WorkerCounter, {});
const values: unknown[] = [];
counter.count.subscribe((value) => values.push(value));
await until(() => values.at(-1) !== undefined);
const resyncs = () => toWorker.filter((frame) => frame.t === "resync").length;
assert.equal(resyncs(), 0);
gate.paused = true;
for (let i = 0; i < 100; i++) await counter.add(1);
// An overflow drops the waiting ops and asks the worker for a reset (which then waits in the slot).
assert.ok(resyncs() >= 1, "the overflowing stream was resynced downstream");
assert.ok(!toWorker.some((frame) => frame.t === "sub" || frame.t === "unsub"));
gate.release();
await until(() => (values.at(-1) as { n: number }).n === 100);
await host.close();
await server.close();
});
it("neither windows nor acknowledges a peer whose hello does not announce acks", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
serveCounter(server);
const [down, raw] = memoryLinkPair();
server.accept(down, { principal: { user: "u" }, window: 1 });
const frames: Frame[] = [];
raw.receive((frame) => frames.push(frame));
raw.send({ t: "hello", protocol: PROTOCOL_VERSION, name: "raw", kind: "client", codeVersion: 0 });
raw.send({ t: "hold", id: 1, at: "server", token: Counter.description, input: {} });
for (let i = 0; i < 5; i++) raw.send({ t: "call", id: 3 + i, hold: 1, member: "add", args: 1 });
await until(() => frames.filter((frame) => frame.t === "result").length === 5);
await sleep(5);
assert.equal(stateFrames(frames).length, 6, "the snapshot and one frame per change");
assert.ok(!frames.some((frame) => frame.t === "ack"));
assert.equal((frames[0] as Extract<Frame, { t: "hello" }>).acks, true);
await server.close();
});
it("closes the link on an acknowledgement of frames never sent", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
const [down, raw] = memoryLinkPair();
server.accept(down, { principal: { user: "u" } });
raw.receive(() => {});
raw.send({ t: "hello", name: "raw", kind: "client", codeVersion: 0, acks: true });
raw.send({ t: "ack", n: 99 });
await until(() => raw.closed);
await server.close();
});
});
describe("link call limit", () => {
it("answers calls beyond maxCalls at once with too_many_calls and keeps the link", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
serveCounter(server);
const [down, up] = memoryLinkPair();
server.accept(down, { principal: { user: "u" }, maxCalls: 2 });
const host = createHost({
self: { kind: "host" },
name: "browser",
kind: "client",
upstream: { link: up, principal: { kind: "server" } },
onError: () => {},
});
const counter = host.hold(Counter, {});
await counter.ready;
release = () => {};
const first = counter.wait();
const second = counter.wait();
await assert.rejects(
counter.add(1),
(error) => isChordError(error, "unavailable") && error.reason === "too_many_calls",
);
release();
await Promise.all([first, second]);
assert.equal(await counter.add(1), 1);
await host.close();
await server.close();
});
});