babavoss 0.0.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
@@ -0,0 +1,85 @@
1
+ // The services bridge: turns a context's calls into `service` messages and
2
+ // resolves their `serviced` answers. Shared by the bundled harness and the
3
+ // in-process worker.
4
+ import type { Context, Services, Who } from "../kernel/context.ts";
5
+ import { ServiceError } from "../kernel/context.ts";
6
+ import { isStream, type Serviced, type ServiceName, type ServiceRequest, type Cancel, type Credit } from "../transport/messages.ts";
7
+ import type { Port } from "../ecs/runtime.ts";
8
+
9
+ const errorOf = (m: Serviced) => new ServiceError(m.code === "denied" || m.code === "notfound" || m.code === "args" ? m.code : "failed", m.error ?? `${m.code}`);
10
+
11
+ /**
12
+ * The bridge: a request once, or a stream many times, each with the job's
13
+ * signal. Aborted, a request's promise rejects and a stream's iterator ends,
14
+ * and the kernel is told to let go, whether or not it ever answers again.
15
+ */
16
+ export function makeBridge(post: (m: ServiceRequest | Cancel | Credit) => void): { bridge: Services; answer(m: Serviced): void } {
17
+ const waiting = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
18
+ const streams = new Map<number, { push: (v: unknown[]) => void; end: (e?: Error) => void }>();
19
+ let next = 1;
20
+ const request = (who: Who, service: ServiceName, op: string, args: unknown[], signal?: AbortSignal) => new Promise<unknown>((resolve, reject) => {
21
+ const id = next++;
22
+ if (signal?.aborted) { reject(new ServiceError("failed", `${service}.${op}: cancelled`)); return; }
23
+ const cancel = () => { if (!waiting.has(id)) return; waiting.delete(id); post({ type: "cancel", id }); reject(new ServiceError("failed", `${service}.${op}: cancelled`)); };
24
+ signal?.addEventListener("abort", cancel, { once: true });
25
+ waiting.set(id, { resolve: (v) => { signal?.removeEventListener("abort", cancel); resolve(v); }, reject: (e) => { signal?.removeEventListener("abort", cancel); reject(e); } });
26
+ post({ type: "service", id, system: who.system, effect: who.effect, job: who.job, entity: who.entity, service, op, args });
27
+ });
28
+ const stream = (who: Who, service: ServiceName, op: string, args: unknown[], signal?: AbortSignal): AsyncIterable<unknown> => ({
29
+ // Lazy: constructing an unused iterator never allocates an OS resource.
30
+ async *[Symbol.asyncIterator]() {
31
+ if (signal?.aborted) throw new ServiceError("failed", `${service}.${op}: cancelled`);
32
+ const id = next++;
33
+ const queue: unknown[] = [];
34
+ let done = false, error: Error | null = null;
35
+ let wake: (() => void) | null = null;
36
+ const finish = (e?: Error) => { if (done) return; done = true; if (e) error = e; streams.delete(id); signal?.removeEventListener("abort", cancel); wake?.(); };
37
+ const cancel = () => { if (done) return; post({ type: "cancel", id }); queue.length = 0; finish(); };
38
+ streams.set(id, { push: (vs) => { queue.push(...vs); wake?.(); }, end: finish });
39
+ signal?.addEventListener("abort", cancel, { once: true });
40
+ post({ type: "service", id, system: who.system, effect: who.effect, job: who.job, entity: who.entity, service, op, args });
41
+ try {
42
+ for (;;) {
43
+ if (queue.length) {
44
+ yield queue.shift();
45
+ if (!done && service === "exec") post({ type: "credit", id });
46
+ continue;
47
+ }
48
+ if (done) { if (error) throw error; return; }
49
+ await new Promise<void>((r) => { wake = r; }); wake = null;
50
+ }
51
+ } finally { cancel(); }
52
+ },
53
+ });
54
+ const ask = (who: Who, service: ServiceName, op: string, args: unknown[], signal?: AbortSignal) => isStream(service, op) ? stream(who, service, op, args, signal) : request(who, service, op, args, signal);
55
+ return {
56
+ bridge: {
57
+ fs: (who, op, args, signal) => ask(who, "fs", op, args, signal),
58
+ http: (who, op, args, signal) => ask(who, "http", op, args, signal),
59
+ exec: (who, op, args, signal) => ask(who, "exec", op, args, signal),
60
+ secrets: (who, op, args, signal) => ask(who, "secrets", op, args, signal),
61
+ },
62
+ answer(m) {
63
+ const s = streams.get(m.id);
64
+ if (s) { if (m.more) s.push(m.values ?? []); else s.end(m.ok ? undefined : errorOf(m)); return; }
65
+ const w = waiting.get(m.id);
66
+ if (!w) return;
67
+ waiting.delete(m.id);
68
+ if (m.ok) w.resolve(m.value);
69
+ else w.reject(errorOf(m));
70
+ },
71
+ };
72
+ }
73
+
74
+ /** The port a runtime gives its jobs: a context per run, every service a message with who asked, carrying the job's signal. */
75
+ export function portOver(bridge: Services, project: string, env: Record<string, string>): Port {
76
+ const frozen = Object.freeze({ ...env });
77
+ return (system, effect, job, signal, entity) => {
78
+ const who: Who = { project, system, effect, job, entity };
79
+ const service = <S extends keyof Services>(name: S) => new Proxy({}, {
80
+ get: (_, op: string) => (...args: unknown[]) => bridge[name](who, op as never, args, signal),
81
+ });
82
+ const ctx: Context = { project, fs: service("fs") as Context["fs"], http: service("http") as Context["http"], exec: service("exec") as Context["exec"], secrets: service("secrets") as Context["secrets"], env: frozen, signal };
83
+ return ctx;
84
+ };
85
+ }
@@ -0,0 +1,54 @@
1
+ // A Node's transport to a runtime process: the same program in the
2
+ // `--runtime` role, spoken to as the worker is. `load-dir` becomes a
3
+ // bundle of the project, its .env files and a `load`. The rest passes
4
+ // through. The spawn hook is where the integrator puts the wall.
5
+ import { join } from "node:path";
6
+ import { framework } from "../kernel/where.ts";
7
+ import { ProcessTransport, type Transport } from "../transport/transport.ts";
8
+ import type { ToRuntime, FromRuntime } from "../transport/messages.ts";
9
+ import type { Snapshot } from "../ecs/state.ts";
10
+ import type { ToWorker, FromWorker } from "../baba/worker.ts";
11
+ import type { Connect } from "../baba/node.ts";
12
+ import { openProject, type Project } from "../baba/project.ts";
13
+ import { InProcessBuilder, type Builder } from "../build/builder.ts";
14
+ import { readEnv } from "./env.ts";
15
+
16
+ export interface SpawnOptions { cwd: string; env: Record<string, string>; stdout: "inherit" | "pipe" | "ignore"; stderr: "inherit" | "pipe" | "ignore" }
17
+ export type Spawn = (cmd: string[], options: SpawnOptions) => Transport<ToRuntime, FromRuntime>;
18
+
19
+ /** The command that starts voss in a role: Bun on the framework's entry. */
20
+ export function roleCommand(role: "--runtime" | "--builder" | "--server" | "--exec" | "--exec-watch"): string[] {
21
+ return [process.execPath, join(framework, "bin/voss.ts"), role];
22
+ }
23
+ export const runtimeCommand = () => roleCommand("--runtime");
24
+
25
+ export const spawnProcess: Spawn = (cmd, options) => new ProcessTransport<ToRuntime, FromRuntime>(cmd, options);
26
+
27
+ /**
28
+ * `project` names the directory; each load reads it again, so the entry
29
+ * follows the files. The bundle comes from `builder`, in-process unless
30
+ * the kernel gives its walled one.
31
+ */
32
+ export function runtimeConnect(project: Project, spawn: Spawn = spawnProcess, builder: Builder = new InProcessBuilder()): Connect {
33
+ return () => {
34
+ const inner = spawn(runtimeCommand(), { cwd: "/", env: {}, stdout: "inherit", stderr: "inherit" });
35
+ let handler: ((m: FromWorker) => void) | null = null;
36
+ inner.onMessage((m) => handler?.(m));
37
+ const load = async (dir: string, snapshot: Snapshot | null, prepare?: boolean) => {
38
+ try {
39
+ const p = await openProject(dir);
40
+ const code = await builder.project(p);
41
+ const env = await readEnv(p.baba);
42
+ inner.send({ type: "load", project: { name: p.name, dir: p.dir, baba: p.baba }, code, env, snapshot, prepare });
43
+ } catch (err) {
44
+ handler?.({ type: "failed", error: err instanceof Error ? err.message : String(err) });
45
+ }
46
+ };
47
+ return {
48
+ send(m: ToWorker) { if (m.type === "load-dir") void load(m.dir ?? project.dir, m.snapshot, m.prepare); else inner.send(m); },
49
+ onMessage(h) { handler = h; },
50
+ onClose(h) { inner.onClose(h); },
51
+ close() { inner.close(); },
52
+ };
53
+ };
54
+ }
@@ -0,0 +1,35 @@
1
+ // The baba's environment: .env and .env.local under .baba, the local
2
+ // file laid over the other. KEY=VALUE lines, an optional `export `,
3
+ // single or double quotes, and # comments.
4
+ import { join } from "node:path";
5
+
6
+ export function parseEnv(text: string): Record<string, string> {
7
+ const out: Record<string, string> = {};
8
+ for (const raw of text.split(/\r?\n/)) {
9
+ const line = raw.trim();
10
+ if (!line || line.startsWith("#")) continue;
11
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
12
+ if (!m) continue;
13
+ let value = m[2]!;
14
+ const q = value[0];
15
+ if ((q === '"' || q === "'") && value.lastIndexOf(q) > 0) {
16
+ value = value.slice(1, value.lastIndexOf(q));
17
+ if (q === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
18
+ } else {
19
+ const hash = value.indexOf(" #");
20
+ if (hash >= 0) value = value.slice(0, hash);
21
+ value = value.trim();
22
+ }
23
+ out[m[1]!] = value;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ export async function readEnv(baba: string): Promise<Record<string, string>> {
29
+ const env: Record<string, string> = {};
30
+ for (const name of [".env", ".env.local"]) {
31
+ const f = Bun.file(join(baba, name));
32
+ if (await f.exists()) Object.assign(env, parseEnv(await f.text()));
33
+ }
34
+ return env;
35
+ }
@@ -0,0 +1,80 @@
1
+ // What both workers share: a baba, loaded, answering the kernel's messages.
2
+ // The runtime owns the rounds; this turns fire, read, state, raw and retire
3
+ // into its calls, and every round into a `round` the kernel may keep.
4
+ import type { ToRuntime, FromRuntime, Result, ResultCode } from "../transport/messages.ts";
5
+ import { manifestOf, type Baba } from "../ecs/baba.ts";
6
+ import { Runtime, ContractError } from "../ecs/runtime.ts";
7
+ import { StateError, type Snapshot } from "../ecs/state.ts";
8
+ import { makeBridge, portOver } from "./bridge.ts";
9
+ import { withPromptware } from "../promptware/system.ts";
10
+
11
+ /** How long a `state` read waits for the round past `since` before answering the state as it is. */
12
+ export const STATE_WAIT_MS = 25_000;
13
+
14
+ export class Harness {
15
+ private rt: Runtime | null = null;
16
+ private activate: ((snapshot: Snapshot | null) => void) | null = null;
17
+ private readonly bridge: ReturnType<typeof makeBridge>;
18
+
19
+ constructor(private post: (m: FromRuntime) => void) {
20
+ this.bridge = makeBridge(post);
21
+ }
22
+
23
+ /** Loads the baba over the snapshot given; answers `loaded`, or throws for `failed`. */
24
+ load(own: Baba, project: { name: string; dir: string; baba: string }, env: Record<string, string>, snapshot: Snapshot | null, prepare = false): void {
25
+ // Voss's own systems join every baba after its own.
26
+ const v = withPromptware(own);
27
+ const manifest = manifestOf(v);
28
+ const make = (snapshot: Snapshot | null, auto: boolean) => new Runtime(v, {
29
+ auto,
30
+ port: portOver(this.bridge.bridge, project.dir, env),
31
+ snapshot: snapshot ?? undefined,
32
+ onRound: (state) => this.post({ type: "round", state, frame: this.rt!.frame(state.round - 1) }),
33
+ log: (line) => console.error(`${project.name}: ${line}`),
34
+ });
35
+ this.rt?.stop();
36
+ this.rt = make(snapshot, !prepare);
37
+ this.activate = prepare ? (latest) => { this.rt?.stop(); this.rt = make(latest, true); this.post({ type: "loaded", manifest, snapshot: this.rt.snapshot() }); } : null;
38
+ this.post({ type: "loaded", manifest, snapshot: this.rt.snapshot() });
39
+ }
40
+
41
+ async handle(m: ToRuntime): Promise<void> {
42
+ if (m.type === "hello" || m.type === "load") return;
43
+ if (m.type === "activate") {
44
+ const go = this.activate; this.activate = null;
45
+ try { if (!go) throw new Error("generation is not prepared"); go(m.snapshot); }
46
+ catch (e) { this.post({ type: "failed", error: e instanceof Error ? e.message : String(e) }); }
47
+ return;
48
+ }
49
+ if (m.type === "serviced") { this.bridge.answer(m); return; }
50
+ if (m.type === "retire") {
51
+ // Check and stop in the same turn: no new job can start in between.
52
+ if (m.unlessBusy && this.rt && [...this.rt.w.jobs.values()].some((j) => j.status === "running" && !j.effect.repeat)) {
53
+ this.post({ type: "retired", errors: [], snapshot: this.rt.snapshot(), busy: true }); return;
54
+ }
55
+ this.activate = null;
56
+ const snapshot = this.rt?.stop() ?? { round: 0, resources: {}, entities: {}, jobs: [] };
57
+ this.rt = null;
58
+ this.post({ type: "retired", errors: [], snapshot });
59
+ return;
60
+ }
61
+ const rt = this.rt;
62
+ if (!rt) { this.post({ type: "result", id: m.id, ok: false, code: "down", error: "not loaded" }); return; }
63
+ try {
64
+ let value: unknown;
65
+ if (m.type === "fire") value = await rt.fire(m.name, m.args, m.coerce, m.key);
66
+ else if (m.type === "read") value = rt.read(m.name, m.args, m.coerce);
67
+ else if (m.type === "state") { if (m.parts) value = rt.parts(m.parts); else { if (m.wait !== false) await rt.waitRound(m.since, STATE_WAIT_MS); value = m.frame ? rt.frame(m.since) : rt.state(m.since, { redacted: true }); } }
68
+ else value = await rt.raw(m.raw);
69
+ // A fire or a raw write resolves inside its round, so the round is the one it ran in: what a window waits for.
70
+ this.post({ type: "result", id: m.id, ok: true, value, ...(m.type === "fire" || m.type === "raw" ? { round: rt.round } : {}) });
71
+ } catch (err) {
72
+ this.post(failure(m.id, err));
73
+ }
74
+ }
75
+ }
76
+
77
+ export function failure(id: number, err: unknown): Result {
78
+ const code: ResultCode = err instanceof ContractError ? err.code : err instanceof StateError ? (err.code === "value" ? "args" : err.code) : "failed";
79
+ return { type: "result", id, ok: false, code, error: err instanceof Error ? err.message : String(err) };
80
+ }
@@ -0,0 +1,119 @@
1
+ // The runtime role: one process per baba, walled, speaking IPC to the
2
+ // kernel. The main thread owns the channel; the baba's bundle runs as a
3
+ // Worker made from the text the kernel sends. Requests go to the worker and
4
+ // results come back; the worker's service requests go to the kernel and
5
+ // its answers back. A new load makes a new worker and retires the old one
6
+ // once the new is live; a load that fails leaves the old one serving.
7
+ import { ParentTransport } from "../transport/transport.ts";
8
+ import { PROTOCOL, type ToRuntime, type FromRuntime, type Load } from "../transport/messages.ts";
9
+ import type { FromHarness, ToHarness } from "./worker.ts";
10
+
11
+ interface Live { worker: Worker; url: string; loaded: boolean }
12
+
13
+ const parent = new ParentTransport<FromRuntime, ToRuntime>();
14
+ let live: Live | null = null;
15
+ let loading: Promise<void> = Promise.resolve();
16
+ // Service requests by the id the kernel sees; the worker's own id is smaller in scope. Both ways, so a cancel finds its request.
17
+ const asked = new Map<number, { live: Live; id: number }>();
18
+ const kernelIds = new Map<Live, Map<number, number>>();
19
+ let nextAsk = 1;
20
+
21
+ parent.onMessage((m) => {
22
+ switch (m.type) {
23
+ case "hello": return;
24
+ case "load": loading = loading.then(() => load(m)); return;
25
+ case "activate":
26
+ if (!live) parent.send({ type: "failed", error: "no prepared generation" });
27
+ else live.worker.postMessage(m satisfies ToHarness);
28
+ return;
29
+ case "fire": case "read": case "state": case "raw":
30
+ if (!live) return parent.send({ type: "result", id: m.id, ok: false, code: "down", error: "no baba loaded" });
31
+ live.worker.postMessage(m satisfies ToHarness);
32
+ return;
33
+ case "serviced": {
34
+ const a = asked.get(m.id);
35
+ if (!a) return;
36
+ if (!m.more) { asked.delete(m.id); kernelIds.get(a.live)?.delete(a.id); }
37
+ a.live.worker.postMessage({ ...m, id: a.id } satisfies ToHarness);
38
+ return;
39
+ }
40
+ case "retire": {
41
+ const l = live;
42
+ if (!l) return parent.send({ type: "retired", errors: [], snapshot: { round: 0, resources: {}, entities: {}, jobs: [] } });
43
+ l.worker.postMessage(m satisfies ToHarness);
44
+ // The worker answers `retired`, forwarded by fromWorker, then is dropped.
45
+ return;
46
+ }
47
+ }
48
+ });
49
+ parent.send({ type: "hello", protocol: PROTOCOL, role: "runtime" });
50
+
51
+ async function load(m: Load): Promise<void> {
52
+ const url = URL.createObjectURL(new Blob([m.code], { type: "application/javascript" }));
53
+ const fresh: Live = { worker: new Worker(url), url, loaded: false };
54
+ try {
55
+ const loaded = await new Promise<Extract<FromHarness, { type: "loaded" }>>((resolve, reject) => {
56
+ fresh.worker.onmessage = (e: MessageEvent<FromHarness>) => {
57
+ const h = e.data;
58
+ if (h.type === "loaded") resolve(h);
59
+ else if (h.type === "failed") reject(new Error(h.error));
60
+ else fromWorker(fresh, h); // a job asked for at load reaches the kernel before the load is reported
61
+ };
62
+ fresh.worker.onerror = (e) => reject(new Error(e.message));
63
+ fresh.worker.postMessage({ type: "init", project: m.project, env: m.env, snapshot: m.snapshot, prepare: m.prepare } satisfies ToHarness);
64
+ });
65
+ fresh.worker.onmessage = (e: MessageEvent<FromHarness>) => fromWorker(fresh, e.data);
66
+ fresh.worker.onerror = (e) => died(fresh, e.message);
67
+ fresh.loaded = true;
68
+ const old = live;
69
+ live = fresh;
70
+ if (old) drop(old);
71
+ parent.send(loaded);
72
+ } catch (err) {
73
+ drop(fresh);
74
+ parent.send({ type: "failed", error: err instanceof Error ? err.message : String(err) });
75
+ }
76
+ }
77
+
78
+ function fromWorker(from: Live, h: FromHarness) {
79
+ if (h.type === "service") {
80
+ const id = nextAsk++;
81
+ asked.set(id, { live: from, id: h.id });
82
+ if (!kernelIds.has(from)) kernelIds.set(from, new Map());
83
+ kernelIds.get(from)!.set(h.id, id);
84
+ parent.send({ ...h, id });
85
+ } else if (h.type === "credit") {
86
+ const id = kernelIds.get(from)?.get(h.id);
87
+ if (id !== undefined) parent.send({ type: "credit", id });
88
+ } else if (h.type === "cancel") {
89
+ const id = kernelIds.get(from)?.get(h.id);
90
+ if (id === undefined) return;
91
+ kernelIds.get(from)!.delete(h.id);
92
+ asked.delete(id);
93
+ parent.send({ type: "cancel", id });
94
+ } else if (h.type === "retired") {
95
+ parent.send(h);
96
+ if (!h.busy) { if (live === from) live = null; drop(from); }
97
+ } else if (h.type === "failed") {
98
+ if (from === live) parent.send(h);
99
+ } else if (h.type === "loaded") {
100
+ if (from === live) parent.send(h);
101
+ } else if (h.type === "result" || h.type === "round") {
102
+ if (from === live || h.type === "result") parent.send(h);
103
+ }
104
+ }
105
+
106
+ /** The worker crashed: its service answers are unwanted; the kernel hears nothing more from it. */
107
+ function died(from: Live, why: string) {
108
+ console.error(`the baba's worker died: ${why}`);
109
+ if (live === from) { live = null; parent.send({ type: "failed", error: `runtime worker died: ${why}` }); }
110
+ drop(from);
111
+ }
112
+
113
+ /** Ends a generation's worker; whatever it still asked the kernel for is cancelled. */
114
+ function drop(l: Live) {
115
+ for (const [id, a] of [...asked]) if (a.live === l) { asked.delete(id); parent.send({ type: "cancel", id }); }
116
+ kernelIds.delete(l);
117
+ l.worker.terminate();
118
+ URL.revokeObjectURL(l.url);
119
+ }
@@ -0,0 +1,33 @@
1
+ // The harness bundled with a baba: runs inside the Worker the bundle is,
2
+ // and turns every service a job asks for into a request the runtime's main
3
+ // thread carries to the kernel. Messages in: init, then the kernel's. Out:
4
+ // loaded, failed, result, round, retired, service.
5
+ import type { FromRuntime, ToRuntime, Load } from "../transport/messages.ts";
6
+ import { isBaba, type Baba } from "../ecs/baba.ts";
7
+ import { Harness } from "./harness.ts";
8
+
9
+ declare const self: Worker;
10
+
11
+ export interface Init { type: "init"; project: Load["project"]; env: Record<string, string>; snapshot: Load["snapshot"]; prepare?: boolean }
12
+ export type ToHarness = Init | Exclude<ToRuntime, Load>;
13
+ export type FromHarness = FromRuntime;
14
+
15
+ /** Called by the bundle's entry with the baba its default export is; does nothing outside a worker. */
16
+ export function serve(v: unknown): void {
17
+ if (Bun.isMainThread) return;
18
+ const post = (m: FromHarness) => self.postMessage(m);
19
+ const h = new Harness(post);
20
+ self.onmessage = (e: MessageEvent<ToHarness>) => {
21
+ const m = e.data;
22
+ if (m.type === "init") {
23
+ try {
24
+ if (!isBaba(v)) throw new Error("the entry's default export is not a baba(); export default baba({ systems: [...] })");
25
+ h.load(v as Baba, m.project, m.env, m.snapshot, m.prepare);
26
+ } catch (err) {
27
+ post({ type: "failed", error: err instanceof Error ? err.message : String(err) });
28
+ }
29
+ return;
30
+ }
31
+ void h.handle(m);
32
+ };
33
+ }