mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
createWorkers bundles the worker entry once (esbuild, Node builtins and installed packages external) and forks every worker from that snapshot, so an edit of the app's source while the server runs reaches workers only through a restart. The kit watches the bundle's inputs as base code. p2 creates its session workers with SessionWorkers.create and keeps the worker base's inputs as a dev restart baseline.
153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
// Test support for the workers component (slice 10 §3, §4): a server routing `Session` to forked fixture workers, the
|
|
// fixture plugins copied into a temporary root, each address's start data (its behaviour) chosen by the test.
|
|
|
|
import { cp, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterEach } from "vitest";
|
|
import { type Context, BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
|
|
import { hostKind, remote, str } from "../../../src/contracts/index.ts";
|
|
import { createHost, type Host, type HostError } from "../../../src/host/index.ts";
|
|
import * as chord from "../../../src/index.ts";
|
|
import type { JsonValue } from "../../../src/json.ts";
|
|
import { createPlugins, type Plugins } from "../../../src/node/plugins/index.ts";
|
|
import { createWorkers, type Workers, type WorkersError } from "../../../src/node/workers/index.ts";
|
|
import { Session, type WorkerBehaviour } from "../fixtures/workers/control.ts";
|
|
|
|
/** The server's own kind. */
|
|
export const Server = hostKind("server");
|
|
|
|
/** The fixture plugin's notes at a worker. */
|
|
export const Notes = remote("test.notes", {
|
|
version: 1,
|
|
params: { id: str() },
|
|
hostAddress: ({ id }) => Session({ id }),
|
|
variants: { main: { state: { label: str() } } },
|
|
});
|
|
|
|
/** The principals of these tests. */
|
|
export type TestPrincipal =
|
|
| { readonly kind: "server" }
|
|
| { readonly kind: "worker"; readonly id: string }
|
|
| { readonly kind: "guest"; readonly id: string };
|
|
|
|
/** The fixture worker's entry. */
|
|
export const ENTRY = fileURLToPath(new URL("../fixtures/workers/worker.ts", import.meta.url));
|
|
|
|
const PLUGINS = fileURLToPath(new URL("../fixtures/workers/plugins/", import.meta.url));
|
|
|
|
const made: { readonly root: string; readonly close: () => Promise<void> }[] = [];
|
|
|
|
afterEach(async () => {
|
|
for (const { root, close } of made.splice(0)) {
|
|
await close();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
/** A server with workers; closed after the test. */
|
|
export type Fleet = {
|
|
readonly server: Host<TestPrincipal>;
|
|
readonly workers: Workers<typeof Session>;
|
|
readonly plugins: Plugins;
|
|
/** The copy of the fixture plugins. */
|
|
readonly root: string;
|
|
readonly errors: WorkersError[];
|
|
readonly hostErrors: HostError[];
|
|
/** The addresses `start` ran for, in order. */
|
|
readonly starts: string[];
|
|
/** The workers' stdout lines, each with its address key. */
|
|
readonly output: string[];
|
|
/** Each address's behaviour (its start data); none: a plain worker. */
|
|
readonly behaviours: Map<string, WorkerBehaviour>;
|
|
/** Writes `label` into the copy's notes plugin. */
|
|
label(label: string): Promise<void>;
|
|
};
|
|
|
|
/** Starts a server routing `Session` to fixture workers; `options` override the workers' defaults. */
|
|
export async function fleet(
|
|
options: {
|
|
readonly entry?: string;
|
|
readonly idleMs?: number;
|
|
readonly heartbeat?: { readonly ping?: number; readonly close?: number };
|
|
readonly start?: (address: { readonly key: string }, context: Context) => Promise<JsonValue>;
|
|
readonly principal?: () => TestPrincipal;
|
|
readonly authorize?: () => boolean;
|
|
} = {},
|
|
): Promise<Fleet> {
|
|
const root = await mkdtemp(join(tmpdir(), "chord-workers-"));
|
|
await cp(PLUGINS, root, { recursive: true });
|
|
const errors: WorkersError[] = [];
|
|
const hostErrors: HostError[] = [];
|
|
const starts: string[] = [];
|
|
const behaviours = new Map<string, WorkerBehaviour>();
|
|
const output: string[] = [];
|
|
const modules = { "@earendil-works/chord": chord };
|
|
const plugins = createPlugins(
|
|
{ roots: [root], kinds: { worker: { platform: "node", hostPackages: Object.keys(modules) } }, onError: () => {} },
|
|
context,
|
|
);
|
|
const server = createHost<TestPrincipal>({
|
|
address: Server(),
|
|
principal: { kind: "server" },
|
|
onError: (error) => hostErrors.push(error),
|
|
...(options.heartbeat === undefined ? {} : { heartbeat: options.heartbeat }),
|
|
});
|
|
const workers = await createWorkers(
|
|
server,
|
|
Session,
|
|
{
|
|
kind: "worker",
|
|
entry: options.entry ?? ENTRY,
|
|
execArgv: ["--conditions=source", "--experimental-strip-types", "--no-warnings"],
|
|
authorize: options.authorize ?? (() => true),
|
|
principal: options.principal ?? ((address) => ({ kind: "worker", id: address.params.id }) as const),
|
|
start: async (address, startContext) => {
|
|
starts.push(address.key);
|
|
if (options.start !== undefined) return options.start(address, startContext);
|
|
return (behaviours.get(address.params.id) ?? {}) as JsonValue;
|
|
},
|
|
lock: (address) => `chord-test:${root}:${address.key}`,
|
|
idleMs: () => options.idleMs ?? 60_000,
|
|
plugins,
|
|
onOutput: (address, stream, chunk) => {
|
|
if (stream === "stdout") output.push(`${address.key} ${Buffer.from(chunk).toString().trim()}`);
|
|
},
|
|
onError: (error) => errors.push(error),
|
|
},
|
|
context,
|
|
);
|
|
await server.load([], context);
|
|
// The worker kind's set made now (discovery, builds): the watcher marks edits from then on.
|
|
await plugins.scripts("worker", context);
|
|
const close = async () => {
|
|
await workers.close(context);
|
|
await server.close(context);
|
|
plugins.close();
|
|
};
|
|
made.push({ root, close });
|
|
return {
|
|
server,
|
|
workers,
|
|
plugins,
|
|
root,
|
|
errors,
|
|
hostErrors,
|
|
starts,
|
|
output,
|
|
behaviours,
|
|
label: (label) => writeFile(join(root, "notes", "label.ts"), `export const label = ${JSON.stringify(label)};\n`),
|
|
};
|
|
}
|
|
|
|
/** Waits until `ready()` holds, polling every 10 ms, for at most `ms`. */
|
|
export async function until(ready: () => boolean, ms = 10_000): Promise<void> {
|
|
const deadline = Date.now() + ms;
|
|
while (!ready()) {
|
|
if (Date.now() > deadline) throw new Error("until: timed out");
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
|
|
export { context, Session };
|