@dcasegr/skein 0.0.0-stage → 0.1.2

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 (176) hide show
  1. package/README.md +525 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +782 -0
  89. package/dist/host/router.js +2411 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. package/wasm/wire-probe.wasm +0 -0
@@ -0,0 +1,603 @@
1
+ // One instance's kernel as the router holds it (#33): a `skein-kernel serve`
2
+ // process spoken to over its stdin/stdout — length-prefixed dag-cbor frames
3
+ // (kernel-zig/src/ipc.zig). This is the kernel's whole surface:
4
+ //
5
+ // the router asks tip · get · put · has · putblock · restore · append · genesis · boxes · byEnvelope ·
6
+ // admit (the one call in that writes: a request as received, #68, a feed's or a
7
+ // proof's event) · answer (#66: wait on the thread a request entry launched; its answer once it
8
+ // comes to rest, or its state at the wait's bound) · call (#40: a program's function
9
+ // over the state, no entry, no writes: host-side reads only) · idle · start · running
10
+ // the kernel asks wallet (a BRC-100 wire frame → its answer: the signer, #67; #157: nothing
11
+ // else — the in-step authfetch and its `http` frame are gone)
12
+ // the host chain headers {raws} · chain-import {set, root} · chain (#156: the header chain the
13
+ // host keeps once, in the kernel process — a serve instance's in its own store —
14
+ // → {root, height, hash, added?, replaced?} | null)
15
+ // the kernel tells emit (#70: a signed message for the host to carry out — {message, body,
16
+ // transport, address}, a `local` provider's or the libp2p node's, the answer
17
+ // coming back as an entry; #65: a broadcast event, transport `event`) · stop
18
+ //
19
+ // Nothing here keeps an instance's time (#69, #126): a step's deadline and a
20
+ // shell's sleep are `deadline` events the host's waker keeps.
21
+ //
22
+ // `Kernel` offers `store` (get/put/log), `admit`, `answer`, `invoke` (the call),
23
+ // `boxes`, `idle`. Log lines (stderr) go to `log`.
24
+ //
25
+ // Two ways to run kernels (#158): `Kernel` is one `skein-kernel serve`
26
+ // process per instance (a SQLite file or an LMDB set); `KernelHost` is the one
27
+ // `skein-kernel host` process of a host whose instances are sets of a RocksDB
28
+ // database (SKEIN_STORE_ROCKSDB), every frame tagged with the instance's
29
+ // handle `h`, and `HostedKernel` its per-instance view with the same surface
30
+ // (`KernelSurface`): hydrate = `open`, stop = `close`. The router holds either.
31
+ // The settled split (2026-10-31): a third, `WasmKernel` (wasm-kernel.ts) — the
32
+ // kernel as a wasm module the host LOADS, one per active instance on a worker
33
+ // thread of this process, over a set of the host's one SQLite database
34
+ // (`sqlite:<path>`); what the npm package runs. These two are the native host,
35
+ // kept for file, LMDB and RocksDB stores.
36
+ import { spawn } from "node:child_process";
37
+ import { mkdirSync } from "node:fs";
38
+ import { dirname, join, resolve as resolvePath } from "node:path";
39
+ import { fileURLToPath } from "node:url";
40
+ import { WalletWireProcessor } from "@bsv/sdk";
41
+ import { decode, encode } from "../runtime/cid.js";
42
+ import { parseLmdbSpec, parseRocksSpec } from "../runtime/index-store.js";
43
+ import { now as clockNow } from "./clock.js";
44
+ import { NotFound, Rejected } from "../runtime/store.js";
45
+ const ROOT = resolvePath(dirname(fileURLToPath(import.meta.url)), "../..");
46
+ /** The kernel binary: $SKEIN_KERNEL_BIN, else kernel-zig's release build. */
47
+ export const KERNEL_BIN = process.env.SKEIN_KERNEL_BIN || join(ROOT, "kernel-zig/zig-out/bin/skein-kernel");
48
+ export { DoorAnswered, doorAnswer } from "./door-answer.js";
49
+ import { DoorAnswered, doorAnswer } from "./door-answer.js";
50
+ /**
51
+ * What the router holds of an instance's kernel, whichever process runs it (#158): the frames
52
+ * above, as calls. `call` is the transport's; the rest is built on it.
53
+ */
54
+ export class KernelSurface {
55
+ /** Pending calls in both directions: the router stops a kernel only when this is 0. */
56
+ busy = 0;
57
+ last = Date.now();
58
+ gone = false;
59
+ store = {
60
+ get: async (cid) => { const v = await this.call("get", cid); if (v === null || v === undefined)
61
+ throw new NotFound(cid.toString()); return v; },
62
+ put: async (value) => await this.call("put", value),
63
+ has: async (cid) => (await this.call("get", cid)) != null,
64
+ log: {
65
+ tip: async () => await this.call("tip") ?? undefined,
66
+ byEnvelope: async (envelope) => await this.call("byEnvelope", envelope) ?? undefined,
67
+ append: async (entry) => await this.call("append", entry),
68
+ },
69
+ };
70
+ /** Any block, whatever its codec. */
71
+ async hasBlock(cid) { return await this.call("has", cid) === true; }
72
+ /** A block minted elsewhere (git-raw, raw, dag-cbor, bitcoin-*), hash-checked by the kernel (#4: the loader's pre-fill). */
73
+ async putBlock(cid, bytes) { await this.call("putblock", { cid, bytes }); }
74
+ /** Adopt a checkpoint's state record (its blocks put first) as this empty store's state (#4). */
75
+ async restore(state) { await this.call("restore", state); }
76
+ async boxes() { return await this.call("boxes"); }
77
+ /** The dispatch table as it stands (#77): its chain's tip (the change key; null before the genesis is processed) and its rows. */
78
+ async dispatch() { return await this.call("dispatch"); }
79
+ async genesis() { return await this.call("genesis"); }
80
+ /**
81
+ * Admit an arrival (#163): its entry's body and its stamp (`time`, the host's clock at
82
+ * admission; default now) — the kernel appends it, assigning `prev` and `n` from its own tip.
83
+ * A request's record beside it (#121: the kernel puts it, after the door), an event's likewise
84
+ * (#164: through the door when its box's route names kernel.beef), else put first. A request
85
+ * or event the door turns away or answers writes no entry (#143): DoorAnswered. The entry's CID.
86
+ */
87
+ async admit(body, records = {}, time = clockNow()) {
88
+ const r = await this.call("admit", { entry: { ...body, time }, ...(records.body ? { body: records.body } : {}), ...(records.request ? { request: records.request } : {}), ...(records.event ? { event: records.event } : {}) });
89
+ const answered = r?.answered;
90
+ if (answered && typeof answered === "object" && !(r instanceof Uint8Array))
91
+ throw new DoorAnswered(doorAnswer(answered));
92
+ return r;
93
+ }
94
+ /**
95
+ * The kernel's `call` (#40): a program's function (by CID, or a genesis
96
+ * program's name) over the current state — no entry, no writes, the signer
97
+ * and http answered but not recorded; fuel-limited (`callFuelLimit`). The
98
+ * result is what the program wrote to stdout; `fuel` is reported either way
99
+ * (the router's ledger). On a hosted kernel (#158) it reads the state as
100
+ * committed, beside a step that may be running.
101
+ */
102
+ async invoke(program, fn, arg, o = {}) {
103
+ return await this.call("call", { program, fn, arg, ...(o.caller ? { caller: o.caller } : {}), now: o.now ?? Date.now() });
104
+ }
105
+ /**
106
+ * A synchronous client's wait (#66): the answer of the thread a request
107
+ * entry launched (the middleware stepped on it), once that thread has come
108
+ * to rest for good — finished (`answer`: the middleware's dag-cbor answer)
109
+ * or errored — or, `waitMs` on, its state as it stands (`waiting`; or
110
+ * `pending`: the entry not processed yet). The kernel keeps the bound, so
111
+ * this always settles.
112
+ */
113
+ async answer(entry, waitMs) {
114
+ return await this.call("answer", { entry, wait: Math.max(0, Math.floor(waitMs)) });
115
+ }
116
+ async idle() { await this.call("idle"); }
117
+ async start() { await this.call("start"); }
118
+ /** #156: a run of headers (parents first) into the host chain this instance reads; its record and tip after. */
119
+ async chainHeaders(raws) { return tipOf(await this.call("headers", { raws })); }
120
+ /** #156: an image's chain/headers (the tree `root`, held by this instance) into the empty host chain. */
121
+ async chainImport(root) { return tipOf(await this.call("chain-import", { set: this.o.handle, root })); }
122
+ /** #156: the host chain as it stands, or undefined (empty). */
123
+ async chainTip() { const v = await this.call("chain"); return v == null ? undefined : tipOf(v); }
124
+ async tip() { return this.store.log.tip(); }
125
+ }
126
+ function tipOf(v) {
127
+ const o = v;
128
+ return { root: o.root, height: Number(o.height), hash: o.hash, ...(o.added !== undefined ? { added: Number(o.added) } : {}), ...(o.replaced !== undefined ? { replaced: Number(o.replaced) } : {}) };
129
+ }
130
+ /**
131
+ * The kernel's own request (`wallet`, `http`) or notice (`emit`, `stop`), answered for instance `o`
132
+ * through `answer`/`fail` — the same for a serve process and a hosted instance.
133
+ */
134
+ async function kernelRequest(k, wire, f, answer, fail) {
135
+ const o = k.o;
136
+ k.busy++;
137
+ k.last = Date.now();
138
+ try {
139
+ switch (f.op) {
140
+ case "wallet":
141
+ if (!wire)
142
+ throw new Error("no wallet");
143
+ return answer(Uint8Array.from(await wire.transmitToWallet([...f.v])));
144
+ case "emit":
145
+ // A signed message (#70) or an event (#65) to carry out, its step committed: nothing goes back.
146
+ o.emit?.(f.v);
147
+ return;
148
+ case "stop": return;
149
+ }
150
+ }
151
+ catch (e) {
152
+ if (f.id !== undefined)
153
+ fail(e);
154
+ else
155
+ o.log?.(`router: ${String(f.op)}: ${e.message}`);
156
+ }
157
+ finally {
158
+ k.busy--;
159
+ k.last = Date.now();
160
+ }
161
+ }
162
+ /** Length-prefixed dag-cbor frames out of a byte stream (ipc.zig). */
163
+ class FrameReader {
164
+ buf = Buffer.alloc(0);
165
+ onFrame;
166
+ constructor(onFrame) { this.onFrame = onFrame; }
167
+ push(d) {
168
+ this.buf = Buffer.concat([this.buf, d]);
169
+ while (this.buf.length >= 4) {
170
+ const n = this.buf.readUInt32BE(0);
171
+ if (this.buf.length < 4 + n)
172
+ break;
173
+ const f = decode(this.buf.subarray(4, 4 + n));
174
+ this.buf = this.buf.subarray(4 + n);
175
+ this.onFrame(f);
176
+ }
177
+ }
178
+ }
179
+ function frameBytes(v) {
180
+ const body = encode(v).bytes;
181
+ const h = Buffer.alloc(4);
182
+ h.writeUInt32BE(body.length);
183
+ return Buffer.concat([h, body]);
184
+ }
185
+ /** Lines of a child's stderr, whole. */
186
+ function lines(stream, each) {
187
+ let partial = "";
188
+ stream.on("data", (d) => {
189
+ const s = partial + d.toString();
190
+ const ls = s.split("\n");
191
+ partial = ls.pop();
192
+ for (const l of ls)
193
+ each(l);
194
+ });
195
+ }
196
+ export class Kernel extends KernelSurface {
197
+ o;
198
+ proc;
199
+ nextId = 1;
200
+ waiting = new Map();
201
+ reader;
202
+ wire;
203
+ done;
204
+ constructor(o) {
205
+ super();
206
+ this.o = o;
207
+ if (o.wallet)
208
+ this.wire = new WalletWireProcessor(o.wallet);
209
+ if (parseRocksSpec(o.db))
210
+ throw new Error(`kernel ${o.handle}: a RocksDB set (${o.db}) runs in the host's one kernel process (KernelHost), not in serve`);
211
+ // #96: an LMDB set's kernel gets SKEIN_STORE (its set is its handle); a file's gets SKEIN_DB and never a SKEIN_STORE.
212
+ const env = { ...process.env, ...o.env, SKEIN_HANDLE: `${o.handle}@${o.domain}` };
213
+ const lmdb = parseLmdbSpec(o.db);
214
+ if (lmdb && lmdb.set !== o.handle)
215
+ throw new Error(`kernel ${o.handle}: its LMDB set is its handle, not ${lmdb.set}`);
216
+ if (lmdb) {
217
+ env.SKEIN_STORE = lmdb.dir;
218
+ delete env.SKEIN_DB;
219
+ }
220
+ else {
221
+ env.SKEIN_DB = o.db;
222
+ delete env.SKEIN_STORE;
223
+ }
224
+ this.proc = spawn(o.command ?? KERNEL_BIN, ["serve"], {
225
+ env,
226
+ stdio: ["pipe", "pipe", "pipe"],
227
+ });
228
+ lines(this.proc.stderr, (l) => o.log?.(l));
229
+ this.reader = new FrameReader((f) => void this.onFrame(f));
230
+ this.proc.stdout.on("data", (d) => this.reader.push(d));
231
+ this.proc.stdin.on("error", () => { }); // EPIPE once it is gone
232
+ this.done = new Promise((resolve) => this.proc.once("close", (code, signal) => {
233
+ this.gone = true;
234
+ for (const w of this.waiting.values())
235
+ w.reject(new Error(`kernel ${o.handle}: exited (${signal ?? code})`));
236
+ this.waiting.clear();
237
+ o.exited?.(code, signal);
238
+ resolve();
239
+ }));
240
+ this.proc.on("error", (e) => o.log?.(`kernel: ${e.message}`));
241
+ }
242
+ get pid() { return this.proc.pid; }
243
+ // ---------------------------------------------------------------- frames
244
+ write(v) {
245
+ if (this.gone)
246
+ throw new Error(`kernel ${this.o.handle}: not running`);
247
+ this.proc.stdin.write(frameBytes(v));
248
+ }
249
+ call(op, v) {
250
+ const id = this.nextId++;
251
+ this.busy++;
252
+ this.last = Date.now();
253
+ return new Promise((resolve, reject) => {
254
+ this.waiting.set(id, { resolve, reject });
255
+ try {
256
+ this.write({ id, op, v });
257
+ }
258
+ catch (e) {
259
+ this.waiting.delete(id);
260
+ reject(e);
261
+ }
262
+ }).finally(() => { this.busy--; this.last = Date.now(); });
263
+ }
264
+ async onFrame(f) {
265
+ if (typeof f.re === "number") {
266
+ const w = this.waiting.get(f.re);
267
+ if (!w)
268
+ return;
269
+ this.waiting.delete(f.re);
270
+ settle(w, f);
271
+ return;
272
+ }
273
+ // A process that is gone is reported by its exit; any other write failure (a frame that does not encode) is logged.
274
+ const unwritten = (x) => { if (!this.gone)
275
+ this.o.log?.(`the answer to the kernel's ${String(f.op)} frame not written: ${x.message ?? String(x)}`); };
276
+ await kernelRequest(this, this.wire, f, (ok) => { try {
277
+ this.write({ re: f.id, ok });
278
+ }
279
+ catch (x) {
280
+ unwritten(x);
281
+ } }, (e) => { try {
282
+ this.write({ re: f.id, error: e.message ?? String(e) });
283
+ }
284
+ catch (x) {
285
+ unwritten(x);
286
+ } });
287
+ }
288
+ /** Print the ready line (`skein runtime <identity> (<handle>@<domain>) · pid …`). */
289
+ async running(identity) { this.write({ op: "running", v: identity }); }
290
+ /** Close the channel; the kernel stops at EOF. SIGTERM after `killAfterMs`, SIGKILL after twice that. */
291
+ async stop(killAfterMs = 10_000) {
292
+ if (this.gone)
293
+ return;
294
+ this.proc.stdin.end();
295
+ const t = setTimeout(() => this.proc.kill("SIGTERM"), killAfterMs);
296
+ const k = setTimeout(() => this.proc.kill("SIGKILL"), 2 * killAfterMs);
297
+ await this.done;
298
+ clearTimeout(t);
299
+ clearTimeout(k);
300
+ }
301
+ exited() { return this.done; }
302
+ }
303
+ function settle(w, f) {
304
+ if (typeof f.rejected === "string")
305
+ w.reject(new Rejected(f.rejected, String(f.error)));
306
+ else if (typeof f.error === "string")
307
+ w.reject(new Error(f.error));
308
+ else
309
+ w.resolve(f.ok);
310
+ }
311
+ /**
312
+ * The host's one `skein-kernel host` process over the RocksDB database at `dir` (#158): every
313
+ * instance of the host in it, each a `HostedKernel`. One per database per node process
314
+ * (`KernelHost.acquire`, counted: the routers and one-shot boots of a process share it; the last
315
+ * `release` stops it). The process is started at acquire; if it dies, every instance's view is
316
+ * gone (their `exited` called) and the next `open` starts it again (the router hydrates on demand,
317
+ * replay at hydrate as for serve).
318
+ */
319
+ export class KernelHost {
320
+ static all = new Map();
321
+ /** The process for the database at `dir`, started if this node process has none. */
322
+ static acquire(dir, o = {}) {
323
+ let h = KernelHost.all.get(dir);
324
+ if (!h) {
325
+ h = new KernelHost(dir, o);
326
+ KernelHost.all.set(dir, h);
327
+ }
328
+ h.holders++;
329
+ h.ensure();
330
+ return h;
331
+ }
332
+ /** Whether this node process runs the kernel process for `dir` (stores.ts waits for its socket then). */
333
+ static running(dir) { const h = KernelHost.all.get(dir); return !!h?.proc && !h.dead; }
334
+ dir;
335
+ o;
336
+ proc;
337
+ holders = 0;
338
+ nextId = 1;
339
+ waiting = new Map();
340
+ views = new Map();
341
+ /** A handle's close in flight: its next open waits for it. */
342
+ closing = new Map();
343
+ done;
344
+ dead = false;
345
+ /** #156: the last `headers` runs asked for (their answers shared). */
346
+ recent = new Map();
347
+ constructor(dir, o) { this.dir = dir; this.o = o; }
348
+ ensure() {
349
+ if (this.proc && !this.dead)
350
+ return;
351
+ mkdirSync(this.dir, { recursive: true });
352
+ const proc = spawn(this.o.command ?? KERNEL_BIN, ["host"], {
353
+ env: { ...process.env, ...this.o.env, SKEIN_STORE_ROCKSDB: this.dir },
354
+ stdio: ["pipe", "pipe", "pipe"],
355
+ });
356
+ this.proc = proc;
357
+ this.dead = false;
358
+ lines(proc.stderr, (l) => {
359
+ // An instance's line: `@<handle> <line>` (host.zig sayFor) → that instance's log.
360
+ const m = /^@(\S+) (.*)$/s.exec(l);
361
+ const v = m ? this.views.get(m[1]) : undefined;
362
+ if (v)
363
+ v.o.log?.(m[2]);
364
+ else
365
+ this.o.log?.(l);
366
+ });
367
+ const reader = new FrameReader((f) => void this.onFrame(f));
368
+ proc.stdout.on("data", (d) => reader.push(d));
369
+ proc.stdin.on("error", () => { });
370
+ this.done = new Promise((resolve) => proc.once("close", (code, signal) => {
371
+ this.dead = true;
372
+ if (this.proc === proc)
373
+ this.proc = undefined;
374
+ if (code !== 0)
375
+ this.o.log?.(`skein-kernel host exited (${signal ?? `code ${code}`})`);
376
+ for (const w of this.waiting.values())
377
+ w.reject(new Error(`skein-kernel host: exited (${signal ?? code})`));
378
+ this.waiting.clear();
379
+ const views = [...this.views.values()];
380
+ this.views.clear();
381
+ this.closing.clear();
382
+ for (const v of views)
383
+ v.lost(code, signal);
384
+ resolve();
385
+ }));
386
+ proc.on("error", (e) => this.o.log?.(`skein-kernel host: ${e.message}`));
387
+ }
388
+ write(v) {
389
+ if (!this.proc || this.dead)
390
+ throw new Error("skein-kernel host: not running");
391
+ this.proc.stdin.write(frameBytes(v));
392
+ }
393
+ /** A frame to the process (`h`: an instance's, else a host-level op) and its answer. */
394
+ request(h, op, v) {
395
+ const id = this.nextId++;
396
+ return new Promise((resolve, reject) => {
397
+ this.waiting.set(id, { resolve, reject });
398
+ try {
399
+ this.write({ id, op, ...(h !== undefined ? { h } : {}), v });
400
+ }
401
+ catch (e) {
402
+ this.waiting.delete(id);
403
+ reject(e);
404
+ }
405
+ });
406
+ }
407
+ /** A frame that wants no answer. */
408
+ tell(h, op, v) { this.write({ op, h, v }); }
409
+ async onFrame(f) {
410
+ if (typeof f.re === "number") {
411
+ const w = this.waiting.get(f.re);
412
+ if (!w)
413
+ return;
414
+ this.waiting.delete(f.re);
415
+ settle(w, f);
416
+ return;
417
+ }
418
+ const v = typeof f.h === "string" ? this.views.get(f.h) : undefined;
419
+ const unwritten = (x) => { if (!this.dead)
420
+ this.o.log?.(`the answer to the kernel's ${String(f.op)} frame not written: ${x.message ?? String(x)}`); };
421
+ const answer = (ok) => { try {
422
+ this.write({ re: f.id, ...(typeof f.h === "string" ? { h: f.h } : {}), ok });
423
+ }
424
+ catch (x) {
425
+ unwritten(x);
426
+ } };
427
+ const fail = (e) => { try {
428
+ this.write({ re: f.id, ...(typeof f.h === "string" ? { h: f.h } : {}), error: e.message ?? String(e) });
429
+ }
430
+ catch (x) {
431
+ unwritten(x);
432
+ } };
433
+ if (!v) {
434
+ if (f.op === "stop")
435
+ return;
436
+ if (f.id !== undefined)
437
+ fail(new Error(`no instance ${String(f.h)} open here`));
438
+ return;
439
+ }
440
+ await v.onKernelFrame(f, answer, fail);
441
+ }
442
+ /** Hydrate an instance (#158 `open`): its set opened in the process, its runtime built (replay at hydrate). */
443
+ async open(o) {
444
+ const p = parseRocksSpec(o.db);
445
+ if (!p)
446
+ throw new Error(`kernel ${o.handle}: ${o.db} is not a RocksDB set`);
447
+ if (p.set !== o.handle)
448
+ throw new Error(`kernel ${o.handle}: its RocksDB set is its handle, not ${p.set}`);
449
+ if (p.dir !== this.dir)
450
+ throw new Error(`kernel ${o.handle}: its database is ${p.dir}, this process's ${this.dir}`);
451
+ await this.closing.get(o.handle)?.catch(() => { });
452
+ if (this.views.has(o.handle))
453
+ throw new Error(`kernel ${o.handle}: open already`);
454
+ this.ensure();
455
+ const v = new HostedKernel(this, o);
456
+ this.views.set(o.handle, v);
457
+ try {
458
+ await this.request(o.handle, "open", { domain: o.domain, db: o.db });
459
+ }
460
+ catch (e) {
461
+ if (this.views.get(o.handle) === v)
462
+ this.views.delete(o.handle);
463
+ v.lost(null, null, true);
464
+ throw e;
465
+ }
466
+ return v;
467
+ }
468
+ /** Dehydrate (#158 `close`): after its inbox drains, its runtime freed. */
469
+ async close(v) {
470
+ if (this.views.get(v.o.handle) !== v)
471
+ return;
472
+ const p = this.request(v.o.handle, "close");
473
+ this.closing.set(v.o.handle, p);
474
+ try {
475
+ await p;
476
+ }
477
+ finally {
478
+ if (this.closing.get(v.o.handle) === p)
479
+ this.closing.delete(v.o.handle);
480
+ if (this.views.get(v.o.handle) === v)
481
+ this.views.delete(v.o.handle);
482
+ }
483
+ }
484
+ /** The instances open in the process (#158 `instances`). */
485
+ async instances() { return await this.request(undefined, "instances"); }
486
+ /** The host reads (#158): a block by CID — through `set`'s gate if named, else whichever set holds it; a set's committed state pointer. */
487
+ async get(cid, set) { return await this.hostRead("get", cid, set) ?? undefined; }
488
+ async has(cid, set) { return await this.hostRead("has", cid, set) === true; }
489
+ /** A host read, through `set`'s gate when one is named. */
490
+ hostRead(op, cid, set) {
491
+ const id = this.nextId++;
492
+ return new Promise((resolve, reject) => {
493
+ this.waiting.set(id, { resolve, reject });
494
+ try {
495
+ this.write({ id, op, ...(set !== undefined ? { set } : {}), v: cid });
496
+ }
497
+ catch (e) {
498
+ this.waiting.delete(id);
499
+ reject(e);
500
+ }
501
+ });
502
+ }
503
+ async state(set) { return await this.request(set, "state") ?? undefined; }
504
+ /**
505
+ * #156: a run of headers into the host chain (the frame `headers`, run in order on the process's
506
+ * chain thread). The same run asked for again while it is in flight or just answered (every
507
+ * instance subscribed to one feed asks for the same SSE event) is one frame: its answer shared.
508
+ */
509
+ chainHeaders(raws) {
510
+ const key = raws.map((r) => Buffer.from(r).toString("hex")).join("");
511
+ const had = this.recent.get(key);
512
+ if (had)
513
+ return had;
514
+ const p = this.request(undefined, "headers", { raws }).then(tipOf);
515
+ this.recent.set(key, p);
516
+ p.catch(() => this.recent.delete(key));
517
+ while (this.recent.size > 64)
518
+ this.recent.delete(this.recent.keys().next().value);
519
+ return p;
520
+ }
521
+ /** #156: an image's chain/headers (the tree `root`, whose blocks the set `set` holds) into the empty host chain. */
522
+ async chainImport(set, root) { return tipOf(await this.request(undefined, "chain-import", { set, root })); }
523
+ /** #156: the host chain as it stands, or undefined (empty). */
524
+ async chainTip() { const v = await this.request(undefined, "chain"); return v == null ? undefined : tipOf(v); }
525
+ /** One holder less; the last one's release stops the process (every instance closed, the database flushed). */
526
+ async release() {
527
+ if (--this.holders > 0)
528
+ return;
529
+ KernelHost.all.delete(this.dir);
530
+ await this.shutdown();
531
+ }
532
+ /** `stop`: the process finishes what runs, closes every instance and the database, exits. */
533
+ async shutdown(killAfterMs = 30_000) {
534
+ const proc = this.proc;
535
+ if (!proc || this.dead)
536
+ return;
537
+ const t = setTimeout(() => proc.kill("SIGTERM"), killAfterMs);
538
+ const k = setTimeout(() => proc.kill("SIGKILL"), 2 * killAfterMs);
539
+ try {
540
+ await this.request(undefined, "stop");
541
+ }
542
+ catch { /* it exited */ }
543
+ proc.stdin.end();
544
+ await this.done;
545
+ clearTimeout(t);
546
+ clearTimeout(k);
547
+ }
548
+ /** Until the process has exited. */
549
+ exited() { return this.done ?? Promise.resolve(); }
550
+ }
551
+ /** One instance of the host's one kernel process (#158): the `Kernel` surface over frames tagged `h`. */
552
+ export class HostedKernel extends KernelSurface {
553
+ o;
554
+ host;
555
+ wire;
556
+ resolveDone;
557
+ done = new Promise((r) => { this.resolveDone = r; });
558
+ constructor(host, o) {
559
+ super();
560
+ this.host = host;
561
+ this.o = o;
562
+ if (o.wallet)
563
+ this.wire = new WalletWireProcessor(o.wallet);
564
+ }
565
+ get pid() { return this.host.proc?.pid; }
566
+ call(op, v) {
567
+ if (this.gone)
568
+ return Promise.reject(new Error(`kernel ${this.o.handle}: not running`));
569
+ this.busy++;
570
+ this.last = Date.now();
571
+ return this.host.request(this.o.handle, op, v).finally(() => { this.busy--; this.last = Date.now(); });
572
+ }
573
+ async onKernelFrame(f, answer, fail) {
574
+ await kernelRequest(this, this.wire, f, answer, fail);
575
+ }
576
+ async running(identity) { this.host.tell(this.o.handle, "running", identity); }
577
+ /** #156: the host chain is the process's, not the instance's: its host-level frames. */
578
+ chainHeaders(raws) { return this.host.chainHeaders(raws); }
579
+ chainImport(root) { return this.host.chainImport(this.o.handle, root); }
580
+ chainTip() { return this.host.chainTip(); }
581
+ /** Close the instance in the process (its inbox drained first); the process keeps running. */
582
+ async stop() {
583
+ if (this.gone)
584
+ return;
585
+ try {
586
+ await this.host.close(this);
587
+ }
588
+ catch (e) {
589
+ this.o.log?.(`kernel: close: ${e.message}`);
590
+ }
591
+ this.lost(0, null);
592
+ }
593
+ /** Gone: closed, or the process exited (code/signal), or its open failed (`quiet`: no exited). */
594
+ lost(code, signal, quiet = false) {
595
+ if (this.gone)
596
+ return;
597
+ this.gone = true;
598
+ if (!quiet)
599
+ this.o.exited?.(code, signal);
600
+ this.resolveDone();
601
+ }
602
+ exited() { return this.done; }
603
+ }
@@ -0,0 +1,65 @@
1
+ import type { Store } from "../runtime/store.ts";
2
+ /** A window's bounds (ms): a second, a day (subscriptions.zig LIVENESS_MIN_MS / _MAX_MS). */
3
+ export declare const LIVENESS_MIN_MS = 1000;
4
+ export declare const LIVENESS_MAX_MS = 86400000;
5
+ /** One app's liveness for a topic: how long a beat stays live (ms). */
6
+ export interface Live {
7
+ app: string;
8
+ topic: string;
9
+ window: number;
10
+ }
11
+ export type LivenessEvent = {
12
+ event: "liveness";
13
+ live: Live;
14
+ } | {
15
+ event: "unliveness";
16
+ app: string;
17
+ topic: string;
18
+ };
19
+ /** A `liveness` / `unliveness` event record, read (subscriptions.zig livenessEventOf); or why it is not one this host follows. */
20
+ export declare function livenessEvent(rec: Record<string, unknown>): LivenessEvent | {
21
+ refused: string;
22
+ };
23
+ /** Fold liveness events in order: a liveness replaces the app's own on the topic, an unliveness removes it. */
24
+ export declare function foldLiveness(events: Iterable<LivenessEvent>, into?: Live[]): Live[];
25
+ /** The liveness standing, folded from the store's log as the subscriptions and beacons are (p2p.ts subscriptionsOf). Read only. */
26
+ export declare function livenessOf(store: Pick<Store, "get" | "chains" | "edges">): Promise<Live[]>;
27
+ /** A beat kept: the sender's identity key (hex), the beat's time, its body, the peer that published it (peer ID). */
28
+ export interface LiveBeat {
29
+ sender: string;
30
+ at: number;
31
+ body: Uint8Array;
32
+ from: string;
33
+ }
34
+ /** A message's verdict for GossipSub: a verified beat is accepted (forwarded), a bad one rejected, a stale one ignored. */
35
+ export type LiveVerdict = "accept" | "reject" | "ignore";
36
+ /** The tool's state: per instance, the apps' liveness and the beats kept per topic (in memory). */
37
+ export declare class Liveness {
38
+ private lives;
39
+ /** handle → topic → sender → the latest beat. */
40
+ private beats;
41
+ private readonly now;
42
+ /** Told each beat newly kept (shruggr/skein#150: the overlay's sync reads a peer's tip anchor from it). */
43
+ kept?: (handle: string, topic: string, beat: LiveBeat) => void;
44
+ constructor(now?: () => number);
45
+ /** `handle`'s apps keep this liveness now: the beats of topics none keeps any more are dropped. */
46
+ set(handle: string, lives: Live[]): void;
47
+ /** The topics `handle`'s apps keep liveness for (each once). */
48
+ topics(handle: string): string[];
49
+ /** Whether an app of `handle` keeps liveness for `topic`. */
50
+ takes(handle: string, topic: string): boolean;
51
+ /** The longest window on `topic` among `handle`'s apps (ms), or undefined. */
52
+ private windowOf;
53
+ /**
54
+ * A message on `topic` at `handle`'s node, published by peer `from`: kept when it is a beat whose
55
+ * instance signature verifies (beaconBeat) and is newer than the longest window on the topic — the
56
+ * sender's latest replaces an older one. Nothing logged.
57
+ */
58
+ observe(handle: string, topic: string, frame: Uint8Array, from: string): LiveVerdict;
59
+ /** The beats live for `app`'s liveness on `topic` (newer than its window), newest first; undefined: the app keeps no liveness for the topic. */
60
+ read(handle: string, app: string, topic: string): LiveBeat[] | undefined;
61
+ /** Every beat live on `topic` at `handle` (newer than the longest window on it), whichever app keeps it. */
62
+ live(handle: string, topic: string): LiveBeat[];
63
+ /** Forget `handle`: its liveness and its beats. */
64
+ drop(handle: string): void;
65
+ }