@earendil-works/chord 1.0.4 → 1.2.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 (186) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +77 -194
  3. package/dist/cell.d.ts +101 -0
  4. package/dist/cell.d.ts.map +1 -0
  5. package/dist/cell.js +316 -0
  6. package/dist/cell.js.map +1 -0
  7. package/dist/cluster.d.ts +26 -0
  8. package/dist/cluster.d.ts.map +1 -0
  9. package/dist/cluster.js +214 -0
  10. package/dist/cluster.js.map +1 -0
  11. package/dist/connection.d.ts +157 -0
  12. package/dist/connection.d.ts.map +1 -0
  13. package/dist/connection.js +757 -0
  14. package/dist/connection.js.map +1 -0
  15. package/dist/context/index.d.ts +12 -1
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js.map +1 -1
  18. package/dist/contract.d.ts +133 -0
  19. package/dist/contract.d.ts.map +1 -0
  20. package/dist/contract.js +157 -0
  21. package/dist/contract.js.map +1 -0
  22. package/dist/declare.d.ts +70 -0
  23. package/dist/declare.d.ts.map +1 -0
  24. package/dist/declare.js +22 -0
  25. package/dist/declare.js.map +1 -0
  26. package/dist/delta/apply-immutable-trusted.js.map +1 -1
  27. package/dist/delta/diff.d.ts +1 -1
  28. package/dist/delta/diff.d.ts.map +1 -1
  29. package/dist/delta/diff.js.map +1 -1
  30. package/dist/delta/index.d.ts +2 -2
  31. package/dist/delta/index.d.ts.map +1 -1
  32. package/dist/delta/index.js +41 -0
  33. package/dist/delta/index.js.map +1 -1
  34. package/dist/delta/revision-validator.d.ts +1 -1
  35. package/dist/delta/revision-validator.d.ts.map +1 -1
  36. package/dist/delta/revision-validator.js.map +1 -1
  37. package/dist/delta/tracker.d.ts +1 -1
  38. package/dist/delta/tracker.d.ts.map +1 -1
  39. package/dist/delta/tracker.js +40 -4
  40. package/dist/delta/tracker.js.map +1 -1
  41. package/dist/errors.d.ts +27 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +44 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/host.d.ts +254 -0
  46. package/dist/host.d.ts.map +1 -0
  47. package/dist/host.js +795 -0
  48. package/dist/host.js.map +1 -0
  49. package/dist/index.d.ts +17 -10
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -8
  52. package/dist/index.js.map +1 -1
  53. package/dist/json.d.ts +9 -1
  54. package/dist/json.d.ts.map +1 -1
  55. package/dist/json.js.map +1 -1
  56. package/dist/link.d.ts +72 -0
  57. package/dist/link.d.ts.map +1 -0
  58. package/dist/link.js +169 -0
  59. package/dist/link.js.map +1 -0
  60. package/dist/loader.d.ts +4 -0
  61. package/dist/loader.d.ts.map +1 -0
  62. package/dist/loader.js +68 -0
  63. package/dist/loader.js.map +1 -0
  64. package/dist/manager.d.ts +115 -0
  65. package/dist/manager.d.ts.map +1 -0
  66. package/dist/manager.js +319 -0
  67. package/dist/manager.js.map +1 -0
  68. package/dist/node.d.ts +3 -4
  69. package/dist/node.d.ts.map +1 -1
  70. package/dist/node.js +3 -2
  71. package/dist/node.js.map +1 -1
  72. package/dist/publish.d.ts +5 -0
  73. package/dist/publish.d.ts.map +1 -0
  74. package/dist/publish.js +40 -0
  75. package/dist/publish.js.map +1 -0
  76. package/dist/record.d.ts +320 -0
  77. package/dist/record.d.ts.map +1 -0
  78. package/dist/record.js +1292 -0
  79. package/dist/record.js.map +1 -0
  80. package/dist/registry.d.ts +25 -0
  81. package/dist/registry.d.ts.map +1 -0
  82. package/dist/registry.js +6 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/reload.d.ts +118 -0
  85. package/dist/reload.d.ts.map +1 -0
  86. package/dist/reload.js +589 -0
  87. package/dist/reload.js.map +1 -0
  88. package/dist/remote.d.ts +70 -0
  89. package/dist/remote.d.ts.map +1 -0
  90. package/dist/remote.js +308 -0
  91. package/dist/remote.js.map +1 -0
  92. package/dist/schema.d.ts +21 -0
  93. package/dist/schema.d.ts.map +1 -0
  94. package/dist/schema.js +228 -0
  95. package/dist/schema.js.map +1 -0
  96. package/dist/state.d.ts +92 -0
  97. package/dist/state.d.ts.map +1 -0
  98. package/dist/{services/state.js → state.js} +28 -138
  99. package/dist/state.js.map +1 -0
  100. package/dist/types.d.ts +62 -262
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/dist/websocket.d.ts +16 -0
  104. package/dist/websocket.d.ts.map +1 -0
  105. package/dist/websocket.js +15 -0
  106. package/dist/websocket.js.map +1 -0
  107. package/dist/wire.d.ts +135 -0
  108. package/dist/wire.d.ts.map +1 -0
  109. package/dist/wire.js +112 -0
  110. package/dist/wire.js.map +1 -0
  111. package/docs/guide.md +750 -0
  112. package/docs/spec.md +261 -0
  113. package/docs/tutorial.md +909 -0
  114. package/package.json +10 -11
  115. package/src/delta/README.md +5 -3
  116. package/dist/api.d.ts +0 -20
  117. package/dist/api.d.ts.map +0 -1
  118. package/dist/api.js +0 -77
  119. package/dist/api.js.map +0 -1
  120. package/dist/bundler.d.ts +0 -6
  121. package/dist/bundler.d.ts.map +0 -1
  122. package/dist/bundler.js +0 -3
  123. package/dist/bundler.js.map +0 -1
  124. package/dist/facets/host.d.ts +0 -12
  125. package/dist/facets/host.d.ts.map +0 -1
  126. package/dist/facets/host.js +0 -763
  127. package/dist/facets/host.js.map +0 -1
  128. package/dist/facets/loader.d.ts +0 -3
  129. package/dist/facets/loader.d.ts.map +0 -1
  130. package/dist/facets/loader.js +0 -5
  131. package/dist/facets/loader.js.map +0 -1
  132. package/dist/node/bundle-loader.d.ts +0 -30
  133. package/dist/node/bundle-loader.d.ts.map +0 -1
  134. package/dist/node/bundle-loader.js +0 -377
  135. package/dist/node/bundle-loader.js.map +0 -1
  136. package/dist/node/bundle.d.ts +0 -26
  137. package/dist/node/bundle.d.ts.map +0 -1
  138. package/dist/node/bundle.js +0 -183
  139. package/dist/node/bundle.js.map +0 -1
  140. package/dist/node/manifest.d.ts +0 -36
  141. package/dist/node/manifest.d.ts.map +0 -1
  142. package/dist/node/manifest.js +0 -6
  143. package/dist/node/manifest.js.map +0 -1
  144. package/dist/node/package.d.ts +0 -15
  145. package/dist/node/package.d.ts.map +0 -1
  146. package/dist/node/package.js +0 -199
  147. package/dist/node/package.js.map +0 -1
  148. package/dist/services/consumer.d.ts +0 -11
  149. package/dist/services/consumer.d.ts.map +0 -1
  150. package/dist/services/consumer.js +0 -585
  151. package/dist/services/consumer.js.map +0 -1
  152. package/dist/services/errors.d.ts +0 -8
  153. package/dist/services/errors.d.ts.map +0 -1
  154. package/dist/services/errors.js +0 -22
  155. package/dist/services/errors.js.map +0 -1
  156. package/dist/services/handle.d.ts +0 -15
  157. package/dist/services/handle.d.ts.map +0 -1
  158. package/dist/services/handle.js +0 -101
  159. package/dist/services/handle.js.map +0 -1
  160. package/dist/services/instances.d.ts +0 -26
  161. package/dist/services/instances.d.ts.map +0 -1
  162. package/dist/services/instances.js +0 -140
  163. package/dist/services/instances.js.map +0 -1
  164. package/dist/services/loopback.d.ts +0 -5
  165. package/dist/services/loopback.d.ts.map +0 -1
  166. package/dist/services/loopback.js +0 -15
  167. package/dist/services/loopback.js.map +0 -1
  168. package/dist/services/provider.d.ts +0 -37
  169. package/dist/services/provider.d.ts.map +0 -1
  170. package/dist/services/provider.js +0 -521
  171. package/dist/services/provider.js.map +0 -1
  172. package/dist/services/state-codec.d.ts +0 -15
  173. package/dist/services/state-codec.d.ts.map +0 -1
  174. package/dist/services/state-codec.js +0 -133
  175. package/dist/services/state-codec.js.map +0 -1
  176. package/dist/services/state-internals.d.ts +0 -13
  177. package/dist/services/state-internals.d.ts.map +0 -1
  178. package/dist/services/state-internals.js +0 -10
  179. package/dist/services/state-internals.js.map +0 -1
  180. package/dist/services/state.d.ts +0 -28
  181. package/dist/services/state.d.ts.map +0 -1
  182. package/dist/services/state.js.map +0 -1
  183. package/dist/services/wire.d.ts +0 -63
  184. package/dist/services/wire.d.ts.map +0 -1
  185. package/dist/services/wire.js +0 -186
  186. package/dist/services/wire.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Frame } from \"./wire.ts\";\n\n/** An ordered (FIFO), bidirectional frame channel between two hosts. */\nexport interface Link {\n\treadonly closed: boolean;\n\tsend(frame: Frame): void;\n\t/** Sets the single receiver. Frames arriving earlier are queued. */\n\treceive(handler: (frame: Frame) => void): void;\n\tonClose(handler: (reason: string) => void): void;\n\t/** Closes both ends: the peer gets a `bye`. */\n\tclose(reason?: string): void;\n}\n\n/** Shared plumbing: receive queue, close handlers, `bye`. Transports implement `transmit` and `shutdown`. */\nabstract class BaseLink implements Link {\n\t#handler: ((frame: Frame) => void) | undefined;\n\t#queue: Frame[] = [];\n\treadonly #closeHandlers = new Set<(reason: string) => void>();\n\t#closed = false;\n\n\tget closed(): boolean {\n\t\treturn this.#closed;\n\t}\n\n\tprotected abstract transmit(frame: Frame): void;\n\tprotected abstract shutdown(): void;\n\n\tsend(frame: Frame): void {\n\t\tif (this.#closed) return;\n\t\tthis.transmit(frame);\n\t}\n\n\treceive(handler: (frame: Frame) => void): void {\n\t\tthis.#handler = handler;\n\t\tfor (const frame of this.#queue.splice(0)) handler(frame);\n\t}\n\n\tonClose(handler: (reason: string) => void): void {\n\t\tif (this.#closed) handler(\"closed\");\n\t\telse this.#closeHandlers.add(handler);\n\t}\n\n\tclose(reason = \"closed\"): void {\n\t\tif (this.#closed) return;\n\t\ttry {\n\t\t\tthis.transmit({ t: \"bye\", reason });\n\t\t} catch {\n\t\t\t// The transport may already be gone.\n\t\t}\n\t\tthis.ended(reason);\n\t}\n\n\t/** Delivers a parsed frame from the transport. */\n\tprotected deliver(frame: Frame): void {\n\t\tif (this.#closed) return;\n\t\tif (frame.t === \"bye\") {\n\t\t\tthis.ended(frame.reason ?? \"peer closed\");\n\t\t\treturn;\n\t\t}\n\t\tif (this.#handler === undefined) this.#queue.push(frame);\n\t\telse this.#handler(frame);\n\t}\n\n\t/** Marks the link closed (transport ended) and notifies once. */\n\tprotected ended(reason: string): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#queue = [];\n\t\ttry {\n\t\t\tthis.shutdown();\n\t\t} finally {\n\t\t\tfor (const handler of this.#closeHandlers) handler(reason);\n\t\t\tthis.#closeHandlers.clear();\n\t\t}\n\t}\n}\n\n/** Parses a JSON frame; anything else is a protocol error. */\nfunction parseFrame(data: string): Frame {\n\tconst frame = JSON.parse(data) as Frame;\n\tif (frame === null || typeof frame !== \"object\" || typeof frame.t !== \"string\") throw new TypeError(\"not a frame\");\n\treturn frame;\n}\n\n// ─── memory ──────────────────────────────────────────────────────────────────\n\nexport class MemoryLink extends BaseLink {\n\tpeer: MemoryLink | undefined;\n\t#severed = false;\n\t#pending = Promise.resolve();\n\n\t/** Simulates a dead network: frames are dropped silently in both directions and no close is signalled. */\n\tsever(): void {\n\t\tthis.#severed = true;\n\t\tif (this.peer) this.peer.#severed = true;\n\t}\n\n\tprotected transmit(frame: Frame): void {\n\t\tif (this.#severed) return;\n\t\t// A JSON round trip detaches the frame (and rejects what JSON would mangle silently, e.g. bigint).\n\t\tconst data = JSON.stringify(frame);\n\t\tconst peer = this.peer;\n\t\t// One promise chain per direction keeps delivery FIFO and asynchronous, like a real transport.\n\t\tthis.#pending = this.#pending.then(() => {\n\t\t\tif (!this.#severed && peer !== undefined) peer.deliver(parseFrame(data));\n\t\t});\n\t}\n\n\tprotected shutdown(): void {\n\t\tconst peer = this.peer;\n\t\tthis.#pending = this.#pending.then(() => peer?.ended(\"peer closed\"));\n\t}\n}\n\n/** Two connected in-memory links (tests). */\nexport function memoryLinkPair(): [MemoryLink, MemoryLink] {\n\tconst a = new MemoryLink();\n\tconst b = new MemoryLink();\n\ta.peer = b;\n\tb.peer = a;\n\treturn [a, b];\n}\n\n// ─── Node IPC ────────────────────────────────────────────────────────────────\n\n/** `process` in a forked child, or the parent's `ChildProcess`. Structural, so Chord needs no Node types here. */\nexport interface IpcChannel {\n\treadonly connected: boolean;\n\tsend(message: unknown): boolean;\n\ton(event: \"message\", listener: (message: unknown) => void): unknown;\n\ton(event: \"disconnect\", listener: () => void): unknown;\n\toff(event: \"message\", listener: (message: unknown) => void): unknown;\n\toff(event: \"disconnect\", listener: () => void): unknown;\n}\n\n/** Chord frames on an IPC channel are wrapped as `{ chord2: frame }`; other messages (control) pass by. */\nexport class IpcLink extends BaseLink {\n\treadonly #channel: IpcChannel;\n\treadonly #onMessage = (message: unknown): void => {\n\t\tconst frame = (message as { chord2?: Frame } | null)?.chord2;\n\t\tif (frame !== undefined) this.deliver(frame);\n\t};\n\treadonly #onDisconnect = (): void => this.ended(\"ipc disconnected\");\n\n\tconstructor(channel: IpcChannel) {\n\t\tsuper();\n\t\tthis.#channel = channel;\n\t\tchannel.on(\"message\", this.#onMessage);\n\t\tchannel.on(\"disconnect\", this.#onDisconnect);\n\t}\n\n\tprotected transmit(frame: Frame): void {\n\t\tif (this.#channel.connected) this.#channel.send({ chord2: frame });\n\t}\n\n\tprotected shutdown(): void {\n\t\tthis.#channel.off(\"message\", this.#onMessage);\n\t\tthis.#channel.off(\"disconnect\", this.#onDisconnect);\n\t}\n}\n\nexport const ipcLink = (channel: IpcChannel): IpcLink => new IpcLink(channel);\n\n// ─── WebSocket ───────────────────────────────────────────────────────────────\n\n/** The WHATWG WebSocket surface: the browser `WebSocket`, Node's global `WebSocket`, and `ws` sockets. */\nexport interface WebSocketLike {\n\treadonly readyState: number;\n\tsend(data: string): void;\n\tclose(code?: number, reason?: string): void;\n\taddEventListener(type: \"message\", listener: (event: { readonly data: unknown }) => void): void;\n\taddEventListener(type: \"open\" | \"close\" | \"error\", listener: () => void): void;\n}\n\nconst OPEN = 1;\n\nexport class WebSocketLink extends BaseLink {\n\treadonly #socket: WebSocketLike;\n\t#outbox: string[] = [];\n\n\tconstructor(socket: WebSocketLike) {\n\t\tsuper();\n\t\tthis.#socket = socket;\n\t\tsocket.addEventListener(\"open\", () => {\n\t\t\tfor (const data of this.#outbox.splice(0)) socket.send(data);\n\t\t});\n\t\tsocket.addEventListener(\"message\", (event) => {\n\t\t\ttry {\n\t\t\t\tthis.deliver(parseFrame(typeof event.data === \"string\" ? event.data : String(event.data)));\n\t\t\t} catch {\n\t\t\t\tthis.close(\"protocol error\");\n\t\t\t}\n\t\t});\n\t\tsocket.addEventListener(\"close\", () => this.ended(\"socket closed\"));\n\t}\n\n\tprotected transmit(frame: Frame): void {\n\t\tconst data = JSON.stringify(frame);\n\t\tif (this.#socket.readyState === OPEN) this.#socket.send(data);\n\t\telse this.#outbox.push(data);\n\t}\n\n\tprotected shutdown(): void {\n\t\tthis.#outbox = [];\n\t\tif (this.#socket.readyState <= OPEN) this.#socket.close(1000);\n\t}\n}\n\nexport const webSocketLink = (socket: WebSocketLike): WebSocketLink => new WebSocketLink(socket);\n"]}
