@net-mesh/sdk 0.19.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.
- package/README.md +1684 -0
- package/dist/_internal.d.ts +25 -0
- package/dist/_internal.js +60 -0
- package/dist/capabilities.d.ts +271 -0
- package/dist/capabilities.js +186 -0
- package/dist/capability-enhancements.d.ts +574 -0
- package/dist/capability-enhancements.js +1324 -0
- package/dist/capability-schema.d.ts +112 -0
- package/dist/capability-schema.js +317 -0
- package/dist/channel.d.ts +56 -0
- package/dist/channel.js +95 -0
- package/dist/compute.d.ts +546 -0
- package/dist/compute.js +741 -0
- package/dist/cortex.d.ts +236 -0
- package/dist/cortex.js +584 -0
- package/dist/deck.d.ts +342 -0
- package/dist/deck.js +717 -0
- package/dist/groups.d.ts +208 -0
- package/dist/groups.js +431 -0
- package/dist/identity.d.ts +149 -0
- package/dist/identity.js +264 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +147 -0
- package/dist/mesh.d.ts +369 -0
- package/dist/mesh.js +433 -0
- package/dist/meshdb.d.ts +87 -0
- package/dist/meshdb.js +111 -0
- package/dist/meshos.d.ts +277 -0
- package/dist/meshos.js +359 -0
- package/dist/node.d.ts +120 -0
- package/dist/node.js +246 -0
- package/dist/redis-dedup.d.ts +48 -0
- package/dist/redis-dedup.js +52 -0
- package/dist/stream.d.ts +47 -0
- package/dist/stream.js +118 -0
- package/dist/subnets.d.ts +75 -0
- package/dist/subnets.js +54 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +5 -0
- package/package.json +43 -0
package/dist/compute.js
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compute surface — `MeshDaemon` + `DaemonRuntime`.
|
|
4
|
+
*
|
|
5
|
+
* Stage 3 of `SDK_COMPUTE_SURFACE_PLAN.md`. Sub-step 1 lands the
|
|
6
|
+
* skeleton: a caller can build a runtime against an existing
|
|
7
|
+
* {@link MeshNode}, register a factory (stored but not yet
|
|
8
|
+
* invoked), start the runtime, and shut it down. Event delivery,
|
|
9
|
+
* migration, and snapshot/restore land in subsequent sub-steps.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { MeshNode, DaemonRuntime } from '@net-mesh/sdk';
|
|
14
|
+
*
|
|
15
|
+
* const mesh = await MeshNode.create({ bindAddr: '127.0.0.1:0', psk: '...' });
|
|
16
|
+
* const rt = DaemonRuntime.create(mesh);
|
|
17
|
+
*
|
|
18
|
+
* // Sub-step 1: register a factory shape the TS side can see.
|
|
19
|
+
* // Sub-step 2+ will actually invoke the returned object on
|
|
20
|
+
* // events delivered by Rust.
|
|
21
|
+
* rt.registerFactory('echo', () => ({
|
|
22
|
+
* name: 'echo',
|
|
23
|
+
* process: (event) => [event.payload],
|
|
24
|
+
* }));
|
|
25
|
+
*
|
|
26
|
+
* await rt.start();
|
|
27
|
+
* // ... daemons would run here (sub-step 3+) ...
|
|
28
|
+
* await rt.shutdown();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.DaemonRuntime = exports.MigrationHandle = exports.DaemonHandle = exports.MigrationError = exports.DaemonError = void 0;
|
|
33
|
+
const core_1 = require("@net-mesh/core");
|
|
34
|
+
const _internal_js_1 = require("./_internal.js");
|
|
35
|
+
// ----------------------------------------------------------------------------
|
|
36
|
+
// Errors — `daemon:` prefix dispatch, mirrors identity/token/cortex pattern.
|
|
37
|
+
// ----------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Base class for daemon-layer errors: factory registration, runtime
|
|
40
|
+
* lifecycle, spawn/stop, migration. The Rust side prefixes every
|
|
41
|
+
* message with `daemon:`; this file peels the prefix and rethrows
|
|
42
|
+
* the typed class so TS callers can `catch (e: DaemonError)`.
|
|
43
|
+
*/
|
|
44
|
+
class DaemonError extends Error {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'DaemonError';
|
|
48
|
+
Object.setPrototypeOf(this, DaemonError.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.DaemonError = DaemonError;
|
|
52
|
+
/**
|
|
53
|
+
* Typed migration failure. Subclass of {@link DaemonError} so
|
|
54
|
+
* `catch (e: DaemonError)` still matches; callers who want to
|
|
55
|
+
* discriminate use `e instanceof MigrationError` + `e.kind`.
|
|
56
|
+
*
|
|
57
|
+
* **Retriability:** only `kind === 'not-ready'` is retriable
|
|
58
|
+
* (the source SDK auto-retries on this by default). Everything
|
|
59
|
+
* else is terminal — a caller's own retry loop won't help.
|
|
60
|
+
*/
|
|
61
|
+
class MigrationError extends DaemonError {
|
|
62
|
+
kind;
|
|
63
|
+
/** Number of NotReady retries on `not-ready-timeout`. */
|
|
64
|
+
attempts;
|
|
65
|
+
/** Daemon origin on `daemon-not-found` / `already-migrating`. */
|
|
66
|
+
originHash;
|
|
67
|
+
/** Node ID on `target-unavailable`. */
|
|
68
|
+
nodeId;
|
|
69
|
+
/** Size / max on `snapshot-too-large`. */
|
|
70
|
+
size;
|
|
71
|
+
max;
|
|
72
|
+
/** Underlying string detail on `state-failed` / `identity-transport-failed`. */
|
|
73
|
+
detail;
|
|
74
|
+
constructor(kind, message, extras = {}) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = 'MigrationError';
|
|
77
|
+
this.kind = kind;
|
|
78
|
+
this.attempts = extras.attempts;
|
|
79
|
+
this.originHash = extras.originHash;
|
|
80
|
+
this.nodeId = extras.nodeId;
|
|
81
|
+
this.size = extras.size;
|
|
82
|
+
this.max = extras.max;
|
|
83
|
+
this.detail = extras.detail;
|
|
84
|
+
Object.setPrototypeOf(this, MigrationError.prototype);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.MigrationError = MigrationError;
|
|
88
|
+
/**
|
|
89
|
+
* Parse a `migration: <kind>[: <detail>]` body (already stripped
|
|
90
|
+
* of the `daemon:` prefix) into a typed {@link MigrationError}.
|
|
91
|
+
* Unknown kinds fall back to `kind: 'unknown'` with the raw body
|
|
92
|
+
* as the message — defensive default so the error surface stays
|
|
93
|
+
* typed even if the Rust side adds new variants.
|
|
94
|
+
*/
|
|
95
|
+
function parseMigrationError(body, fullMessage) {
|
|
96
|
+
// Body shape (after stripping `migration: `):
|
|
97
|
+
// <kind>
|
|
98
|
+
// <kind>: <detail>
|
|
99
|
+
// For some kinds detail is parsed; for others it's free text.
|
|
100
|
+
const afterPrefix = body.slice('migration:'.length).trim();
|
|
101
|
+
const firstColon = afterPrefix.indexOf(':');
|
|
102
|
+
const kind = firstColon === -1 ? afterPrefix : afterPrefix.slice(0, firstColon).trim();
|
|
103
|
+
const rest = firstColon === -1 ? '' : afterPrefix.slice(firstColon + 1).trim();
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case 'not-ready':
|
|
106
|
+
case 'factory-not-found':
|
|
107
|
+
case 'compute-not-supported':
|
|
108
|
+
case 'already-migrating': {
|
|
109
|
+
// `already-migrating` may also carry an originHash on the
|
|
110
|
+
// orchestrator path; parse it when present.
|
|
111
|
+
if (kind === 'already-migrating' && rest) {
|
|
112
|
+
return new MigrationError(kind, fullMessage, {
|
|
113
|
+
originHash: parseMaybeHex(rest),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return new MigrationError(kind, fullMessage);
|
|
117
|
+
}
|
|
118
|
+
case 'state-failed':
|
|
119
|
+
case 'identity-transport-failed':
|
|
120
|
+
return new MigrationError(kind, fullMessage, { detail: rest });
|
|
121
|
+
case 'not-ready-timeout':
|
|
122
|
+
return new MigrationError(kind, fullMessage, {
|
|
123
|
+
attempts: Number.parseInt(rest, 10),
|
|
124
|
+
});
|
|
125
|
+
case 'daemon-not-found':
|
|
126
|
+
return new MigrationError(kind, fullMessage, {
|
|
127
|
+
originHash: parseMaybeHex(rest),
|
|
128
|
+
});
|
|
129
|
+
case 'target-unavailable':
|
|
130
|
+
return new MigrationError(kind, fullMessage, {
|
|
131
|
+
nodeId: parseMaybeHexBigInt(rest),
|
|
132
|
+
});
|
|
133
|
+
case 'wrong-phase':
|
|
134
|
+
return new MigrationError(kind, fullMessage, { detail: rest });
|
|
135
|
+
case 'snapshot-too-large': {
|
|
136
|
+
const [sizeStr, maxStr] = rest.split(':').map((s) => s.trim());
|
|
137
|
+
return new MigrationError(kind, fullMessage, {
|
|
138
|
+
size: Number.parseInt(sizeStr ?? '', 10),
|
|
139
|
+
max: Number.parseInt(maxStr ?? '', 10),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return new MigrationError('unknown', fullMessage);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function parseMaybeHex(s) {
|
|
147
|
+
const trimmed = s.trim();
|
|
148
|
+
if (!trimmed)
|
|
149
|
+
return undefined;
|
|
150
|
+
const n = trimmed.startsWith('0x')
|
|
151
|
+
? Number.parseInt(trimmed.slice(2), 16)
|
|
152
|
+
: Number.parseInt(trimmed, 10);
|
|
153
|
+
return Number.isFinite(n) ? n : undefined;
|
|
154
|
+
}
|
|
155
|
+
function parseMaybeHexBigInt(s) {
|
|
156
|
+
const trimmed = s.trim();
|
|
157
|
+
if (!trimmed)
|
|
158
|
+
return undefined;
|
|
159
|
+
try {
|
|
160
|
+
return trimmed.startsWith('0x') ? BigInt(trimmed) : BigInt(trimmed);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function toDaemonError(e) {
|
|
167
|
+
const msg = e?.message ?? String(e);
|
|
168
|
+
if (msg.startsWith('daemon:')) {
|
|
169
|
+
const body = msg.slice('daemon:'.length).trim();
|
|
170
|
+
if (body.startsWith('migration:')) {
|
|
171
|
+
throw parseMigrationError(body, msg.slice('daemon:'.length).trim());
|
|
172
|
+
}
|
|
173
|
+
throw new DaemonError(body);
|
|
174
|
+
}
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
// ----------------------------------------------------------------------------
|
|
178
|
+
// DaemonHandle — thin wrapper over the NAPI handle.
|
|
179
|
+
// ----------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Handle to a running daemon. Returned by
|
|
182
|
+
* {@link DaemonRuntime.spawn}; pass its `originHash` back to
|
|
183
|
+
* {@link DaemonRuntime.stop} to tear the daemon down.
|
|
184
|
+
*
|
|
185
|
+
* Cloning the JS object shares the same underlying daemon.
|
|
186
|
+
* Dropping the handle does **not** stop the daemon — callers must
|
|
187
|
+
* call `stop` explicitly.
|
|
188
|
+
*/
|
|
189
|
+
class DaemonHandle {
|
|
190
|
+
inner;
|
|
191
|
+
/** @internal */
|
|
192
|
+
constructor(inner) {
|
|
193
|
+
this.inner = inner;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 64-bit hash of the daemon's identity — the key used by the
|
|
197
|
+
* registry, factory registry, and migration dispatcher.
|
|
198
|
+
*/
|
|
199
|
+
get originHash() {
|
|
200
|
+
return this.inner.originHash;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Full 32-byte `EntityId` (ed25519 public key) of the daemon's
|
|
204
|
+
* identity. Returned as a `Buffer` to match the convention used
|
|
205
|
+
* by `Identity.entityId`.
|
|
206
|
+
*/
|
|
207
|
+
get entityId() {
|
|
208
|
+
return this.inner.entityId;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Current runtime statistics for this daemon. Reads a live
|
|
212
|
+
* atomic snapshot from the registry — cheap enough to poll.
|
|
213
|
+
*
|
|
214
|
+
* Throws {@link DaemonError} if the daemon has been stopped.
|
|
215
|
+
*/
|
|
216
|
+
stats() {
|
|
217
|
+
try {
|
|
218
|
+
return this.inner.stats();
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
return toDaemonError(e);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.DaemonHandle = DaemonHandle;
|
|
226
|
+
// ----------------------------------------------------------------------------
|
|
227
|
+
// MigrationHandle — observe and abort an in-flight migration.
|
|
228
|
+
// ----------------------------------------------------------------------------
|
|
229
|
+
/**
|
|
230
|
+
* Handle to an in-flight migration. Returned by
|
|
231
|
+
* {@link DaemonRuntime.startMigration} /
|
|
232
|
+
* {@link DaemonRuntime.startMigrationWith}.
|
|
233
|
+
*
|
|
234
|
+
* Dropping the handle does NOT cancel the migration — the
|
|
235
|
+
* orchestrator keeps driving it to completion in the background.
|
|
236
|
+
* Keep the handle to observe phase transitions or request abort.
|
|
237
|
+
*/
|
|
238
|
+
class MigrationHandle {
|
|
239
|
+
inner;
|
|
240
|
+
/** @internal */
|
|
241
|
+
constructor(inner) {
|
|
242
|
+
this.inner = inner;
|
|
243
|
+
}
|
|
244
|
+
/** 64-bit origin hash of the daemon being migrated. */
|
|
245
|
+
get originHash() {
|
|
246
|
+
return this.inner.originHash;
|
|
247
|
+
}
|
|
248
|
+
/** Node ID of the source (currently hosting) node. */
|
|
249
|
+
get sourceNode() {
|
|
250
|
+
return this.inner.sourceNode;
|
|
251
|
+
}
|
|
252
|
+
/** Node ID of the target (post-cutover) node. */
|
|
253
|
+
get targetNode() {
|
|
254
|
+
return this.inner.targetNode;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Current migration phase, or `null` once the migration has
|
|
258
|
+
* left the orchestrator's records (terminal success or abort).
|
|
259
|
+
* Callers distinguish success from abort by remembering the
|
|
260
|
+
* last non-null phase they observed.
|
|
261
|
+
*/
|
|
262
|
+
phase() {
|
|
263
|
+
const p = this.inner.phase();
|
|
264
|
+
return p ?? null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Async iterator that yields each distinct migration phase as
|
|
268
|
+
* the orchestrator transitions through them, and terminates
|
|
269
|
+
* cleanly once the migration reaches a terminal state (either
|
|
270
|
+
* `complete` on success, or abort / failure — the orchestrator
|
|
271
|
+
* record is gone either way).
|
|
272
|
+
*
|
|
273
|
+
* **Usage pattern:**
|
|
274
|
+
* ```ts
|
|
275
|
+
* const mig = await rt.startMigration(origin, a, b);
|
|
276
|
+
* const phases: MigrationPhase[] = [];
|
|
277
|
+
* for await (const phase of mig.phases()) {
|
|
278
|
+
* phases.push(phase);
|
|
279
|
+
* }
|
|
280
|
+
* // Inspect `phases.at(-1)` — `'complete'` vs anything else
|
|
281
|
+
* // distinguishes success from abort / failure.
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* **Call site ordering:** iterate as soon as the handle is
|
|
285
|
+
* returned. If you await `wait()` first and then call
|
|
286
|
+
* `phases()`, the orchestrator record may already be cleared
|
|
287
|
+
* and the iterator yields nothing.
|
|
288
|
+
*
|
|
289
|
+
* **Sampling cadence:** polls every 50 ms — matching the Rust
|
|
290
|
+
* SDK's `wait()` cadence. Phase transitions faster than that
|
|
291
|
+
* may be missed; acceptable for Stage 1 since real migrations
|
|
292
|
+
* spend hundreds of ms per phase on network round-trips. A
|
|
293
|
+
* broadcast-channel push replacement is documented as future
|
|
294
|
+
* work in `DAEMON_IDENTITY_MIGRATION_PLAN.md`.
|
|
295
|
+
*/
|
|
296
|
+
async *phases() {
|
|
297
|
+
let last = null;
|
|
298
|
+
while (true) {
|
|
299
|
+
const current = this.phase();
|
|
300
|
+
if (current === null) {
|
|
301
|
+
// Orchestrator cleaned up — terminal state reached.
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (current !== last) {
|
|
305
|
+
yield current;
|
|
306
|
+
last = current;
|
|
307
|
+
}
|
|
308
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Block until the migration reaches a terminal state. Resolves
|
|
313
|
+
* on `complete`; rejects with {@link DaemonError} on abort or
|
|
314
|
+
* structured failure (target unavailable, restore failed, etc.).
|
|
315
|
+
*
|
|
316
|
+
* No wall-clock timeout — a migration stalled against an
|
|
317
|
+
* unresponsive peer blocks indefinitely. Use
|
|
318
|
+
* {@link MigrationHandle.waitWithTimeout} for a bound.
|
|
319
|
+
*/
|
|
320
|
+
async wait() {
|
|
321
|
+
try {
|
|
322
|
+
await this.inner.wait();
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
toDaemonError(e);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Like {@link wait} with a caller-controlled timeout (in
|
|
330
|
+
* milliseconds). On timeout the orchestrator record is aborted
|
|
331
|
+
* and the promise rejects with {@link DaemonError}.
|
|
332
|
+
*/
|
|
333
|
+
async waitWithTimeout(timeoutMs) {
|
|
334
|
+
try {
|
|
335
|
+
await this.inner.waitWithTimeout(timeoutMs);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
toDaemonError(e);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Request cancellation of the migration. Best-effort: past
|
|
343
|
+
* `cutover` the routing flip cannot be undone cleanly, and
|
|
344
|
+
* this call resolves without aborting.
|
|
345
|
+
*/
|
|
346
|
+
async cancel() {
|
|
347
|
+
try {
|
|
348
|
+
await this.inner.cancel();
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
toDaemonError(e);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
exports.MigrationHandle = MigrationHandle;
|
|
356
|
+
// ----------------------------------------------------------------------------
|
|
357
|
+
// DaemonRuntime — thin wrapper over the NAPI class.
|
|
358
|
+
// ----------------------------------------------------------------------------
|
|
359
|
+
/**
|
|
360
|
+
* Per-mesh compute runtime. Holds the kind-keyed factory table and
|
|
361
|
+
* drives the `Registering → Ready → ShuttingDown` lifecycle.
|
|
362
|
+
*
|
|
363
|
+
* Construct via {@link create}; the runtime shares the given mesh's
|
|
364
|
+
* underlying `MeshNode` (no second socket). Shutting down the
|
|
365
|
+
* runtime does NOT shut down the mesh — the caller owns that.
|
|
366
|
+
*/
|
|
367
|
+
class DaemonRuntime {
|
|
368
|
+
inner;
|
|
369
|
+
/**
|
|
370
|
+
* TS-side factory table, keyed by `kind`. `registerFactory`
|
|
371
|
+
* inserts here; `spawn` looks up and invokes. Duplicates the
|
|
372
|
+
* kind set that lives on the NAPI side — the NAPI copy drives
|
|
373
|
+
* migration-targeting and the `already registered` check at
|
|
374
|
+
* registration time; this map is what actually gets *called*.
|
|
375
|
+
*/
|
|
376
|
+
factories = new Map();
|
|
377
|
+
constructor(inner) {
|
|
378
|
+
this.inner = inner;
|
|
379
|
+
// Register on the WeakMap so sibling SDK modules (currently
|
|
380
|
+
// `groups`) can reach the native pointer without a public
|
|
381
|
+
// escape-hatch method on the class instance. See
|
|
382
|
+
// `./_internal.ts` for the rationale.
|
|
383
|
+
(0, _internal_js_1.setNapiRuntime)(this, inner);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Build a compute runtime against an existing {@link MeshNode}.
|
|
387
|
+
*/
|
|
388
|
+
static create(mesh) {
|
|
389
|
+
try {
|
|
390
|
+
return new DaemonRuntime(core_1.DaemonRuntime.create((0, _internal_js_1.getNapiMesh)(mesh)));
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
return toDaemonError(e);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Promote to `Ready`. Installs the migration subprotocol handler.
|
|
398
|
+
* Idempotent on an already-ready runtime; rejects on a runtime
|
|
399
|
+
* that has been shut down.
|
|
400
|
+
*/
|
|
401
|
+
async start() {
|
|
402
|
+
try {
|
|
403
|
+
await this.inner.start();
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
toDaemonError(e);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Tear down the runtime. Drains daemons, clears factory
|
|
411
|
+
* registrations, uninstalls the migration handler. Idempotent:
|
|
412
|
+
* a second call on an already-shut-down runtime is a no-op.
|
|
413
|
+
*/
|
|
414
|
+
async shutdown() {
|
|
415
|
+
try {
|
|
416
|
+
await this.inner.shutdown();
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
toDaemonError(e);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* `true` iff the runtime has transitioned to `Ready` and has not
|
|
424
|
+
* yet begun shutting down.
|
|
425
|
+
*/
|
|
426
|
+
isReady() {
|
|
427
|
+
return this.inner.isReady();
|
|
428
|
+
}
|
|
429
|
+
/** Number of daemons currently registered with the runtime. */
|
|
430
|
+
daemonCount() {
|
|
431
|
+
return this.inner.daemonCount();
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Register a factory closure under `kind`. The factory returns a
|
|
435
|
+
* {@link MeshDaemon}-shaped object. Second registration of the
|
|
436
|
+
* same `kind` throws {@link DaemonError}.
|
|
437
|
+
*
|
|
438
|
+
* Sub-step 1 stores the factory but does not invoke it — event
|
|
439
|
+
* dispatch to daemon `process` lands in sub-step 3.
|
|
440
|
+
*
|
|
441
|
+
* ## Migration targeting
|
|
442
|
+
*
|
|
443
|
+
* `registerFactory` alone is **not sufficient** to accept
|
|
444
|
+
* inbound migrations — it registers the kind-to-factory mapping
|
|
445
|
+
* only on the SDK side. Migrations lookup by `origin_hash`, not
|
|
446
|
+
* by kind. Future sub-steps will surface `expectMigration` and
|
|
447
|
+
* `registerMigrationTargetIdentity` for that wiring.
|
|
448
|
+
*/
|
|
449
|
+
registerFactory(kind, factory) {
|
|
450
|
+
try {
|
|
451
|
+
// Register on NAPI so the kind is tracked for migration
|
|
452
|
+
// targeting and so the `already registered` check there
|
|
453
|
+
// fires on duplicate calls before we mutate our own map.
|
|
454
|
+
// The NAPI side stores a TSFN of the factory but doesn't
|
|
455
|
+
// invoke it — the actual invocation happens on the TS side
|
|
456
|
+
// at `spawn` time (see `spawn` below).
|
|
457
|
+
this.inner.registerFactory(kind, factory);
|
|
458
|
+
this.factories.set(kind, factory);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
toDaemonError(e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Spawn a daemon of `kind` under the given {@link Identity}.
|
|
466
|
+
*
|
|
467
|
+
* Invokes the user-supplied factory (registered via
|
|
468
|
+
* {@link DaemonRuntime.registerFactory}), extracts the
|
|
469
|
+
* returned daemon's `process` / `snapshot` / `restore`
|
|
470
|
+
* methods, and hands each to NAPI as a separate JS function.
|
|
471
|
+
* NAPI builds a `ThreadsafeFunction` per method so the
|
|
472
|
+
* eventual event-dispatch path (sub-step 3) can call them
|
|
473
|
+
* from any tokio task.
|
|
474
|
+
*
|
|
475
|
+
* **Sub-step 2b** (current): method TSFNs are stored on the
|
|
476
|
+
* Rust side but **not yet invoked**. `process` / `snapshot` /
|
|
477
|
+
* `restore` behave as no-ops. Sub-step 3 wires the full
|
|
478
|
+
* round-trip so events land in the JS daemon.
|
|
479
|
+
*
|
|
480
|
+
* `kind` must have been registered first — spawning an
|
|
481
|
+
* unregistered kind throws {@link DaemonError}.
|
|
482
|
+
*/
|
|
483
|
+
async spawn(kind, identity, config) {
|
|
484
|
+
const factory = this.factories.get(kind);
|
|
485
|
+
if (!factory) {
|
|
486
|
+
throw new DaemonError(`no factory registered for kind '${kind}'`);
|
|
487
|
+
}
|
|
488
|
+
// Invoke the factory in JS. Accepts both sync and async
|
|
489
|
+
// factories per the `DaemonFactory` type. The returned
|
|
490
|
+
// instance owns its own state (closures, class fields); the
|
|
491
|
+
// method bindings below capture `this` so per-instance state
|
|
492
|
+
// survives across calls.
|
|
493
|
+
const instance = await factory();
|
|
494
|
+
// Method extraction. `snapshot` / `restore` are optional —
|
|
495
|
+
// stateless daemons omit them. `bind(instance)` preserves
|
|
496
|
+
// `this` inside user code when NAPI invokes the function
|
|
497
|
+
// off the main thread via the TSFN.
|
|
498
|
+
//
|
|
499
|
+
// Shape conversion for `process`: the SDK `MeshDaemon.process`
|
|
500
|
+
// returns `Buffer[]`; NAPI's generated type is
|
|
501
|
+
// `(arg: CausalEventJs) => Buffer[]`. Signatures match in
|
|
502
|
+
// practice — the Rust side marshals a full `CausalEventJs`,
|
|
503
|
+
// and the SDK's `MeshDaemon` contract requires `Buffer[]`.
|
|
504
|
+
const process = instance.process.bind(instance);
|
|
505
|
+
const snapshot = instance.snapshot
|
|
506
|
+
? instance.snapshot.bind(instance)
|
|
507
|
+
: undefined;
|
|
508
|
+
const restore = instance.restore
|
|
509
|
+
? instance.restore.bind(instance)
|
|
510
|
+
: undefined;
|
|
511
|
+
try {
|
|
512
|
+
const handle = await this.inner.spawn(kind, identity.toNapi(), process, snapshot, restore, config
|
|
513
|
+
? {
|
|
514
|
+
autoSnapshotInterval: config.autoSnapshotInterval,
|
|
515
|
+
maxLogEntries: config.maxLogEntries,
|
|
516
|
+
callbackTimeoutMs: config.callbackTimeoutMs,
|
|
517
|
+
}
|
|
518
|
+
: undefined);
|
|
519
|
+
return new DaemonHandle(handle);
|
|
520
|
+
}
|
|
521
|
+
catch (e) {
|
|
522
|
+
return toDaemonError(e);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Spawn a daemon of `kind` from a previously-taken snapshot.
|
|
527
|
+
* Parallel to {@link DaemonRuntime.spawn} but seeds the
|
|
528
|
+
* daemon's initial state from `snapshotBytes` by calling its
|
|
529
|
+
* `restore` method before any events land.
|
|
530
|
+
*
|
|
531
|
+
* `snapshotBytes` must be the exact `Buffer` returned by a
|
|
532
|
+
* prior call to {@link DaemonRuntime.snapshot}; mismatched or
|
|
533
|
+
* corrupted bytes surface as `daemon: snapshot decode failed`.
|
|
534
|
+
*
|
|
535
|
+
* `kind` must be registered and the caller's {@link Identity}
|
|
536
|
+
* must match the snapshot's `entityId` — a mismatch throws
|
|
537
|
+
* {@link DaemonError} before any side effects.
|
|
538
|
+
*/
|
|
539
|
+
async spawnFromSnapshot(kind, identity, snapshotBytes, config) {
|
|
540
|
+
const factory = this.factories.get(kind);
|
|
541
|
+
if (!factory) {
|
|
542
|
+
throw new DaemonError(`no factory registered for kind '${kind}'`);
|
|
543
|
+
}
|
|
544
|
+
// Same factory-decomposition dance as `spawn`: invoke in JS,
|
|
545
|
+
// extract methods, hand them to NAPI as separate functions.
|
|
546
|
+
// The daemon's initial (pre-restore) state is built by the
|
|
547
|
+
// factory here; the core's `from_snapshot` will then call
|
|
548
|
+
// `restore` on the bridge with `snapshotBytes`.
|
|
549
|
+
const instance = await factory();
|
|
550
|
+
const process = instance.process.bind(instance);
|
|
551
|
+
const snapshot = instance.snapshot
|
|
552
|
+
? instance.snapshot.bind(instance)
|
|
553
|
+
: undefined;
|
|
554
|
+
const restore = instance.restore
|
|
555
|
+
? instance.restore.bind(instance)
|
|
556
|
+
: undefined;
|
|
557
|
+
try {
|
|
558
|
+
const handle = await this.inner.spawnFromSnapshot(kind, identity.toNapi(), snapshotBytes, process, snapshot, restore, config
|
|
559
|
+
? {
|
|
560
|
+
autoSnapshotInterval: config.autoSnapshotInterval,
|
|
561
|
+
maxLogEntries: config.maxLogEntries,
|
|
562
|
+
callbackTimeoutMs: config.callbackTimeoutMs,
|
|
563
|
+
}
|
|
564
|
+
: undefined);
|
|
565
|
+
return new DaemonHandle(handle);
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
return toDaemonError(e);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Take a snapshot of a running daemon by `originHash`. Returns
|
|
573
|
+
* the daemon's serialized state bytes, or `null` if the daemon
|
|
574
|
+
* is stateless (no `snapshot` method, or it returned `null`).
|
|
575
|
+
*
|
|
576
|
+
* The returned `Buffer` is opaque to the caller — the wire
|
|
577
|
+
* format is the core's `StateSnapshot` encoding, including
|
|
578
|
+
* version headers and the chain link at the snapshot point.
|
|
579
|
+
* Feed it unchanged to {@link DaemonRuntime.spawnFromSnapshot}
|
|
580
|
+
* to restore the daemon on another node or after a restart.
|
|
581
|
+
*/
|
|
582
|
+
async snapshot(originHash) {
|
|
583
|
+
try {
|
|
584
|
+
const buf = await this.inner.snapshot(originHash);
|
|
585
|
+
return buf ?? null;
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
return toDaemonError(e);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Stop a daemon, removing it from the runtime's registry.
|
|
593
|
+
* Idempotent during `ShuttingDown`; rejects with
|
|
594
|
+
* {@link DaemonError} during `Registering` or when the origin
|
|
595
|
+
* is unknown.
|
|
596
|
+
*/
|
|
597
|
+
async stop(originHash) {
|
|
598
|
+
try {
|
|
599
|
+
await this.inner.stop(originHash);
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
toDaemonError(e);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Deliver a single causal event to a live daemon and return
|
|
607
|
+
* the daemon's output buffers. Routes through the core
|
|
608
|
+
* `DaemonRegistry::deliver` → `MeshDaemon::process` path,
|
|
609
|
+
* which invokes the JS `process(event)` callback registered
|
|
610
|
+
* at spawn time and waits for its return.
|
|
611
|
+
*
|
|
612
|
+
* Direct ingress — Stage 1 convenience. Mesh-dispatched
|
|
613
|
+
* delivery (via the causal subprotocol on an inbound packet)
|
|
614
|
+
* lands in a later stage; this method stays as test sugar + a
|
|
615
|
+
* manual-trigger surface.
|
|
616
|
+
*
|
|
617
|
+
* Throws {@link DaemonError} if `originHash` doesn't match a
|
|
618
|
+
* live daemon, if the daemon's `process` throws, or if the
|
|
619
|
+
* runtime is shutting down.
|
|
620
|
+
*/
|
|
621
|
+
async deliver(originHash, event) {
|
|
622
|
+
try {
|
|
623
|
+
return await this.inner.deliver(originHash, {
|
|
624
|
+
originHash: event.originHash,
|
|
625
|
+
sequence: event.sequence,
|
|
626
|
+
payload: event.payload,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
return toDaemonError(e);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Initiate a migration for the daemon identified by
|
|
635
|
+
* `originHash`, moving it from `sourceNode` to `targetNode`.
|
|
636
|
+
*
|
|
637
|
+
* Returns a {@link MigrationHandle} whose `wait()` resolves
|
|
638
|
+
* when the migration reaches a terminal state. On local-source
|
|
639
|
+
* migrations (`sourceNode === mesh.nodeId`) the snapshot is
|
|
640
|
+
* taken synchronously inside this call; on remote-source
|
|
641
|
+
* migrations the orchestrator drives the state machine via
|
|
642
|
+
* inbound wire messages.
|
|
643
|
+
*
|
|
644
|
+
* Both node IDs are `u64` — pass as `bigint` to avoid silent
|
|
645
|
+
* precision loss past 2^53.
|
|
646
|
+
*/
|
|
647
|
+
async startMigration(originHash, sourceNode, targetNode) {
|
|
648
|
+
try {
|
|
649
|
+
const handle = await this.inner.startMigration(originHash, sourceNode, targetNode);
|
|
650
|
+
return new MigrationHandle(handle);
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
return toDaemonError(e);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* {@link startMigration} with caller-supplied options. Use this
|
|
658
|
+
* to opt out of identity transport (when the daemon doesn't
|
|
659
|
+
* need to sign on the target) or to tune the NotReady-retry
|
|
660
|
+
* budget.
|
|
661
|
+
*/
|
|
662
|
+
async startMigrationWith(originHash, sourceNode, targetNode, opts) {
|
|
663
|
+
try {
|
|
664
|
+
const handle = await this.inner.startMigrationWith(originHash, sourceNode, targetNode, {
|
|
665
|
+
transportIdentity: opts.transportIdentity,
|
|
666
|
+
retryNotReadyMs: opts.retryNotReadyMs,
|
|
667
|
+
});
|
|
668
|
+
return new MigrationHandle(handle);
|
|
669
|
+
}
|
|
670
|
+
catch (e) {
|
|
671
|
+
return toDaemonError(e);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Declare that a migration will land on this node for the given
|
|
676
|
+
* `originHash` of `kind`. Registers a placeholder factory; the
|
|
677
|
+
* migration snapshot's identity envelope supplies the real
|
|
678
|
+
* keypair at restore time.
|
|
679
|
+
*
|
|
680
|
+
* Must be called BEFORE the source initiates the migration —
|
|
681
|
+
* the target dispatcher checks for a factory entry when the
|
|
682
|
+
* inbound `SnapshotReady` lands, and rejects with
|
|
683
|
+
* `FactoryNotFound` if nothing is registered.
|
|
684
|
+
*
|
|
685
|
+
* The source must migrate with `transportIdentity: true`
|
|
686
|
+
* (default). Without the envelope the dispatcher emits
|
|
687
|
+
* `IdentityTransportFailed` because the placeholder has no
|
|
688
|
+
* keypair. Use {@link registerMigrationTargetIdentity} for the
|
|
689
|
+
* explicit public-identity-migration case.
|
|
690
|
+
*/
|
|
691
|
+
expectMigration(kind, originHash, config) {
|
|
692
|
+
try {
|
|
693
|
+
this.inner.expectMigration(kind, originHash, config
|
|
694
|
+
? {
|
|
695
|
+
autoSnapshotInterval: config.autoSnapshotInterval,
|
|
696
|
+
maxLogEntries: config.maxLogEntries,
|
|
697
|
+
callbackTimeoutMs: config.callbackTimeoutMs,
|
|
698
|
+
}
|
|
699
|
+
: undefined);
|
|
700
|
+
}
|
|
701
|
+
catch (e) {
|
|
702
|
+
toDaemonError(e);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Pre-register a target-side identity for a migration that
|
|
707
|
+
* will NOT carry an identity envelope (source used
|
|
708
|
+
* `transportIdentity: false`). The target holds the matching
|
|
709
|
+
* {@link Identity}; the dispatcher restores the daemon with
|
|
710
|
+
* that identity instead of overriding it from an envelope.
|
|
711
|
+
*
|
|
712
|
+
* For the common envelope-transport case, prefer
|
|
713
|
+
* {@link expectMigration} — the caller doesn't need to know
|
|
714
|
+
* the daemon's private key ahead of time.
|
|
715
|
+
*/
|
|
716
|
+
registerMigrationTargetIdentity(kind, identity, config) {
|
|
717
|
+
try {
|
|
718
|
+
this.inner.registerMigrationTargetIdentity(kind, identity.toNapi(), config
|
|
719
|
+
? {
|
|
720
|
+
autoSnapshotInterval: config.autoSnapshotInterval,
|
|
721
|
+
maxLogEntries: config.maxLogEntries,
|
|
722
|
+
callbackTimeoutMs: config.callbackTimeoutMs,
|
|
723
|
+
}
|
|
724
|
+
: undefined);
|
|
725
|
+
}
|
|
726
|
+
catch (e) {
|
|
727
|
+
toDaemonError(e);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Query the orchestrator's current migration phase for
|
|
732
|
+
* `originHash`, or `null` if no migration is in flight for
|
|
733
|
+
* that origin. Works on any node — source, target, or an
|
|
734
|
+
* observer that heard the migration on the mesh.
|
|
735
|
+
*/
|
|
736
|
+
migrationPhase(originHash) {
|
|
737
|
+
const p = this.inner.migrationPhase(originHash);
|
|
738
|
+
return p ?? null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
exports.DaemonRuntime = DaemonRuntime;
|