@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,277 @@
1
+ /**
2
+ * MeshOS daemon-author SDK — TypeScript wrapper.
3
+ *
4
+ * Sits on top of the napi-rs binding at `@net-mesh/core`. Adds:
5
+ *
6
+ * - {@link MeshOsDaemon} interface for daemon implementors.
7
+ * - Typed {@link DaemonControl} / {@link MaintenanceState} unions
8
+ * the binding emits as plain POJOs.
9
+ * - {@link MeshOsSdkError} typed Error subclass that parses the
10
+ * substrate `<<meshos-sdk-kind:KIND>>MSG` envelope into a
11
+ * structured `.kind` field.
12
+ * - `AsyncIterable<DaemonControl>` over the raw `nextControl()`
13
+ * napi method.
14
+ *
15
+ * Slice 1 (Phase 3) ships: `start` / `registerDaemon` /
16
+ * `nextControl` / `tryNextControl` / `publishLog` /
17
+ * `gracefulShutdown` / `metadata` / `refreshMetadata`. Capability
18
+ * publishing is a substrate-side stub (`publishCapabilities`
19
+ * returns without committing). Snapshot / restore work via the
20
+ * daemon object's optional methods; the supervisor invokes them
21
+ * on migration.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { Identity } from '@net-mesh/core';
26
+ * import { MeshOsDaemonSdk, type MeshOsDaemon } from '@net-mesh/sdk/meshos';
27
+ *
28
+ * const daemon: MeshOsDaemon = {
29
+ * name: 'echo',
30
+ * process: (event) => [event.payload],
31
+ * };
32
+ *
33
+ * const sdk = MeshOsDaemonSdk.start();
34
+ * const handle = sdk.registerDaemon(daemon, Identity.generate());
35
+ * handle.publishLog('info', 'started');
36
+ *
37
+ * for await (const ev of handle.controlEvents()) {
38
+ * if (ev.kind === 'Shutdown') break;
39
+ * }
40
+ *
41
+ * await handle.gracefulShutdown(5_000n);
42
+ * await sdk.shutdown();
43
+ * ```
44
+ */
45
+ import { MeshOsDaemonHandle as NapiHandle, type CausalEventJs, type Identity, type CapabilitySetJs } from '@net-mesh/core';
46
+ /**
47
+ * Typed error raised by the MeshOS SDK. The Rust side embeds a
48
+ * `<<meshos-sdk-kind:KIND>>MSG` envelope in the message; this
49
+ * class parses the kind out so callers can branch programmatically
50
+ * instead of regexing the message.
51
+ */
52
+ export declare class MeshOsSdkError extends Error {
53
+ readonly kind: string;
54
+ constructor(kind: string, message: string);
55
+ /**
56
+ * Parse the envelope on a caught napi error. Returns the typed
57
+ * subclass when the envelope is present; falls through to the
58
+ * original error otherwise.
59
+ */
60
+ static fromCaught(err: unknown): MeshOsSdkError | Error;
61
+ }
62
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
63
+ export type DaemonControlShutdown = {
64
+ kind: 'Shutdown';
65
+ gracePeriodMs: bigint;
66
+ };
67
+ export type DaemonControlDrainStart = {
68
+ kind: 'DrainStart';
69
+ gracePeriodMs: bigint;
70
+ };
71
+ export type DaemonControlDrainFinish = {
72
+ kind: 'DrainFinish';
73
+ };
74
+ export type DaemonControlBackpressureOn = {
75
+ kind: 'BackpressureOn';
76
+ level: number;
77
+ };
78
+ export type DaemonControlBackpressureOff = {
79
+ kind: 'BackpressureOff';
80
+ };
81
+ export type DaemonControlUnknown = {
82
+ kind: 'Unknown';
83
+ };
84
+ export type DaemonControl = DaemonControlShutdown | DaemonControlDrainStart | DaemonControlDrainFinish | DaemonControlBackpressureOn | DaemonControlBackpressureOff | DaemonControlUnknown;
85
+ export type MaintenanceStateActive = {
86
+ kind: 'Active';
87
+ };
88
+ export type MaintenanceStateEntering = {
89
+ kind: 'EnteringMaintenance';
90
+ sinceMs: bigint;
91
+ deadlineRemainingMs: bigint | null;
92
+ };
93
+ export type MaintenanceStateSteady = {
94
+ kind: 'Maintenance';
95
+ sinceMs: bigint;
96
+ };
97
+ export type MaintenanceStateExiting = {
98
+ kind: 'ExitingMaintenance';
99
+ sinceMs: bigint;
100
+ };
101
+ export type MaintenanceStateDrainFailed = {
102
+ kind: 'DrainFailed';
103
+ sinceMs: bigint;
104
+ reason: string;
105
+ };
106
+ export type MaintenanceStateRecovery = {
107
+ kind: 'Recovery';
108
+ sinceMs: bigint;
109
+ };
110
+ export type MaintenanceStateUnknown = {
111
+ kind: 'Unknown';
112
+ };
113
+ export type MaintenanceState = MaintenanceStateActive | MaintenanceStateEntering | MaintenanceStateSteady | MaintenanceStateExiting | MaintenanceStateDrainFailed | MaintenanceStateRecovery | MaintenanceStateUnknown;
114
+ export type PeerHealth = 'Healthy' | 'Degraded' | 'Unreachable' | 'Unknown';
115
+ export type PeerMaintenance = 'Active' | 'EnteringMaintenance' | 'Maintenance' | 'ExitingMaintenance' | 'DrainFailed' | 'Recovery' | 'Unknown';
116
+ export interface PeerSnapshot {
117
+ rttMs: bigint | null;
118
+ health: PeerHealth | null;
119
+ maintenance: PeerMaintenance | null;
120
+ cpuLoad1m: number | null;
121
+ memUsedBytes: bigint | null;
122
+ memTotalBytes: bigint | null;
123
+ diskUsedBytes: bigint | null;
124
+ diskTotalBytes: bigint | null;
125
+ saturationTrend: number | null;
126
+ capabilitySet: string[];
127
+ softwareVersion: string | null;
128
+ forkedFrom: bigint | null;
129
+ }
130
+ export interface MetadataView {
131
+ nodeId: bigint;
132
+ daemonId: bigint;
133
+ daemonName: string;
134
+ maintenanceState: MaintenanceState;
135
+ /** Keyed by peer node id. The binding emits a list of entries
136
+ * so BigInt keys round-trip cleanly; we materialize a Map here. */
137
+ peers: Map<bigint, PeerSnapshot>;
138
+ }
139
+ export interface CausalEvent {
140
+ originHash: bigint;
141
+ sequence: bigint;
142
+ payload: Buffer;
143
+ }
144
+ /**
145
+ * Daemon health envelope. Either a plain string discriminator
146
+ * (`'healthy'` / `'degraded'` / `'unhealthy'`) or an object form
147
+ * carrying an optional `reason` that rides into the substrate's
148
+ * `Degraded { reason }` variant.
149
+ */
150
+ export type DaemonHealth = 'healthy' | 'degraded' | 'unhealthy' | {
151
+ kind: 'healthy' | 'degraded' | 'unhealthy';
152
+ reason?: string;
153
+ };
154
+ /**
155
+ * Capability tag advertisement. Mirrors the `requiredCapabilities`
156
+ * / `optionalCapabilities` slot consumed by the substrate's
157
+ * placement filter. Either a static `string[]` or a `() => string[]`
158
+ * callable resolved once at registration time.
159
+ */
160
+ export type CapabilityAdvert = string[] | (() => string[]);
161
+ /**
162
+ * A MeshOS-supervised daemon. Required: `name` (string property) +
163
+ * `process(event)` (returns `Buffer[]` per inbound event, or `[]`
164
+ * for sinks).
165
+ *
166
+ * Optional methods (the substrate falls back to defaults if
167
+ * absent):
168
+ * - `snapshot()` — returns the daemon's serialized state, or
169
+ * `null` when stateless.
170
+ * - `restore(state: Buffer)` — re-seed state at migration target.
171
+ * - `onControl(event: DaemonControl)` — trait callback; the SDK
172
+ * *also* delivers the same event over `controlEvents()`. Use
173
+ * whichever delivery model fits.
174
+ * - `health()` — return liveness state; default `'healthy'`.
175
+ * - `saturation()` — return load factor in `[0.0, 1.0]`; default 0.
176
+ *
177
+ * Optional properties:
178
+ * - `requiredCapabilities` / `optionalCapabilities` — static tag
179
+ * list or callable returning one. Resolved once at registration
180
+ * time and cached on the substrate side.
181
+ */
182
+ export interface MeshOsDaemon {
183
+ name: string;
184
+ process(event: CausalEvent): Buffer[];
185
+ snapshot?(): Buffer | null;
186
+ restore?(state: Buffer): void;
187
+ onControl?(event: DaemonControl): void;
188
+ health?(): DaemonHealth;
189
+ saturation?(): number;
190
+ requiredCapabilities?: CapabilityAdvert;
191
+ optionalCapabilities?: CapabilityAdvert;
192
+ }
193
+ export interface MeshOsConfig {
194
+ thisNode?: bigint;
195
+ tickIntervalMs?: bigint;
196
+ eventQueueCapacity?: number;
197
+ actionQueueCapacity?: number;
198
+ }
199
+ export interface MeshOsDaemonSdkOptions {
200
+ /** Per-daemon control-channel capacity. Default 8 events. */
201
+ controlCapacity?: number;
202
+ /** Max time (ms) the bridge waits for a JS callback (`process`
203
+ * / `snapshot` / `restore` / `onControl`) to respond. Default
204
+ * 60_000. */
205
+ callbackTimeoutMs?: number;
206
+ }
207
+ export declare class MeshOsDaemonSdk {
208
+ private readonly raw;
209
+ private constructor();
210
+ /**
211
+ * Start the SDK with optional config + the substrate's
212
+ * `LoggingDispatcher`. Async so the napi-side factory runs in
213
+ * the napi tokio context (a nested local runtime would deadlock).
214
+ */
215
+ static start(config?: MeshOsConfig, options?: MeshOsDaemonSdkOptions): Promise<MeshOsDaemonSdk>;
216
+ /**
217
+ * Register a daemon under the supplied identity. The daemon
218
+ * object must satisfy {@link MeshOsDaemon}; the binding eagerly
219
+ * resolves `process` (and optional `snapshot` / `restore` /
220
+ * `onControl`) into TSFNs at registration time, so a missing
221
+ * `process` raises immediately rather than later on the first
222
+ * event.
223
+ */
224
+ registerDaemon(daemon: MeshOsDaemon, identity: Identity): Promise<MeshOsDaemonHandle>;
225
+ /** Diagnostic counter — total control events the router dropped
226
+ * across every registered daemon because a daemon's channel was
227
+ * full. */
228
+ droppedControlEvents(): Promise<bigint>;
229
+ /** Tear down the wrapped runtime. Subsequent calls throw
230
+ * `MeshOsSdkError(kind: "already_shutdown")`. */
231
+ shutdown(): Promise<void>;
232
+ }
233
+ export declare class MeshOsDaemonHandle {
234
+ private readonly raw;
235
+ constructor(raw: NapiHandle);
236
+ /** Substrate identifier (origin hash). Stable across the
237
+ * handle's lifetime, readable after shutdown. */
238
+ get daemonId(): bigint;
239
+ /** Daemon's `name` at registration. Readable after shutdown. */
240
+ get daemonName(): string;
241
+ /** Cached metadata view. Refresh via {@link refreshMetadata}. */
242
+ metadata(): Promise<MetadataView>;
243
+ /** Rebuild the metadata view from the runtime's latest snapshot. */
244
+ refreshMetadata(): Promise<MetadataView>;
245
+ /** Block until the next control event arrives, the runtime
246
+ * shuts down, or `timeoutMs` elapses. Resolves to the event or
247
+ * `null` on timeout / shutdown. */
248
+ nextControl(timeoutMs?: bigint): Promise<DaemonControl | null>;
249
+ /** Non-blocking control-event receive. Returns the next event
250
+ * or `null` if the channel is empty. */
251
+ tryNextControl(): Promise<DaemonControl | null>;
252
+ /** Async-iterable view over control events. Terminates when
253
+ * `gracefulShutdown` runs or the runtime exits. Use:
254
+ *
255
+ * ```ts
256
+ * for await (const ev of handle.controlEvents()) {
257
+ * if (ev.kind === 'Shutdown') break;
258
+ * }
259
+ * ```
260
+ */
261
+ controlEvents(): AsyncIterable<DaemonControl>;
262
+ /** Publish a log line tagged with this daemon's id. Non-blocking
263
+ * on the substrate side; the napi call is async because every
264
+ * handle method serializes on the inner mutex. Throws
265
+ * `MeshOsSdkError(kind: "queue_full" | "loop_closed")` when the
266
+ * substrate's log ring is saturated. */
267
+ publishLog(level: LogLevel, message: string): Promise<void>;
268
+ /** Publish (or update) the daemon's capability set. Slice 1 is
269
+ * a substrate-side stub — the call returns without committing. */
270
+ publishCapabilities(caps?: CapabilitySetJs | null): Promise<void>;
271
+ /** Drive a graceful shutdown. Sends
272
+ * `Shutdown { gracePeriodMs }` on the daemon's control channel,
273
+ * parks for `graceMs`, then unregisters. Consumes the handle —
274
+ * subsequent calls throw `already_shutdown`. */
275
+ gracefulShutdown(graceMs?: bigint): Promise<void>;
276
+ }
277
+ export type { CausalEventJs };
package/dist/meshos.js ADDED
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ /**
3
+ * MeshOS daemon-author SDK — TypeScript wrapper.
4
+ *
5
+ * Sits on top of the napi-rs binding at `@net-mesh/core`. Adds:
6
+ *
7
+ * - {@link MeshOsDaemon} interface for daemon implementors.
8
+ * - Typed {@link DaemonControl} / {@link MaintenanceState} unions
9
+ * the binding emits as plain POJOs.
10
+ * - {@link MeshOsSdkError} typed Error subclass that parses the
11
+ * substrate `<<meshos-sdk-kind:KIND>>MSG` envelope into a
12
+ * structured `.kind` field.
13
+ * - `AsyncIterable<DaemonControl>` over the raw `nextControl()`
14
+ * napi method.
15
+ *
16
+ * Slice 1 (Phase 3) ships: `start` / `registerDaemon` /
17
+ * `nextControl` / `tryNextControl` / `publishLog` /
18
+ * `gracefulShutdown` / `metadata` / `refreshMetadata`. Capability
19
+ * publishing is a substrate-side stub (`publishCapabilities`
20
+ * returns without committing). Snapshot / restore work via the
21
+ * daemon object's optional methods; the supervisor invokes them
22
+ * on migration.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { Identity } from '@net-mesh/core';
27
+ * import { MeshOsDaemonSdk, type MeshOsDaemon } from '@net-mesh/sdk/meshos';
28
+ *
29
+ * const daemon: MeshOsDaemon = {
30
+ * name: 'echo',
31
+ * process: (event) => [event.payload],
32
+ * };
33
+ *
34
+ * const sdk = MeshOsDaemonSdk.start();
35
+ * const handle = sdk.registerDaemon(daemon, Identity.generate());
36
+ * handle.publishLog('info', 'started');
37
+ *
38
+ * for await (const ev of handle.controlEvents()) {
39
+ * if (ev.kind === 'Shutdown') break;
40
+ * }
41
+ *
42
+ * await handle.gracefulShutdown(5_000n);
43
+ * await sdk.shutdown();
44
+ * ```
45
+ */
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.MeshOsDaemonHandle = exports.MeshOsDaemonSdk = exports.MeshOsSdkError = void 0;
48
+ const core_1 = require("@net-mesh/core");
49
+ // ----------------------------------------------------------------------------
50
+ // Typed error envelope
51
+ // ----------------------------------------------------------------------------
52
+ /**
53
+ * Typed error raised by the MeshOS SDK. The Rust side embeds a
54
+ * `<<meshos-sdk-kind:KIND>>MSG` envelope in the message; this
55
+ * class parses the kind out so callers can branch programmatically
56
+ * instead of regexing the message.
57
+ */
58
+ class MeshOsSdkError extends Error {
59
+ kind;
60
+ constructor(kind, message) {
61
+ super(`<<meshos-sdk-kind:${kind}>>${message}`);
62
+ this.name = 'MeshOsSdkError';
63
+ this.kind = kind;
64
+ Object.setPrototypeOf(this, MeshOsSdkError.prototype);
65
+ }
66
+ /**
67
+ * Parse the envelope on a caught napi error. Returns the typed
68
+ * subclass when the envelope is present; falls through to the
69
+ * original error otherwise.
70
+ */
71
+ static fromCaught(err) {
72
+ if (err instanceof MeshOsSdkError)
73
+ return err;
74
+ if (!(err instanceof Error)) {
75
+ return new Error(String(err));
76
+ }
77
+ const parsed = parseEnvelope(err.message);
78
+ if (!parsed)
79
+ return err;
80
+ return new MeshOsSdkError(parsed.kind, parsed.body);
81
+ }
82
+ }
83
+ exports.MeshOsSdkError = MeshOsSdkError;
84
+ function parseEnvelope(message) {
85
+ const marker = '<<meshos-sdk-kind:';
86
+ const start = message.indexOf(marker);
87
+ if (start === -1)
88
+ return null;
89
+ const kindStart = start + marker.length;
90
+ const end = message.indexOf('>>', kindStart);
91
+ if (end === -1)
92
+ return null;
93
+ return { kind: message.slice(kindStart, end), body: message.slice(end + 2) };
94
+ }
95
+ /**
96
+ * Run an operation, rethrowing the envelope as a typed
97
+ * `MeshOsSdkError` whose `.kind` callers can branch on.
98
+ */
99
+ function rethrow(fn) {
100
+ try {
101
+ return fn();
102
+ }
103
+ catch (e) {
104
+ throw MeshOsSdkError.fromCaught(e);
105
+ }
106
+ }
107
+ async function rethrowAsync(fn) {
108
+ try {
109
+ return await fn();
110
+ }
111
+ catch (e) {
112
+ throw MeshOsSdkError.fromCaught(e);
113
+ }
114
+ }
115
+ // ----------------------------------------------------------------------------
116
+ // MeshOsDaemonSdk — entry point
117
+ // ----------------------------------------------------------------------------
118
+ class MeshOsDaemonSdk {
119
+ raw;
120
+ constructor(raw) {
121
+ this.raw = raw;
122
+ }
123
+ /**
124
+ * @internal Accessor for sibling SDK modules (currently the
125
+ * Deck SDK's `DeckClient.fromMeshos`) that need to compose
126
+ * against the underlying napi handle. Not part of the public
127
+ * API; calling it from consumer code is unsupported.
128
+ */
129
+ __rawNapiSdk() {
130
+ return this.raw;
131
+ }
132
+ /**
133
+ * Start the SDK with optional config + the substrate's
134
+ * `LoggingDispatcher`. Async so the napi-side factory runs in
135
+ * the napi tokio context (a nested local runtime would deadlock).
136
+ */
137
+ static async start(config, options) {
138
+ return rethrowAsync(async () => {
139
+ const raw = await core_1.MeshOsDaemonSdk.start(config ? toNapiConfig(config) : undefined, options?.controlCapacity, options?.callbackTimeoutMs);
140
+ return new MeshOsDaemonSdk(raw);
141
+ });
142
+ }
143
+ /**
144
+ * Register a daemon under the supplied identity. The daemon
145
+ * object must satisfy {@link MeshOsDaemon}; the binding eagerly
146
+ * resolves `process` (and optional `snapshot` / `restore` /
147
+ * `onControl`) into TSFNs at registration time, so a missing
148
+ * `process` raises immediately rather than later on the first
149
+ * event.
150
+ */
151
+ async registerDaemon(daemon, identity) {
152
+ return rethrowAsync(async () => {
153
+ // The napi `registerDaemon` accepts a plain object with the
154
+ // typed callable fields. Cast through `unknown` because the
155
+ // generated d.ts references an internal `DaemonObjectTsfns`
156
+ // type that isn't reified at the napi boundary.
157
+ const handle = await this.raw.registerDaemon(daemon, identity);
158
+ return new MeshOsDaemonHandle(handle);
159
+ });
160
+ }
161
+ /** Diagnostic counter — total control events the router dropped
162
+ * across every registered daemon because a daemon's channel was
163
+ * full. */
164
+ async droppedControlEvents() {
165
+ return rethrowAsync(() => this.raw.droppedControlEvents());
166
+ }
167
+ /** Tear down the wrapped runtime. Subsequent calls throw
168
+ * `MeshOsSdkError(kind: "already_shutdown")`. */
169
+ async shutdown() {
170
+ await rethrowAsync(() => this.raw.shutdown());
171
+ }
172
+ }
173
+ exports.MeshOsDaemonSdk = MeshOsDaemonSdk;
174
+ // ----------------------------------------------------------------------------
175
+ // MeshOsDaemonHandle — per-daemon handle
176
+ // ----------------------------------------------------------------------------
177
+ class MeshOsDaemonHandle {
178
+ raw;
179
+ constructor(raw) {
180
+ this.raw = raw;
181
+ }
182
+ /** Substrate identifier (origin hash). Stable across the
183
+ * handle's lifetime, readable after shutdown. */
184
+ get daemonId() {
185
+ return this.raw.daemonId;
186
+ }
187
+ /** Daemon's `name` at registration. Readable after shutdown. */
188
+ get daemonName() {
189
+ return this.raw.daemonName;
190
+ }
191
+ /** Cached metadata view. Refresh via {@link refreshMetadata}. */
192
+ async metadata() {
193
+ return rethrowAsync(async () => fromNapiMetadata(await this.raw.metadata()));
194
+ }
195
+ /** Rebuild the metadata view from the runtime's latest snapshot. */
196
+ async refreshMetadata() {
197
+ return rethrowAsync(async () => fromNapiMetadata(await this.raw.refreshMetadata()));
198
+ }
199
+ /** Block until the next control event arrives, the runtime
200
+ * shuts down, or `timeoutMs` elapses. Resolves to the event or
201
+ * `null` on timeout / shutdown. */
202
+ async nextControl(timeoutMs) {
203
+ return rethrowAsync(async () => {
204
+ const ev = await this.raw.nextControl(timeoutMs);
205
+ return ev ? fromNapiControl(ev) : null;
206
+ });
207
+ }
208
+ /** Non-blocking control-event receive. Returns the next event
209
+ * or `null` if the channel is empty. */
210
+ async tryNextControl() {
211
+ return rethrowAsync(async () => {
212
+ const ev = await this.raw.tryNextControl();
213
+ return ev ? fromNapiControl(ev) : null;
214
+ });
215
+ }
216
+ /** Async-iterable view over control events. Terminates when
217
+ * `gracefulShutdown` runs or the runtime exits. Use:
218
+ *
219
+ * ```ts
220
+ * for await (const ev of handle.controlEvents()) {
221
+ * if (ev.kind === 'Shutdown') break;
222
+ * }
223
+ * ```
224
+ */
225
+ controlEvents() {
226
+ const handle = this;
227
+ return {
228
+ [Symbol.asyncIterator]() {
229
+ return {
230
+ async next() {
231
+ try {
232
+ const ev = await handle.nextControl();
233
+ if (ev === null)
234
+ return { value: undefined, done: true };
235
+ return { value: ev, done: false };
236
+ }
237
+ catch (e) {
238
+ // After `gracefulShutdown` the napi handle throws
239
+ // `already_shutdown`. Terminate cleanly.
240
+ const err = MeshOsSdkError.fromCaught(e);
241
+ if (err instanceof MeshOsSdkError && err.kind === 'already_shutdown') {
242
+ return { value: undefined, done: true };
243
+ }
244
+ throw err;
245
+ }
246
+ },
247
+ async return() {
248
+ return { value: undefined, done: true };
249
+ },
250
+ };
251
+ },
252
+ };
253
+ }
254
+ /** Publish a log line tagged with this daemon's id. Non-blocking
255
+ * on the substrate side; the napi call is async because every
256
+ * handle method serializes on the inner mutex. Throws
257
+ * `MeshOsSdkError(kind: "queue_full" | "loop_closed")` when the
258
+ * substrate's log ring is saturated. */
259
+ async publishLog(level, message) {
260
+ await rethrowAsync(() => this.raw.publishLog(level, message));
261
+ }
262
+ /** Publish (or update) the daemon's capability set. Slice 1 is
263
+ * a substrate-side stub — the call returns without committing. */
264
+ async publishCapabilities(caps) {
265
+ await rethrowAsync(() => this.raw.publishCapabilities(caps ?? null));
266
+ }
267
+ /** Drive a graceful shutdown. Sends
268
+ * `Shutdown { gracePeriodMs }` on the daemon's control channel,
269
+ * parks for `graceMs`, then unregisters. Consumes the handle —
270
+ * subsequent calls throw `already_shutdown`. */
271
+ async gracefulShutdown(graceMs) {
272
+ await rethrowAsync(() => this.raw.gracefulShutdown(graceMs));
273
+ }
274
+ }
275
+ exports.MeshOsDaemonHandle = MeshOsDaemonHandle;
276
+ // ----------------------------------------------------------------------------
277
+ // POJO converters
278
+ // ----------------------------------------------------------------------------
279
+ function toNapiConfig(c) {
280
+ return {
281
+ thisNode: c.thisNode,
282
+ tickIntervalMs: c.tickIntervalMs,
283
+ eventQueueCapacity: c.eventQueueCapacity,
284
+ actionQueueCapacity: c.actionQueueCapacity,
285
+ };
286
+ }
287
+ function fromNapiControl(ev) {
288
+ switch (ev.kind) {
289
+ case 'Shutdown':
290
+ return { kind: 'Shutdown', gracePeriodMs: ev.gracePeriodMs ?? 0n };
291
+ case 'DrainStart':
292
+ return { kind: 'DrainStart', gracePeriodMs: ev.gracePeriodMs ?? 0n };
293
+ case 'DrainFinish':
294
+ return { kind: 'DrainFinish' };
295
+ case 'BackpressureOn':
296
+ return { kind: 'BackpressureOn', level: ev.level ?? 0 };
297
+ case 'BackpressureOff':
298
+ return { kind: 'BackpressureOff' };
299
+ default:
300
+ return { kind: 'Unknown' };
301
+ }
302
+ }
303
+ function fromNapiMaintenance(m) {
304
+ switch (m.kind) {
305
+ case 'Active':
306
+ return { kind: 'Active' };
307
+ case 'EnteringMaintenance':
308
+ return {
309
+ kind: 'EnteringMaintenance',
310
+ sinceMs: m.sinceMs ?? 0n,
311
+ deadlineRemainingMs: m.deadlineRemainingMs ?? null,
312
+ };
313
+ case 'Maintenance':
314
+ return { kind: 'Maintenance', sinceMs: m.sinceMs ?? 0n };
315
+ case 'ExitingMaintenance':
316
+ return { kind: 'ExitingMaintenance', sinceMs: m.sinceMs ?? 0n };
317
+ case 'DrainFailed':
318
+ return {
319
+ kind: 'DrainFailed',
320
+ sinceMs: m.sinceMs ?? 0n,
321
+ reason: m.reason ?? '',
322
+ };
323
+ case 'Recovery':
324
+ return { kind: 'Recovery', sinceMs: m.sinceMs ?? 0n };
325
+ default:
326
+ return { kind: 'Unknown' };
327
+ }
328
+ }
329
+ function fromNapiPeer(p) {
330
+ return {
331
+ rttMs: p.rttMs ?? null,
332
+ health: p.health ?? null,
333
+ maintenance: p.maintenance ?? null,
334
+ // napi-derive emits `cpu_load_1m` as `cpuLoad1M` (camelCase
335
+ // capitalizes digit-adjacent letters); accept that form.
336
+ cpuLoad1m: p.cpuLoad1M ?? null,
337
+ memUsedBytes: p.memUsedBytes ?? null,
338
+ memTotalBytes: p.memTotalBytes ?? null,
339
+ diskUsedBytes: p.diskUsedBytes ?? null,
340
+ diskTotalBytes: p.diskTotalBytes ?? null,
341
+ saturationTrend: p.saturationTrend ?? null,
342
+ capabilitySet: p.capabilitySet,
343
+ softwareVersion: p.softwareVersion ?? null,
344
+ forkedFrom: p.forkedFrom ?? null,
345
+ };
346
+ }
347
+ function fromNapiMetadata(v) {
348
+ const peers = new Map();
349
+ for (const entry of v.peers) {
350
+ peers.set(entry.nodeId, fromNapiPeer(entry.snapshot));
351
+ }
352
+ return {
353
+ nodeId: v.nodeId,
354
+ daemonId: v.daemonId,
355
+ daemonName: v.daemonName,
356
+ maintenanceState: fromNapiMaintenance(v.maintenanceState),
357
+ peers,
358
+ };
359
+ }