@@ -0,0 +1,4 @@
1
+ import type { FacetLoader } from "./host.ts";
2
+ /** Imports fresh copies of facet modules (default export: a FacetDefinition); each facet is named by its URL. */
3
+ export declare const nodeLoader: FacetLoader;
4
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0B,WAAW,EAAE,MAAM,WAAW,CAAC;AAmCrE,iHAAiH;AACjH,eAAO,MAAM,UAAU,EAAE,WAmBxB,CAAC"}
package/dist/loader.js ADDED
@@ -0,0 +1,68 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // Node facet loader: `createHost({ loader: nodeLoader })`. Kept out of index.ts so the core stays browser-safe.
10
+ //
11
+ // TODO(loader): replace with an isolating, evicting loader. Every reload imports fresh module copies under a new
12
+ // query and the old copies are never evicted (they leak until the process exits). TUI plugins need isolation, which
13
+ // this loader does not provide.
14
+ import { registerHooks } from "node:module";
15
+ import { pathToFileURL } from "node:url";
16
+ const PARAM = "chord2-reload";
17
+ /** Directory URLs (trailing slash) of loaded facet modules; their files are re-imported on reload. */
18
+ const roots = new Set();
19
+ let hooked = false;
20
+ let generation = 0;
21
+ /**
22
+ * A query on the entry URL only re-imports the entry module: its relative imports would resolve to cached copies.
23
+ * This hook carries the reload query from a re-imported parent to children inside a facet module's directory.
24
+ * Modules outside (Chord itself, shared code) stay single instances.
25
+ */
26
+ function hook() {
27
+ if (hooked)
28
+ return;
29
+ hooked = true;
30
+ registerHooks({
31
+ resolve(specifier, context, nextResolve) {
32
+ const result = nextResolve(specifier, context);
33
+ const version = context.parentURL === undefined ? null : new URL(context.parentURL).searchParams.get(PARAM);
34
+ if (version === null || !result.url.startsWith("file:"))
35
+ return result;
36
+ const url = new URL(result.url);
37
+ if (url.searchParams.has(PARAM) || ![...roots].some((root) => url.href.startsWith(root)))
38
+ return result;
39
+ url.searchParams.set(PARAM, version);
40
+ return { ...result, url: url.href };
41
+ },
42
+ });
43
+ }
44
+ const toUrl = (module) => module instanceof URL
45
+ ? new URL(module.href)
46
+ : new URL(module.startsWith("file:") ? module : pathToFileURL(module).href);
47
+ /** Imports fresh copies of facet modules (default export: a FacetDefinition); each facet is named by its URL. */
48
+ export const nodeLoader = async (modules) => {
49
+ hook();
50
+ const version = String(++generation);
51
+ const facets = [];
52
+ for (const module of modules) {
53
+ const url = toUrl(module);
54
+ url.search = "";
55
+ const name = url.href;
56
+ roots.add(new URL(".", url).href);
57
+ url.searchParams.set(PARAM, version);
58
+ // Owner-approved exception to the no-dynamic-import rule; see TODO(loader) above.
59
+ const loaded = (await import(__rewriteRelativeImportExtension(url.href)));
60
+ const definition = loaded.default;
61
+ if (definition === undefined || typeof definition.setup !== "function") {
62
+ throw new TypeError(`${name}: default export must be a facet (defineFacet)`);
63
+ }
64
+ facets.push({ name, setup: definition.setup, uses: definition.uses });
65
+ }
66
+ return facets;
67
+ };
68
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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","sourcesContent":["// Node facet loader: `createHost({ loader: nodeLoader })`. Kept out of index.ts so the core stays browser-safe.\n//\n// TODO(loader): replace with an isolating, evicting loader. Every reload imports fresh module copies under a new\n// query and the old copies are never evicted (they leak until the process exits). TUI plugins need isolation, which\n// this loader does not provide.\nimport { registerHooks } from \"node:module\";\nimport { pathToFileURL } from \"node:url\";\nimport type { Facet, FacetDefinition, FacetLoader } from \"./host.ts\";\n\nconst PARAM = \"chord2-reload\";\n\n/** Directory URLs (trailing slash) of loaded facet modules; their files are re-imported on reload. */\nconst roots = new Set<string>();\nlet hooked = false;\nlet generation = 0;\n\n/**\n * A query on the entry URL only re-imports the entry module: its relative imports would resolve to cached copies.\n * This hook carries the reload query from a re-imported parent to children inside a facet module's directory.\n * Modules outside (Chord itself, shared code) stay single instances.\n */\nfunction hook(): void {\n\tif (hooked) return;\n\thooked = true;\n\tregisterHooks({\n\t\tresolve(specifier, context, nextResolve) {\n\t\t\tconst result = nextResolve(specifier, context);\n\t\t\tconst version = context.parentURL === undefined ? null : new URL(context.parentURL).searchParams.get(PARAM);\n\t\t\tif (version === null || !result.url.startsWith(\"file:\")) return result;\n\t\t\tconst url = new URL(result.url);\n\t\t\tif (url.searchParams.has(PARAM) || ![...roots].some((root) => url.href.startsWith(root))) return result;\n\t\t\turl.searchParams.set(PARAM, version);\n\t\t\treturn { ...result, url: url.href };\n\t\t},\n\t});\n}\n\nconst toUrl = (module: string | URL): URL =>\n\tmodule instanceof URL\n\t\t? new URL(module.href)\n\t\t: new URL(module.startsWith(\"file:\") ? module : pathToFileURL(module).href);\n\n/** Imports fresh copies of facet modules (default export: a FacetDefinition); each facet is named by its URL. */\nexport const nodeLoader: FacetLoader = async (modules) => {\n\thook();\n\tconst version = String(++generation);\n\tconst facets: Facet[] = [];\n\tfor (const module of modules) {\n\t\tconst url = toUrl(module);\n\t\turl.search = \"\";\n\t\tconst name = url.href;\n\t\troots.add(new URL(\".\", url).href);\n\t\turl.searchParams.set(PARAM, version);\n\t\t// Owner-approved exception to the no-dynamic-import rule; see TODO(loader) above.\n\t\tconst loaded = (await import(url.href)) as { default?: FacetDefinition };\n\t\tconst definition = loaded.default;\n\t\tif (definition === undefined || typeof definition.setup !== \"function\") {\n\t\t\tthrow new TypeError(`${name}: default export must be a facet (defineFacet)`);\n\t\t}\n\t\tfacets.push({ name, setup: definition.setup, uses: definition.uses });\n\t}\n\treturn facets;\n};\n"]}
@@ -0,0 +1,115 @@
1
+ import { type IpcLink } from "./link.ts";
2
+ /** Control messages share the IPC channel with chord frames: `{ chord2control: message }`. */
3
+ export type ControlMessage = {
4
+ readonly type: "init";
5
+ readonly value: unknown;
6
+ } | {
7
+ readonly type: "ready";
8
+ } | {
9
+ readonly type: "fatal";
10
+ readonly message: string;
11
+ readonly reason?: string;
12
+ } | {
13
+ readonly type: "shutdown";
14
+ } | {
15
+ readonly type: "status";
16
+ readonly activeCalls?: number;
17
+ readonly viewers?: number;
18
+ readonly durableTasks?: number;
19
+ };
20
+ export interface Liveness {
21
+ /** Started but not ready yet. */
22
+ readonly booting: boolean;
23
+ readonly activeCalls: number;
24
+ /** Viewers the child reports (it counts heartbeating subscribers). */
25
+ readonly viewers: number;
26
+ readonly durableTasks: number;
27
+ /** Milliseconds since the child last reported work (calls, viewers or tasks), or since it became ready. */
28
+ readonly idleFor: number;
29
+ }
30
+ export interface Exit {
31
+ readonly code: number | null;
32
+ readonly signal: string | null;
33
+ }
34
+ /** A running child: its link (for `host.accept`) and its exit. */
35
+ export interface Instance {
36
+ readonly pid: number;
37
+ readonly link: IpcLink;
38
+ readonly exited: Promise<Exit>;
39
+ send(message: ControlMessage): void;
40
+ /** Kills the child and everything it spawned. */
41
+ killTree(): void;
42
+ onControl(listener: (message: ControlMessage) => void): void;
43
+ }
44
+ export interface Driver {
45
+ spawn(): Instance;
46
+ }
47
+ export interface ForkDriverOptions {
48
+ /** The child's entry module. */
49
+ readonly entry: string;
50
+ readonly args?: readonly string[];
51
+ readonly env?: Readonly<Record<string, string>>;
52
+ /** Node flags for the child, e.g. `["--import", sourceResolver]` (default: none). */
53
+ readonly execArgv?: readonly string[];
54
+ /** Sent to the child as its first control message (`childControl().init`). */
55
+ readonly init?: unknown;
56
+ /**
57
+ * A lock name: one live process per name, across managers and processes. The child acquires it as a kernel lock
58
+ * (`childControl().lock()`, before opening its storage); a second child fails to start with `unavailable/locked`.
59
+ */
60
+ readonly lock?: string;
61
+ /** Optional stdout/stderr tap. Streams are still mirrored to the parent's standard output. */
62
+ readonly onOutput?: (stream: "stdout" | "stderr", chunk: Uint8Array) => void;
63
+ /** Optional application metadata tap; control messages and chord frames keep their normal handling. */
64
+ readonly onMessage?: (message: unknown) => void;
65
+ }
66
+ /**
67
+ * A process lock owned by the kernel for the holder's lifetime: an abstract socket (Linux, Android) or a named pipe
68
+ * (Windows) vanishes with its process, so there is no stale state to take over and no race between two would-be
69
+ * owners. Other platforms need a socket file, removed on exit; a leftover one nobody accepts on is taken over.
70
+ * The child acquires it (before opening storage), so the lock follows the process that owns the data.
71
+ */
72
+ export declare function processLock(name: string): Promise<() => Promise<void>>;
73
+ export declare function forkDriver(options: ForkDriverOptions): Driver;
74
+ export interface HostManagerOptions {
75
+ readonly driver: Driver;
76
+ /** Evaluated every `checkInterval` while running; `false` stops the child. */
77
+ readonly keepAlive?: (liveness: Liveness) => boolean;
78
+ readonly checkInterval?: number;
79
+ /** Milliseconds a child may take to shut down before its process tree is killed (default 5000). */
80
+ readonly grace?: number;
81
+ /** Milliseconds to wait for `ready` (default 30000). */
82
+ readonly readyTimeout?: number;
83
+ /** The child exited on its own (crash, fatal) or after a stop. */
84
+ readonly onExit?: (exit: Exit) => void;
85
+ }
86
+ export interface HostManager {
87
+ readonly status: "stopped" | "starting" | "running" | "stopping";
88
+ readonly instance: Instance | undefined;
89
+ readonly liveness: Liveness;
90
+ /** Starts the child (or returns the running one) once it reported ready. Serialized with stop/replace. */
91
+ start(): Promise<Instance>;
92
+ /** Asks the child to shut down, then kills its process tree after the grace period. */
93
+ stop(): Promise<void>;
94
+ /** Stop, wait for the exit, start. */
95
+ replace(): Promise<Instance>;
96
+ }
97
+ export declare function createHostManager(options: HostManagerOptions): HostManager;
98
+ /** The child side of the control channel (call in the child's entry). */
99
+ export declare function childControl(channel?: NodeJS.Process): {
100
+ readonly init: Promise<unknown>;
101
+ /**
102
+ * Acquires the manager's lock (if one was configured) as a kernel lock held for this process's lifetime. Call it
103
+ * before opening storage. Fails with `unavailable/locked`, also reported to the manager as a fatal start error.
104
+ */
105
+ lock(): Promise<void>;
106
+ ready(): void;
107
+ fatal(error: unknown): void;
108
+ report(status: {
109
+ activeCalls?: number;
110
+ viewers?: number;
111
+ durableTasks?: number;
112
+ }): void;
113
+ onShutdown(handler: () => void | Promise<void>): void;
114
+ };
115
+ //# sourceMappingURL=manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,319 @@
1
+ // Host manager (Node only): runs a child host process and keeps at most one instance of it alive.
2
+ import { fork } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { unlinkSync } from "node:fs";
5
+ import { rm } from "node:fs/promises";
6
+ import { connect, createServer } from "node:net";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { ChordError } from "./errors.js";
10
+ import { ipcLink } from "./link.js";
11
+ /**
12
+ * A process lock owned by the kernel for the holder's lifetime: an abstract socket (Linux, Android) or a named pipe
13
+ * (Windows) vanishes with its process, so there is no stale state to take over and no race between two would-be
14
+ * owners. Other platforms need a socket file, removed on exit; a leftover one nobody accepts on is taken over.
15
+ * The child acquires it (before opening storage), so the lock follows the process that owns the data.
16
+ */
17
+ export async function processLock(name) {
18
+ const key = createHash("sha256").update(name).digest("hex").slice(0, 32);
19
+ const file = process.platform !== "win32" && process.platform !== "linux" && process.platform !== "android";
20
+ const address = process.platform === "win32"
21
+ ? `\\\\.\\pipe\\chord2-${key}`
22
+ : file
23
+ ? join(tmpdir(), `chord2-${key}.sock`)
24
+ : `\0chord2-${key}`;
25
+ const server = createServer((socket) => socket.end());
26
+ for (;;) {
27
+ try {
28
+ await new Promise((resolve, reject) => {
29
+ server.once("error", reject);
30
+ server.listen(address, () => {
31
+ server.removeListener("error", reject);
32
+ resolve();
33
+ });
34
+ });
35
+ break;
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "EADDRINUSE")
39
+ throw error;
40
+ if (!file || (await accepting(address))) {
41
+ throw new ChordError("unavailable", `lock ${name} is held by another process`, { reason: "locked" });
42
+ }
43
+ await rm(address, { force: true });
44
+ }
45
+ }
46
+ const removeFile = () => {
47
+ try {
48
+ unlinkSync(address);
49
+ }
50
+ catch {
51
+ // Already gone.
52
+ }
53
+ };
54
+ if (file)
55
+ process.once("exit", removeFile);
56
+ return () => new Promise((resolve, reject) => {
57
+ if (file)
58
+ process.removeListener("exit", removeFile);
59
+ server.close((error) => (error ? reject(error) : resolve()));
60
+ });
61
+ }
62
+ function accepting(address) {
63
+ return new Promise((resolve) => {
64
+ const socket = connect(address);
65
+ socket.once("connect", () => {
66
+ socket.destroy();
67
+ resolve(true);
68
+ });
69
+ socket.once("error", (error) => resolve(error.code !== "ECONNREFUSED" && error.code !== "ENOENT"));
70
+ });
71
+ }
72
+ const LOCK_ENV = "CHORD2_LOCK";
73
+ export function forkDriver(options) {
74
+ return {
75
+ spawn() {
76
+ // Detached: the child leads a process group, so the whole tree can be killed.
77
+ const child = fork(options.entry, [...(options.args ?? [])], {
78
+ env: {
79
+ ...process.env,
80
+ ...options.env,
81
+ ...(options.lock === undefined ? {} : { [LOCK_ENV]: options.lock }),
82
+ },
83
+ execArgv: [...(options.execArgv ?? [])],
84
+ detached: true,
85
+ stdio: ["ignore", options.onOutput ? "pipe" : "inherit", options.onOutput ? "pipe" : "inherit", "ipc"],
86
+ });
87
+ if (options.onOutput) {
88
+ for (const stream of ["stdout", "stderr"]) {
89
+ child[stream]?.on("data", (chunk) => {
90
+ options.onOutput(stream, chunk);
91
+ process[stream].write(chunk);
92
+ });
93
+ }
94
+ }
95
+ const pid = child.pid ?? -1;
96
+ const killGroup = () => {
97
+ try {
98
+ process.kill(-pid, "SIGKILL");
99
+ }
100
+ catch {
101
+ // No group left.
102
+ }
103
+ };
104
+ const exited = new Promise((resolve) => {
105
+ child.once("exit", (code, signal) => {
106
+ // However the leader ended (graceful, crash, kill), nothing it spawned outlives it.
107
+ killGroup();
108
+ resolve({ code, signal });
109
+ });
110
+ });
111
+ const listeners = new Set();
112
+ child.on("message", (message) => {
113
+ options.onMessage?.(message);
114
+ const control = message?.chord2control;
115
+ if (control !== undefined)
116
+ for (const listener of listeners)
117
+ listener(control);
118
+ });
119
+ const send = (message) => {
120
+ if (child.connected)
121
+ child.send({ chord2control: message });
122
+ };
123
+ if (options.init !== undefined)
124
+ send({ type: "init", value: options.init });
125
+ return {
126
+ pid,
127
+ link: ipcLink(child),
128
+ exited,
129
+ send,
130
+ killTree: () => {
131
+ try {
132
+ process.kill(-pid, "SIGKILL");
133
+ }
134
+ catch {
135
+ child.kill("SIGKILL");
136
+ }
137
+ },
138
+ onControl: (listener) => listeners.add(listener),
139
+ };
140
+ },
141
+ };
142
+ }
143
+ export function createHostManager(options) {
144
+ let status = "stopped";
145
+ let instance;
146
+ let queue = Promise.resolve();
147
+ let report = { activeCalls: 0, viewers: 0, durableTasks: 0 };
148
+ /** When the child last became idle; undefined while it reports work. */
149
+ let idleSince;
150
+ let booting = false;
151
+ let timer;
152
+ const serialized = (step) => {
153
+ const next = queue.then(step, step);
154
+ queue = next.catch(() => { });
155
+ return next;
156
+ };
157
+ const liveness = () => ({
158
+ booting,
159
+ ...report,
160
+ idleFor: idleSince === undefined ? 0 : Date.now() - idleSince,
161
+ });
162
+ const doStop = async () => {
163
+ const current = instance;
164
+ if (current === undefined) {
165
+ status = "stopped";
166
+ booting = false;
167
+ return;
168
+ }
169
+ status = "stopping";
170
+ if (timer !== undefined)
171
+ clearInterval(timer);
172
+ current.send({ type: "shutdown" });
173
+ let graceTimer;
174
+ const graceful = await Promise.race([
175
+ current.exited.then(() => true),
176
+ new Promise((resolve) => {
177
+ graceTimer = setTimeout(() => resolve(false), options.grace ?? 5000);
178
+ }),
179
+ ]);
180
+ clearTimeout(graceTimer);
181
+ if (!graceful)
182
+ current.killTree();
183
+ await current.exited;
184
+ if (instance === current)
185
+ instance = undefined;
186
+ status = "stopped";
187
+ };
188
+ const doStart = async () => {
189
+ if (instance !== undefined && status === "running")
190
+ return instance;
191
+ status = "starting";
192
+ booting = true;
193
+ report = { activeCalls: 0, viewers: 0, durableTasks: 0 };
194
+ let current;
195
+ try {
196
+ current = options.driver.spawn();
197
+ }
198
+ catch (error) {
199
+ status = "stopped";
200
+ booting = false;
201
+ throw error;
202
+ }
203
+ instance = current;
204
+ current.exited.then((exit) => {
205
+ if (instance === current) {
206
+ instance = undefined;
207
+ status = "stopped";
208
+ if (timer !== undefined)
209
+ clearInterval(timer);
210
+ }
211
+ options.onExit?.(exit);
212
+ });
213
+ await new Promise((resolve, reject) => {
214
+ const timeout = setTimeout(() => reject(new ChordError("unavailable", "child not ready in time")), options.readyTimeout ?? 30_000);
215
+ const settle = (fn) => {
216
+ clearTimeout(timeout);
217
+ fn();
218
+ };
219
+ current.exited.then((exit) => settle(() => reject(new ChordError("unavailable", `child exited before ready (${exit.code ?? exit.signal})`))));
220
+ current.onControl((message) => {
221
+ if (message.type === "ready")
222
+ settle(resolve);
223
+ else if (message.type === "fatal")
224
+ settle(() => reject(new ChordError("unavailable", `child failed: ${message.message}`, { reason: message.reason })));
225
+ else if (message.type === "status") {
226
+ report = {
227
+ activeCalls: message.activeCalls ?? 0,
228
+ viewers: message.viewers ?? 0,
229
+ durableTasks: message.durableTasks ?? 0,
230
+ };
231
+ // The idle clock starts at the busy → idle transition, not at the last busy report.
232
+ if (report.activeCalls + report.viewers + report.durableTasks > 0)
233
+ idleSince = undefined;
234
+ else
235
+ idleSince ??= Date.now();
236
+ }
237
+ });
238
+ }).catch(async (error) => {
239
+ current.killTree();
240
+ await current.exited;
241
+ throw error;
242
+ });
243
+ booting = false;
244
+ if (report.activeCalls + report.viewers + report.durableTasks === 0)
245
+ idleSince ??= Date.now();
246
+ status = "running";
247
+ if (options.keepAlive !== undefined) {
248
+ const keepAlive = options.keepAlive;
249
+ timer = setInterval(() => {
250
+ if (status === "running" && !keepAlive(liveness()))
251
+ void serialized(doStop);
252
+ }, options.checkInterval ?? 1000);
253
+ timer.unref?.();
254
+ }
255
+ return current;
256
+ };
257
+ return {
258
+ get status() {
259
+ return status;
260
+ },
261
+ get instance() {
262
+ return instance;
263
+ },
264
+ get liveness() {
265
+ return liveness();
266
+ },
267
+ start: () => serialized(doStart),
268
+ stop: () => serialized(doStop),
269
+ replace: () => serialized(async () => {
270
+ await doStop();
271
+ return doStart();
272
+ }),
273
+ };
274
+ }
275
+ /** The child side of the control channel (call in the child's entry). */
276
+ export function childControl(channel = process) {
277
+ const send = (message) => {
278
+ if (channel.connected)
279
+ channel.send?.({ chord2control: message });
280
+ };
281
+ let resolveInit = () => { };
282
+ const init = new Promise((resolve) => {
283
+ resolveInit = resolve;
284
+ });
285
+ const shutdown = new Set();
286
+ channel.on("message", (message) => {
287
+ const control = message?.chord2control;
288
+ if (control?.type === "init")
289
+ resolveInit(control.value);
290
+ if (control?.type === "shutdown")
291
+ for (const handler of shutdown)
292
+ void handler();
293
+ });
294
+ const fatal = (error) => send({
295
+ type: "fatal",
296
+ message: error instanceof Error ? error.message : String(error),
297
+ ...(error instanceof ChordError && error.reason !== undefined ? { reason: error.reason } : {}),
298
+ });
299
+ return {
300
+ init,
301
+ lock: async () => {
302
+ const name = process.env[LOCK_ENV];
303
+ if (name === undefined)
304
+ return;
305
+ try {
306
+ await processLock(name);
307
+ }
308
+ catch (error) {
309
+ fatal(error);
310
+ throw error;
311
+ }
312
+ },
313
+ ready: () => send({ type: "ready" }),
314
+ fatal,
315
+ report: (status) => send({ type: "status", ...status }),
316
+ onShutdown: (handler) => shutdown.add(handler),
317
+ };
318
+ }
319
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,OAAO,EAAqB,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAiC,OAAO,EAAE,MAAM,WAAW,CAAC;AAkEnE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY;IAC7C,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC;IAC5G,MAAM,OAAO,GACZ,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC3B,CAAC,CAAC,uBAAuB,GAAG,EAAE;QAC9B,CAAC,CAAC,IAAI;YACL,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC;YACtC,CAAC,CAAC,YAAY,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACtD,SAAS,CAAC;QACT,IAAI,CAAC;YACJ,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC3C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBAC7B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE;oBAC3B,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBACvC,OAAO,EAAE,CAAC;gBACX,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,MAAM;QACP,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,YAAY;gBAAE,MAAM,KAAK,CAAC;YACxE,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,UAAU,CAAC,aAAa,EAAE,QAAQ,IAAI,6BAA6B,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;YACtG,CAAC;YACD,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;IACF,CAAC;IACD,MAAM,UAAU,GAAG,GAAS,EAAE;QAC7B,IAAI,CAAC;YACJ,UAAU,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,gBAAgB;QACjB,CAAC;IACF,CAAC,CAAC;IACF,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC3C,OAAO,GAAG,EAAE,CACX,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI;YAAE,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrD,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IACjC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;YAC3B,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE,CACrD,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CACjE,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,QAAQ,GAAG,aAAa,CAAC;AAE/B,MAAM,UAAU,UAAU,CAAC,OAA0B;IACpD,OAAO;QACN,KAAK;YACJ,8EAA8E;YAC9E,MAAM,KAAK,GAAiB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE;gBAC1E,GAAG,EAAE;oBACJ,GAAG,OAAO,CAAC,GAAG;oBACd,GAAG,OAAO,CAAC,GAAG;oBACd,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;iBACnE;gBACD,QAAQ,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;gBACvC,QAAQ,EAAE,IAAI;gBACd,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC;aACtG,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACtB,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAU,EAAE,CAAC;oBACpD,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;wBAC3C,OAAO,CAAC,QAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;wBACjC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBAC9B,CAAC,CAAC,CAAC;gBACJ,CAAC;YACF,CAAC;YACD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAC5B,MAAM,SAAS,GAAG,GAAS,EAAE;gBAC5B,IAAI,CAAC;oBACJ,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;gBAC/B,CAAC;gBAAC,MAAM,CAAC;oBACR,iBAAiB;gBAClB,CAAC;YACF,CAAC,CAAC;YACF,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAC5C,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;oBACnC,oFAAoF;oBACpF,SAAS,EAAE,CAAC;oBACZ,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC3B,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAqC,CAAC;YAC/D,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;gBAC/B,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;gBAC7B,MAAM,OAAO,GAAI,OAAqD,EAAE,aAAa,CAAC;gBACtF,IAAI,OAAO,KAAK,SAAS;oBAAE,KAAK,MAAM,QAAQ,IAAI,SAAS;wBAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChF,CAAC,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,OAAuB,EAAQ,EAAE;gBAC9C,IAAI,KAAK,CAAC,SAAS;oBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;YAC7D,CAAC,CAAC;YACF,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5E,OAAO;gBACN,GAAG;gBACH,IAAI,EAAE,OAAO,CAAC,KAA8B,CAAC;gBAC7C,MAAM;gBACN,IAAI;gBACJ,QAAQ,EAAE,GAAG,EAAE;oBACd,IAAI,CAAC;wBACJ,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;oBAC/B,CAAC;oBAAC,MAAM,CAAC;wBACR,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACvB,CAAC;gBACF,CAAC;gBACD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;aAChD,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC;AA2BD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC5D,IAAI,MAAM,GAA0B,SAAS,CAAC;IAC9C,IAAI,QAA8B,CAAC;IACnC,IAAI,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAChD,IAAI,MAAM,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IAC7D,wEAAwE;IACxE,IAAI,SAA6B,CAAC;IAClC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,KAAiD,CAAC;IAEtD,MAAM,UAAU,GAAG,CAAI,IAAsB,EAAc,EAAE;QAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7B,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAa,EAAE,CAAC,CAAC;QACjC,OAAO;QACP,GAAG,MAAM;QACT,OAAO,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;KAC7D,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACxC,MAAM,OAAO,GAAG,QAAQ,CAAC;QACzB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,GAAG,SAAS,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC;YAChB,OAAO;QACR,CAAC;QACD,MAAM,GAAG,UAAU,CAAC;QACpB,IAAI,KAAK,KAAK,SAAS;YAAE,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QACnC,IAAI,UAAqD,CAAC;QAC1D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YACnC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;YAC/B,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;gBAChC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;YACtE,CAAC,CAAC;SACF,CAAC,CAAC;QACH,YAAY,CAAC,UAAU,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;QAClC,MAAM,OAAO,CAAC,MAAM,CAAC;QACrB,IAAI,QAAQ,KAAK,OAAO;YAAE,QAAQ,GAAG,SAAS,CAAC;QAC/C,MAAM,GAAG,SAAS,CAAC;IACpB,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,KAAK,IAAuB,EAAE;QAC7C,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QACpE,MAAM,GAAG,UAAU,CAAC;QACpB,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,GAAG,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QACzD,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACJ,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,GAAG,SAAS,CAAC;YACnB,OAAO,GAAG,KAAK,CAAC;YAChB,MAAM,KAAK,CAAC;QACb,CAAC;QACD,QAAQ,GAAG,OAAO,CAAC;QACnB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAC5B,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;gBAC1B,QAAQ,GAAG,SAAS,CAAC;gBACrB,MAAM,GAAG,SAAS,CAAC;gBACnB,IAAI,KAAK,KAAK,SAAS;oBAAE,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC;YACD,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,OAAO,GAAG,UAAU,CACzB,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAC,EACtE,OAAO,CAAC,YAAY,IAAI,MAAM,CAC9B,CAAC;YACF,MAAM,MAAM,GAAG,CAAC,EAAc,EAAQ,EAAE;gBACvC,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,EAAE,EAAE,CAAC;YACN,CAAC,CAAC;YACF,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAC5B,MAAM,CAAC,GAAG,EAAE,CACX,MAAM,CAAC,IAAI,UAAU,CAAC,aAAa,EAAE,8BAA8B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAChG,CACD,CAAC;YACF,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC7B,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;oBAAE,MAAM,CAAC,OAAO,CAAC,CAAC;qBACzC,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO;oBAChC,MAAM,CAAC,GAAG,EAAE,CACX,MAAM,CAAC,IAAI,UAAU,CAAC,aAAa,EAAE,iBAAiB,OAAO,CAAC,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CACrG,CAAC;qBACE,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACpC,MAAM,GAAG;wBACR,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC;wBACrC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,CAAC;wBAC7B,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC;qBACvC,CAAC;oBACF,oFAAoF;oBACpF,IAAI,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,YAAY,GAAG,CAAC;wBAAE,SAAS,GAAG,SAAS,CAAC;;wBACpF,SAAS,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC/B,CAAC;YACF,CAAC,CAAC,CAAC;QACJ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAc,EAAE,EAAE;YACjC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,OAAO,CAAC,MAAM,CAAC;YACrB,MAAM,KAAK,CAAC;QACb,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,YAAY,KAAK,CAAC;YAAE,SAAS,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9F,MAAM,GAAG,SAAS,CAAC;QACnB,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YACpC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;gBACxB,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;oBAAE,KAAK,UAAU,CAAC,MAAM,CAAC,CAAC;YAC7E,CAAC,EAAE,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC;YAClC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACjB,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC,CAAC;IAEF,OAAO;QACN,IAAI,MAAM;YACT,OAAO,MAAM,CAAC;QACf,CAAC;QACD,IAAI,QAAQ;YACX,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,IAAI,QAAQ;YACX,OAAO,QAAQ,EAAE,CAAC;QACnB,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QAChC,IAAI,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAC9B,OAAO,EAAE,GAAG,EAAE,CACb,UAAU,CAAC,KAAK,IAAI,EAAE;YACrB,MAAM,MAAM,EAAE,CAAC;YACf,OAAO,OAAO,EAAE,CAAC;QAClB,CAAC,CAAC;KACH,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,OAAO,GAAmB,OAAO;IAY7D,MAAM,IAAI,GAAG,CAAC,OAAuB,EAAQ,EAAE;QAC9C,IAAI,OAAO,CAAC,SAAS;YAAE,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC;IACF,IAAI,WAAW,GAA6B,GAAG,EAAE,GAAE,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QAC7C,WAAW,GAAG,OAAO,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA8B,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;QAC1C,MAAM,OAAO,GAAI,OAAqD,EAAE,aAAa,CAAC;QACtF,IAAI,OAAO,EAAE,IAAI,KAAK,MAAM;YAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACzD,IAAI,OAAO,EAAE,IAAI,KAAK,UAAU;YAAE,KAAK,MAAM,OAAO,IAAI,QAAQ;gBAAE,KAAK,OAAO,EAAE,CAAC;IAClF,CAAC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,CAAC,KAAc,EAAQ,EAAE,CACtC,IAAI,CAAC;QACJ,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC/D,GAAG,CAAC,KAAK,YAAY,UAAU,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9F,CAAC,CAAC;IACJ,OAAO;QACN,IAAI;QACJ,IAAI,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO;YAC/B,IAAI,CAAC;gBACJ,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,KAAK,CAAC,KAAK,CAAC,CAAC;gBACb,MAAM,KAAK,CAAC;YACb,CAAC;QACF,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACpC,KAAK;QACL,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;QACvD,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC;KAC9C,CAAC;AACH,CAAC","sourcesContent":["// Host manager (Node only): runs a child host process and keeps at most one instance of it alive.\nimport { type ChildProcess, fork } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { unlinkSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport { connect, createServer } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ChordError } from \"./errors.ts\";\nimport { type IpcChannel, type IpcLink, ipcLink } from \"./link.ts\";\n\n/** Control messages share the IPC channel with chord frames: `{ chord2control: message }`. */\nexport type ControlMessage =\n\t| { readonly type: \"init\"; readonly value: unknown }\n\t| { readonly type: \"ready\" }\n\t| { readonly type: \"fatal\"; readonly message: string; readonly reason?: string }\n\t| { readonly type: \"shutdown\" }\n\t| {\n\t\t\treadonly type: \"status\";\n\t\t\treadonly activeCalls?: number;\n\t\t\treadonly viewers?: number;\n\t\t\treadonly durableTasks?: number;\n\t };\n\nexport interface Liveness {\n\t/** Started but not ready yet. */\n\treadonly booting: boolean;\n\treadonly activeCalls: number;\n\t/** Viewers the child reports (it counts heartbeating subscribers). */\n\treadonly viewers: number;\n\treadonly durableTasks: number;\n\t/** Milliseconds since the child last reported work (calls, viewers or tasks), or since it became ready. */\n\treadonly idleFor: number;\n}\n\nexport interface Exit {\n\treadonly code: number | null;\n\treadonly signal: string | null;\n}\n\n/** A running child: its link (for `host.accept`) and its exit. */\nexport interface Instance {\n\treadonly pid: number;\n\treadonly link: IpcLink;\n\treadonly exited: Promise<Exit>;\n\tsend(message: ControlMessage): void;\n\t/** Kills the child and everything it spawned. */\n\tkillTree(): void;\n\tonControl(listener: (message: ControlMessage) => void): void;\n}\n\nexport interface Driver {\n\tspawn(): Instance;\n}\n\nexport interface ForkDriverOptions {\n\t/** The child's entry module. */\n\treadonly entry: string;\n\treadonly args?: readonly string[];\n\treadonly env?: Readonly<Record<string, string>>;\n\t/** Node flags for the child, e.g. `[\"--import\", sourceResolver]` (default: none). */\n\treadonly execArgv?: readonly string[];\n\t/** Sent to the child as its first control message (`childControl().init`). */\n\treadonly init?: unknown;\n\t/**\n\t * A lock name: one live process per name, across managers and processes. The child acquires it as a kernel lock\n\t * (`childControl().lock()`, before opening its storage); a second child fails to start with `unavailable/locked`.\n\t */\n\treadonly lock?: string;\n\t/** Optional stdout/stderr tap. Streams are still mirrored to the parent's standard output. */\n\treadonly onOutput?: (stream: \"stdout\" | \"stderr\", chunk: Uint8Array) => void;\n\t/** Optional application metadata tap; control messages and chord frames keep their normal handling. */\n\treadonly onMessage?: (message: unknown) => void;\n}\n\n/**\n * A process lock owned by the kernel for the holder's lifetime: an abstract socket (Linux, Android) or a named pipe\n * (Windows) vanishes with its process, so there is no stale state to take over and no race between two would-be\n * owners. Other platforms need a socket file, removed on exit; a leftover one nobody accepts on is taken over.\n * The child acquires it (before opening storage), so the lock follows the process that owns the data.\n */\nexport async function processLock(name: string): Promise<() => Promise<void>> {\n\tconst key = createHash(\"sha256\").update(name).digest(\"hex\").slice(0, 32);\n\tconst file = process.platform !== \"win32\" && process.platform !== \"linux\" && process.platform !== \"android\";\n\tconst address =\n\t\tprocess.platform === \"win32\"\n\t\t\t? `\\\\\\\\.\\\\pipe\\\\chord2-${key}`\n\t\t\t: file\n\t\t\t\t? join(tmpdir(), `chord2-${key}.sock`)\n\t\t\t\t: `\\0chord2-${key}`;\n\tconst server = createServer((socket) => socket.end());\n\tfor (;;) {\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tserver.once(\"error\", reject);\n\t\t\t\tserver.listen(address, () => {\n\t\t\t\t\tserver.removeListener(\"error\", reject);\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t});\n\t\t\tbreak;\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code !== \"EADDRINUSE\") throw error;\n\t\t\tif (!file || (await accepting(address))) {\n\t\t\t\tthrow new ChordError(\"unavailable\", `lock ${name} is held by another process`, { reason: \"locked\" });\n\t\t\t}\n\t\t\tawait rm(address, { force: true });\n\t\t}\n\t}\n\tconst removeFile = (): void => {\n\t\ttry {\n\t\t\tunlinkSync(address);\n\t\t} catch {\n\t\t\t// Already gone.\n\t\t}\n\t};\n\tif (file) process.once(\"exit\", removeFile);\n\treturn () =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tif (file) process.removeListener(\"exit\", removeFile);\n\t\t\tserver.close((error) => (error ? reject(error) : resolve()));\n\t\t});\n}\n\nfunction accepting(address: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst socket = connect(address);\n\t\tsocket.once(\"connect\", () => {\n\t\t\tsocket.destroy();\n\t\t\tresolve(true);\n\t\t});\n\t\tsocket.once(\"error\", (error: NodeJS.ErrnoException) =>\n\t\t\tresolve(error.code !== \"ECONNREFUSED\" && error.code !== \"ENOENT\"),\n\t\t);\n\t});\n}\n\nconst LOCK_ENV = \"CHORD2_LOCK\";\n\nexport function forkDriver(options: ForkDriverOptions): Driver {\n\treturn {\n\t\tspawn() {\n\t\t\t// Detached: the child leads a process group, so the whole tree can be killed.\n\t\t\tconst child: ChildProcess = fork(options.entry, [...(options.args ?? [])], {\n\t\t\t\tenv: {\n\t\t\t\t\t...process.env,\n\t\t\t\t\t...options.env,\n\t\t\t\t\t...(options.lock === undefined ? {} : { [LOCK_ENV]: options.lock }),\n\t\t\t\t},\n\t\t\t\texecArgv: [...(options.execArgv ?? [])],\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", options.onOutput ? \"pipe\" : \"inherit\", options.onOutput ? \"pipe\" : \"inherit\", \"ipc\"],\n\t\t\t});\n\t\t\tif (options.onOutput) {\n\t\t\t\tfor (const stream of [\"stdout\", \"stderr\"] as const) {\n\t\t\t\t\tchild[stream]?.on(\"data\", (chunk: Buffer) => {\n\t\t\t\t\t\toptions.onOutput!(stream, chunk);\n\t\t\t\t\t\tprocess[stream].write(chunk);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst pid = child.pid ?? -1;\n\t\t\tconst killGroup = (): void => {\n\t\t\t\ttry {\n\t\t\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t\t\t} catch {\n\t\t\t\t\t// No group left.\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst exited = new Promise<Exit>((resolve) => {\n\t\t\t\tchild.once(\"exit\", (code, signal) => {\n\t\t\t\t\t// However the leader ended (graceful, crash, kill), nothing it spawned outlives it.\n\t\t\t\t\tkillGroup();\n\t\t\t\t\tresolve({ code, signal });\n\t\t\t\t});\n\t\t\t});\n\t\t\tconst listeners = new Set<(message: ControlMessage) => void>();\n\t\t\tchild.on(\"message\", (message) => {\n\t\t\t\toptions.onMessage?.(message);\n\t\t\t\tconst control = (message as { chord2control?: ControlMessage } | null)?.chord2control;\n\t\t\t\tif (control !== undefined) for (const listener of listeners) listener(control);\n\t\t\t});\n\t\t\tconst send = (message: ControlMessage): void => {\n\t\t\t\tif (child.connected) child.send({ chord2control: message });\n\t\t\t};\n\t\t\tif (options.init !== undefined) send({ type: \"init\", value: options.init });\n\t\t\treturn {\n\t\t\t\tpid,\n\t\t\t\tlink: ipcLink(child as unknown as IpcChannel),\n\t\t\t\texited,\n\t\t\t\tsend,\n\t\t\t\tkillTree: () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tchild.kill(\"SIGKILL\");\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tonControl: (listener) => listeners.add(listener),\n\t\t\t};\n\t\t},\n\t};\n}\n\nexport interface HostManagerOptions {\n\treadonly driver: Driver;\n\t/** Evaluated every `checkInterval` while running; `false` stops the child. */\n\treadonly keepAlive?: (liveness: Liveness) => boolean;\n\treadonly checkInterval?: number;\n\t/** Milliseconds a child may take to shut down before its process tree is killed (default 5000). */\n\treadonly grace?: number;\n\t/** Milliseconds to wait for `ready` (default 30000). */\n\treadonly readyTimeout?: number;\n\t/** The child exited on its own (crash, fatal) or after a stop. */\n\treadonly onExit?: (exit: Exit) => void;\n}\n\nexport interface HostManager {\n\treadonly status: \"stopped\" | \"starting\" | \"running\" | \"stopping\";\n\treadonly instance: Instance | undefined;\n\treadonly liveness: Liveness;\n\t/** Starts the child (or returns the running one) once it reported ready. Serialized with stop/replace. */\n\tstart(): Promise<Instance>;\n\t/** Asks the child to shut down, then kills its process tree after the grace period. */\n\tstop(): Promise<void>;\n\t/** Stop, wait for the exit, start. */\n\treplace(): Promise<Instance>;\n}\n\nexport function createHostManager(options: HostManagerOptions): HostManager {\n\tlet status: HostManager[\"status\"] = \"stopped\";\n\tlet instance: Instance | undefined;\n\tlet queue: Promise<unknown> = Promise.resolve();\n\tlet report = { activeCalls: 0, viewers: 0, durableTasks: 0 };\n\t/** When the child last became idle; undefined while it reports work. */\n\tlet idleSince: number | undefined;\n\tlet booting = false;\n\tlet timer: ReturnType<typeof setInterval> | undefined;\n\n\tconst serialized = <T>(step: () => Promise<T>): Promise<T> => {\n\t\tconst next = queue.then(step, step);\n\t\tqueue = next.catch(() => {});\n\t\treturn next;\n\t};\n\n\tconst liveness = (): Liveness => ({\n\t\tbooting,\n\t\t...report,\n\t\tidleFor: idleSince === undefined ? 0 : Date.now() - idleSince,\n\t});\n\n\tconst doStop = async (): Promise<void> => {\n\t\tconst current = instance;\n\t\tif (current === undefined) {\n\t\t\tstatus = \"stopped\";\n\t\t\tbooting = false;\n\t\t\treturn;\n\t\t}\n\t\tstatus = \"stopping\";\n\t\tif (timer !== undefined) clearInterval(timer);\n\t\tcurrent.send({ type: \"shutdown\" });\n\t\tlet graceTimer: ReturnType<typeof setTimeout> | undefined;\n\t\tconst graceful = await Promise.race([\n\t\t\tcurrent.exited.then(() => true),\n\t\t\tnew Promise<boolean>((resolve) => {\n\t\t\t\tgraceTimer = setTimeout(() => resolve(false), options.grace ?? 5000);\n\t\t\t}),\n\t\t]);\n\t\tclearTimeout(graceTimer);\n\t\tif (!graceful) current.killTree();\n\t\tawait current.exited;\n\t\tif (instance === current) instance = undefined;\n\t\tstatus = \"stopped\";\n\t};\n\n\tconst doStart = async (): Promise<Instance> => {\n\t\tif (instance !== undefined && status === \"running\") return instance;\n\t\tstatus = \"starting\";\n\t\tbooting = true;\n\t\treport = { activeCalls: 0, viewers: 0, durableTasks: 0 };\n\t\tlet current: Instance;\n\t\ttry {\n\t\t\tcurrent = options.driver.spawn();\n\t\t} catch (error) {\n\t\t\tstatus = \"stopped\";\n\t\t\tbooting = false;\n\t\t\tthrow error;\n\t\t}\n\t\tinstance = current;\n\t\tcurrent.exited.then((exit) => {\n\t\t\tif (instance === current) {\n\t\t\t\tinstance = undefined;\n\t\t\t\tstatus = \"stopped\";\n\t\t\t\tif (timer !== undefined) clearInterval(timer);\n\t\t\t}\n\t\t\toptions.onExit?.(exit);\n\t\t});\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst timeout = setTimeout(\n\t\t\t\t() => reject(new ChordError(\"unavailable\", \"child not ready in time\")),\n\t\t\t\toptions.readyTimeout ?? 30_000,\n\t\t\t);\n\t\t\tconst settle = (fn: () => void): void => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tfn();\n\t\t\t};\n\t\t\tcurrent.exited.then((exit) =>\n\t\t\t\tsettle(() =>\n\t\t\t\t\treject(new ChordError(\"unavailable\", `child exited before ready (${exit.code ?? exit.signal})`)),\n\t\t\t\t),\n\t\t\t);\n\t\t\tcurrent.onControl((message) => {\n\t\t\t\tif (message.type === \"ready\") settle(resolve);\n\t\t\t\telse if (message.type === \"fatal\")\n\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\treject(new ChordError(\"unavailable\", `child failed: ${message.message}`, { reason: message.reason })),\n\t\t\t\t\t);\n\t\t\t\telse if (message.type === \"status\") {\n\t\t\t\t\treport = {\n\t\t\t\t\t\tactiveCalls: message.activeCalls ?? 0,\n\t\t\t\t\t\tviewers: message.viewers ?? 0,\n\t\t\t\t\t\tdurableTasks: message.durableTasks ?? 0,\n\t\t\t\t\t};\n\t\t\t\t\t// The idle clock starts at the busy → idle transition, not at the last busy report.\n\t\t\t\t\tif (report.activeCalls + report.viewers + report.durableTasks > 0) idleSince = undefined;\n\t\t\t\t\telse idleSince ??= Date.now();\n\t\t\t\t}\n\t\t\t});\n\t\t}).catch(async (error: unknown) => {\n\t\t\tcurrent.killTree();\n\t\t\tawait current.exited;\n\t\t\tthrow error;\n\t\t});\n\t\tbooting = false;\n\t\tif (report.activeCalls + report.viewers + report.durableTasks === 0) idleSince ??= Date.now();\n\t\tstatus = \"running\";\n\t\tif (options.keepAlive !== undefined) {\n\t\t\tconst keepAlive = options.keepAlive;\n\t\t\ttimer = setInterval(() => {\n\t\t\t\tif (status === \"running\" && !keepAlive(liveness())) void serialized(doStop);\n\t\t\t}, options.checkInterval ?? 1000);\n\t\t\ttimer.unref?.();\n\t\t}\n\t\treturn current;\n\t};\n\n\treturn {\n\t\tget status() {\n\t\t\treturn status;\n\t\t},\n\t\tget instance() {\n\t\t\treturn instance;\n\t\t},\n\t\tget liveness() {\n\t\t\treturn liveness();\n\t\t},\n\t\tstart: () => serialized(doStart),\n\t\tstop: () => serialized(doStop),\n\t\treplace: () =>\n\t\t\tserialized(async () => {\n\t\t\t\tawait doStop();\n\t\t\t\treturn doStart();\n\t\t\t}),\n\t};\n}\n\n/** The child side of the control channel (call in the child's entry). */\nexport function childControl(channel: NodeJS.Process = process): {\n\treadonly init: Promise<unknown>;\n\t/**\n\t * Acquires the manager's lock (if one was configured) as a kernel lock held for this process's lifetime. Call it\n\t * before opening storage. Fails with `unavailable/locked`, also reported to the manager as a fatal start error.\n\t */\n\tlock(): Promise<void>;\n\tready(): void;\n\tfatal(error: unknown): void;\n\treport(status: { activeCalls?: number; viewers?: number; durableTasks?: number }): void;\n\tonShutdown(handler: () => void | Promise<void>): void;\n} {\n\tconst send = (message: ControlMessage): void => {\n\t\tif (channel.connected) channel.send?.({ chord2control: message });\n\t};\n\tlet resolveInit: (value: unknown) => void = () => {};\n\tconst init = new Promise<unknown>((resolve) => {\n\t\tresolveInit = resolve;\n\t});\n\tconst shutdown = new Set<() => void | Promise<void>>();\n\tchannel.on(\"message\", (message: unknown) => {\n\t\tconst control = (message as { chord2control?: ControlMessage } | null)?.chord2control;\n\t\tif (control?.type === \"init\") resolveInit(control.value);\n\t\tif (control?.type === \"shutdown\") for (const handler of shutdown) void handler();\n\t});\n\tconst fatal = (error: unknown): void =>\n\t\tsend({\n\t\t\ttype: \"fatal\",\n\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t...(error instanceof ChordError && error.reason !== undefined ? { reason: error.reason } : {}),\n\t\t});\n\treturn {\n\t\tinit,\n\t\tlock: async () => {\n\t\t\tconst name = process.env[LOCK_ENV];\n\t\t\tif (name === undefined) return;\n\t\t\ttry {\n\t\t\t\tawait processLock(name);\n\t\t\t} catch (error) {\n\t\t\t\tfatal(error);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tready: () => send({ type: \"ready\" }),\n\t\tfatal,\n\t\treport: (status) => send({ type: \"status\", ...status }),\n\t\tonShutdown: (handler) => shutdown.add(handler),\n\t};\n}\n"]}
package/dist/node.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export type { FacetBundleArtifactLoaderOptions, FacetBundleExternalResolver, FacetBundleLoaderOptions, } from "./node/bundle-loader.ts";
2
- export { createFacetBundleArtifactLoader, createFacetBundleLoader, readFacetBundleArtifact, readFacetBundleManifest, } from "./node/bundle-loader.ts";
3
- export type { FacetBundleArtifact, FacetBundleEntry, FacetBundleManifest, FacetBundlePlugin, } from "./node/manifest.ts";
4
- export { FACET_BUNDLE_ARTIFACT_FORMAT, FACET_BUNDLE_ARTIFACT_FORMAT_VERSION, FACET_BUNDLE_FORMAT, FACET_BUNDLE_FORMAT_VERSION, FACET_BUNDLE_MANIFEST_FILE, } from "./node/manifest.ts";
1
+ /** Node-only parts of Chord: child host processes, their manager, and the facet module loader. */
2
+ export { nodeLoader } from "./loader.ts";
3
+ export { type ControlMessage, childControl, createHostManager, type Driver, type Exit, type ForkDriverOptions, forkDriver, type HostManager, type HostManagerOptions, type Instance, type Liveness, processLock, } from "./manager.ts";
5
4
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,gCAAgC,EAChC,2BAA2B,EAC3B,wBAAwB,GACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACN,+BAA+B,EAC/B,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,GACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACN,4BAA4B,EAC5B,oCAAoC,EACpC,mBAAmB,EACnB,2BAA2B,EAC3B,0BAA0B,GAC1B,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACN,KAAK,cAAc,EACnB,YAAY,EACZ,iBAAiB,EACjB,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,WAAW,GACX,MAAM,cAAc,CAAC"}