@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.
@@ -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;