mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
chord/next/http: a Web fetch handler built from the registry Http (routes, wraps, sockets, hosts, relays, CSP sources) and Authenticators, responses as descriptions, a security baseline on by default, Chord's own Node listener as a host-lifetime object, nodeRequest for apps mounting it, links and plugin sockets. chord/next/workers: a forked worker per address, started on demand, stopped when idle, never restarted by itself, plugins built by the server and shipped. Plugins: kinds with platform and host packages, createPluginRuntime, scripts(kind), rollout results per host, imports of other plugins by name. Kernel: RouteHandle (retry, suspend), no route backoff, anonymous host.root, origins only from relays. ws is an optional peer dependency.
208 lines
8.9 KiB
TypeScript
208 lines
8.9 KiB
TypeScript
import { getEventListeners } from "node:events";
|
|
import { describe, expect, it } from "vitest";
|
|
import { BACKGROUND_CONTEXT as context, withAbortSignal, withCancel } from "../../../src/context/index.ts";
|
|
import { hostKind, int, local, method, remote, str } from "../../../src/next/contracts/index.ts";
|
|
import { type Ctx, createHost, defineFacet, sleep } from "../../../src/next/host/index.ts";
|
|
import { constant } from "../../../src/next/state/index.ts";
|
|
import { expectCollected, flushed, loaded, rejection, type TestPrincipal } from "../support.ts";
|
|
|
|
const Info = local("app.info", { version: 1, params: { id: int() }, members: { id: int(), wait: method() } });
|
|
|
|
/** Serves `Info`; its `wait` waits until the caller's context aborts. */
|
|
function serveInfo(h: Parameters<Parameters<typeof loaded>[0]>[0]): void {
|
|
h.serve(Info, ({ id }) => ({
|
|
id: constant(id),
|
|
wait: async (ctx) => {
|
|
await sleep(60_000, ctx);
|
|
return null;
|
|
},
|
|
}));
|
|
}
|
|
|
|
describe("host.root", () => {
|
|
it("gives fn a call context: the host's principal (or the given one), this host's incarnation, `ctx.get`", async () => {
|
|
const host = createHost<TestPrincipal>({ principal: { kind: "server" }, onError: () => {} });
|
|
await host.load([{ name: "@app/info", facet: defineFacet<TestPrincipal>(serveInfo) }], context);
|
|
const seen = await host.root(async (ctx) => {
|
|
const info = await ctx.get(Info, { id: 7 });
|
|
return { principal: ctx.principal, incarnation: ctx.incarnation, id: info.id.value };
|
|
}, context);
|
|
expect(seen).toMatchObject({ principal: { kind: "server" }, id: 7 });
|
|
const owner = { kind: "owner", access: "local" } as const;
|
|
expect(await host.root(async (ctx) => ctx.principal, context, { principal: owner })).toEqual(owner);
|
|
await host.close(context);
|
|
});
|
|
|
|
it("runs anonymous with an explicit undefined principal; only an omitted one takes the host's (slice 10 §6)", async () => {
|
|
const host = createHost<TestPrincipal>({ principal: { kind: "server" }, onError: () => {} });
|
|
await host.load([], context);
|
|
expect(await host.root(async (ctx) => ctx.principal, context, { principal: undefined })).toBeUndefined();
|
|
expect(await host.root(async (ctx) => ctx.principal, context, {})).toEqual({ kind: "server" });
|
|
await host.close(context);
|
|
});
|
|
|
|
// Opus's build review F5: an anonymous scope's `ctx.get` of a remote type served here failed on a WeakMap key.
|
|
it("refuses an anonymous scope a remote type served here (forbidden); a local one it gets", async () => {
|
|
const Server = hostKind("server");
|
|
const Secrets = remote("app.secrets", {
|
|
version: 1,
|
|
params: {},
|
|
hostAddress: () => Server(),
|
|
variants: { main: { methods: { read: method(undefined, str()) } } },
|
|
});
|
|
const host = createHost<TestPrincipal>({ address: Server(), principal: { kind: "server" }, onError: () => {} });
|
|
const facet = defineFacet<TestPrincipal>((h) => {
|
|
serveInfo(h);
|
|
h.serve(Secrets, () => ({ variant: "main", members: { read: async () => "secret" } }));
|
|
});
|
|
await host.load([{ name: "@app/secrets", facet }], context);
|
|
const anonymous = { principal: undefined };
|
|
const refused = await rejection(host.root(async (ctx) => ctx.get(Secrets, {}), context, anonymous));
|
|
expect(refused).toMatchObject({
|
|
code: "forbidden",
|
|
message: "app.secrets: an anonymous caller gets no remote object",
|
|
});
|
|
expect(await host.root(async (ctx) => (await ctx.get(Info, { id: 3 })).id.value, context, anonymous)).toBe(3);
|
|
await host.close(context);
|
|
});
|
|
|
|
it("seals the context when fn settles: its references released, its signal aborted, `ctx.get` refused `released`", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
let kept: Ctx | undefined;
|
|
const info = await host.root(async (ctx) => {
|
|
kept = ctx;
|
|
return ctx.get(Info, { id: 1 });
|
|
}, context);
|
|
expect(info.state.value).toMatchObject({ kind: "ended", error: { code: "released" } });
|
|
expect(kept!.abortSignal?.aborted).toBe(true);
|
|
expect(await rejection(kept!.get(Info, { id: 1 }))).toMatchObject({ code: "released", reason: "settled" });
|
|
// A throwing fn seals too, and rejects with its error.
|
|
await expect(
|
|
host.root(async () => {
|
|
throw new Error("fn failed");
|
|
}, context),
|
|
).rejects.toThrow("fn failed");
|
|
await host.close(context);
|
|
});
|
|
|
|
it("an abort rejects at once and seals: a running call through its reference rejects, a later get is `released`", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const { context: cancellable, cancel } = withCancel(context);
|
|
let kept: Ctx | undefined;
|
|
let call: Promise<unknown> | undefined;
|
|
const running = host.root(async (ctx) => {
|
|
kept = ctx;
|
|
const info = await ctx.get(Info, { id: 1 });
|
|
call = info.wait(context);
|
|
// Uncooperative: it waits on nothing of Chord's.
|
|
await new Promise(() => {});
|
|
}, cancellable);
|
|
await flushed();
|
|
cancel(new Error("stop"));
|
|
expect(await rejection(running)).toEqual(new Error("stop"));
|
|
expect(await rejection(call!)).toMatchObject({ code: "released" });
|
|
expect(await rejection(kept!.get(Info, { id: 2 }))).toMatchObject({ code: "released", reason: "settled" });
|
|
await host.close(context);
|
|
});
|
|
|
|
it("an aborted context rejects before fn runs; a synchronous throw is a rejection", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const { context: aborted, cancel } = withCancel(context);
|
|
cancel(new Error("gone"));
|
|
let ran = false;
|
|
const fn = async (): Promise<void> => {
|
|
ran = true;
|
|
};
|
|
await expect(host.root(fn, aborted)).rejects.toThrow("gone");
|
|
expect(ran).toBe(false);
|
|
await expect(
|
|
host.root(
|
|
(() => {
|
|
throw new Error("sync");
|
|
}) as never,
|
|
context,
|
|
),
|
|
).rejects.toThrow("sync");
|
|
await host.close(context);
|
|
});
|
|
|
|
it("a `ctx.get` before `load` resolved parks until it did", async () => {
|
|
const host = createHost({ onError: () => {} });
|
|
const result = host.root(async (ctx) => (await ctx.get(Info, { id: 3 })).id.value, context);
|
|
await flushed();
|
|
await host.load([{ name: "@app/info", facet: defineFacet(serveInfo) }], context);
|
|
expect(await result).toBe(3);
|
|
await host.close(context);
|
|
});
|
|
|
|
it("`host.close` seals every open scope (an fn sleeping on its ctx ends); after it, `root` rejects `released`", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const sleeping = rejection(host.root((ctx) => sleep(60_000, ctx), context));
|
|
await flushed();
|
|
await host.close(context);
|
|
expect(await sleeping).toMatchObject({ code: "released", reason: "closed" });
|
|
expect(await rejection(host.root(async () => 1, context))).toMatchObject({ code: "released", reason: "closed" });
|
|
});
|
|
|
|
it("aborts the scope's own signal when fn settles and at `host.close`, the caller's context having one (Fable, Opus, Sol)", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const settled = withCancel(context);
|
|
let kept: Ctx | undefined;
|
|
await host.root(async (ctx) => {
|
|
kept = ctx;
|
|
}, settled.context);
|
|
expect(kept!.abortSignal?.aborted).toBe(true);
|
|
expect(settled.context.abortSignal?.aborted).toBe(false);
|
|
const closing = withCancel(context);
|
|
let slept: "running" | "ended" = "running";
|
|
const sleeping = host.root(async (ctx) => {
|
|
try {
|
|
await sleep(60_000, ctx);
|
|
} finally {
|
|
slept = "ended";
|
|
}
|
|
}, closing.context);
|
|
const outcome = rejection(sleeping);
|
|
await flushed();
|
|
await host.close(context);
|
|
await flushed();
|
|
expect(slept).toBe("ended");
|
|
expect(closing.context.abortSignal?.aborted).toBe(false);
|
|
expect(await outcome).toMatchObject({ code: "released", reason: "closed" });
|
|
});
|
|
|
|
it("`host.close` rejects the caller at once, whatever an uncooperative fn still does (Sol)", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const running = host.root(() => new Promise<never>(() => {}), context);
|
|
const outcome = running.then(
|
|
() => "resolved",
|
|
(error: unknown) => error,
|
|
);
|
|
await host.close(context);
|
|
expect(await outcome).toMatchObject({ code: "released", reason: "closed" });
|
|
});
|
|
|
|
it("leaves no listener on the caller's signal once `host.close` rejected an fn that never settles (Astra)", async () => {
|
|
const controller = new AbortController();
|
|
const host = createHost({ onError: () => {} });
|
|
const running = host.root(async () => new Promise<never>(() => {}), withAbortSignal(controller.signal, context));
|
|
const rejected = running.catch((error: unknown) => error);
|
|
await host.close(context);
|
|
expect(await rejected).toMatchObject({ code: "released", reason: "closed" });
|
|
expect(getEventListeners(controller.signal, "abort")).toHaveLength(0);
|
|
});
|
|
|
|
it("keeps nothing once sealed: its scope and references are collected while the caller's signal lives on", async () => {
|
|
const { host } = await loaded(serveInfo);
|
|
const { context: long } = withCancel(context);
|
|
const refs = await (async () => {
|
|
const made: WeakRef<object>[] = [];
|
|
await host.root(async (ctx) => {
|
|
made.push(new WeakRef(ctx), new WeakRef(await ctx.get(Info, { id: 1 })));
|
|
}, long);
|
|
return made;
|
|
})();
|
|
await expectCollected(...refs);
|
|
await host.close(context);
|
|
});
|
|
});
|