@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,208 @@
1
+ /**
2
+ * Groups surface — `ReplicaGroup` / `ForkGroup` / `StandbyGroup`.
3
+ *
4
+ * Stage 2 of `SDK_GROUPS_SURFACE_PLAN.md`. Thin wrappers over the
5
+ * NAPI classes that add:
6
+ *
7
+ * - Typed `GroupError` extending `DaemonError` with a `kind`
8
+ * discriminator parsed from the Rust side's stable
9
+ * `daemon: group: <kind>[: detail]` error prefix.
10
+ * - `Buffer | Uint8Array` interop for `groupSeed`.
11
+ * - `MigrationOptions`-style config shapes with camelCase fields.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { DaemonRuntime, Identity, ReplicaGroup } from '@net-mesh/sdk';
16
+ *
17
+ * // Register the factory the group will invoke for each member.
18
+ * rt.registerFactory('counter', () => new CounterDaemon());
19
+ *
20
+ * // Spawn a 3-member replica group. `spawn` is async — the
21
+ * // factory TSFN round-trip runs on a tokio worker so the Node
22
+ * // main thread stays free to execute JS factory callbacks.
23
+ * const group = await ReplicaGroup.spawn(rt, 'counter', {
24
+ * replicaCount: 3,
25
+ * groupSeed: Buffer.alloc(32, 0x11), // 32 bytes
26
+ * lbStrategy: 'round-robin',
27
+ * });
28
+ *
29
+ * // Route a request.
30
+ * const origin = group.routeEvent({ routingKey: 'user:42' });
31
+ * await rt.deliver(origin, someEvent);
32
+ * ```
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ import { type ReplicaGroupConfigJs, type ForkGroupConfigJs, type StandbyGroupConfigJs, type GroupHealthJs, type GroupHostConfigJs, type MemberInfoJs, type ForkRecordJs, type RequestContextJs, StrategyJs } from '@net-mesh/core';
37
+ import { DaemonError } from './compute';
38
+ import type { DaemonRuntime } from './compute';
39
+ /**
40
+ * Stable machine-readable discriminator for group-layer failures.
41
+ * Parsed from the Rust side's `daemon: group: <kind>[: detail]`
42
+ * prefix; use `err.kind` in catch blocks rather than parsing the
43
+ * message string by hand.
44
+ */
45
+ export type GroupErrorKind = 'not-ready' | 'factory-not-found' | 'no-healthy-member' | 'placement-failed' | 'registry-failed' | 'invalid-config' | 'daemon' | 'unknown';
46
+ /**
47
+ * Typed group failure. Subclass of {@link DaemonError} so
48
+ * `catch (e: DaemonError)` still matches.
49
+ */
50
+ export declare class GroupError extends DaemonError {
51
+ readonly kind: GroupErrorKind;
52
+ /** Optional detail string carried by `placement-failed` /
53
+ * `registry-failed` / `invalid-config` / `daemon` variants. */
54
+ readonly detail?: string;
55
+ /** `factory-not-found` carries the requested kind name. */
56
+ readonly requestedKind?: string;
57
+ constructor(kind: GroupErrorKind, message: string, extras?: {
58
+ detail?: string;
59
+ requestedKind?: string;
60
+ });
61
+ }
62
+ /**
63
+ * Load-balancing strategy for inbound group events.
64
+ *
65
+ * - `round-robin` — rotate across healthy members.
66
+ * - `consistent-hash` — stable routing on `routingKey`.
67
+ * - `least-load` — pick the member with the lowest utilization.
68
+ * - `least-connections` — pick the member with the fewest in-flight calls.
69
+ * - `random` — uniformly-random healthy pick.
70
+ */
71
+ export type GroupStrategy = StrategyJs;
72
+ /** Per-member metadata. */
73
+ export type GroupMemberInfo = MemberInfoJs;
74
+ /** Aggregate health surface. */
75
+ export type GroupHealth = GroupHealthJs;
76
+ /** Lineage record for a single fork. */
77
+ export type ForkRecord = ForkRecordJs;
78
+ /** Routing context handed to `routeEvent`. */
79
+ export type RequestContext = RequestContextJs;
80
+ /** Per-daemon host config applied to every group member. */
81
+ export type GroupHostConfig = GroupHostConfigJs;
82
+ /** Config for a replica group. */
83
+ export interface ReplicaGroupConfig extends Omit<ReplicaGroupConfigJs, 'groupSeed'> {
84
+ /** 32-byte seed. Accepts `Buffer` or `Uint8Array`. */
85
+ groupSeed: Buffer | Uint8Array;
86
+ }
87
+ /** Config for a fork group. */
88
+ export type ForkGroupConfig = ForkGroupConfigJs;
89
+ /** Config for a standby group. */
90
+ export interface StandbyGroupConfig extends Omit<StandbyGroupConfigJs, 'groupSeed'> {
91
+ /** 32-byte seed. Accepts `Buffer` or `Uint8Array`. */
92
+ groupSeed: Buffer | Uint8Array;
93
+ }
94
+ /**
95
+ * N interchangeable copies of a daemon. Each replica has a
96
+ * deterministic identity derived from `groupSeed + index`;
97
+ * the group load-balances inbound events across healthy members
98
+ * and auto-replaces members on node failure.
99
+ */
100
+ export declare class ReplicaGroup {
101
+ private readonly inner;
102
+ /**
103
+ * Spawn a replica group bound to `runtime`. `kind` must have
104
+ * been registered via {@link DaemonRuntime.registerFactory};
105
+ * the group calls the factory once per replica at spawn and
106
+ * again on scale-up / failure replacement.
107
+ *
108
+ * Async because the underlying SDK `spawn` runs on a tokio
109
+ * worker — the factory TSFN round-trip needs the Node main
110
+ * thread free to execute the JS factory callback, so a sync
111
+ * main-thread spawn would deadlock.
112
+ *
113
+ * Throws {@link GroupError} on `not-ready`, `factory-not-found`,
114
+ * `placement-failed`, `invalid-config`, or `registry-failed`.
115
+ */
116
+ static spawn(runtime: DaemonRuntime, kind: string, config: ReplicaGroupConfig): Promise<ReplicaGroup>;
117
+ /** Route to the best-available replica; returns the target
118
+ * `origin_hash` which the caller feeds to `runtime.deliver`. */
119
+ routeEvent(ctx?: RequestContext): bigint;
120
+ /** Resize the group to `n` members. The kind is fixed at
121
+ * spawn time and not accepted here — see the class docstring
122
+ * for why. Async because growing invokes the factory (TSFN)
123
+ * once per new replica. */
124
+ scaleTo(n: number): Promise<void>;
125
+ /** Replace all members on `failedNodeId` onto other nodes.
126
+ * Returns the indices of replicas that were respawned.
127
+ * Reuses the group's spawn kind. */
128
+ onNodeFailure(failedNodeId: bigint): Promise<number[]>;
129
+ onNodeRecovery(recoveredNodeId: bigint): void;
130
+ get health(): GroupHealth;
131
+ get groupId(): number;
132
+ get replicas(): GroupMemberInfo[];
133
+ get replicaCount(): number;
134
+ get healthyCount(): number;
135
+ }
136
+ /**
137
+ * N independent daemons forked from a common parent at
138
+ * `forkSeq`. Unique identities, shared ancestry via `ForkRecord`.
139
+ */
140
+ export declare class ForkGroup {
141
+ private readonly inner;
142
+ /** Fork `config.forkCount` new daemons from `parentOrigin` at
143
+ * `forkSeq`. Each fork gets a fresh unique keypair + a
144
+ * `ForkRecord` linking it to the parent. Async for the same
145
+ * deadlock-avoidance reason as {@link ReplicaGroup.spawn}. */
146
+ static fork(runtime: DaemonRuntime, kind: string, parentOrigin: bigint, forkSeq: bigint, config: ForkGroupConfig): Promise<ForkGroup>;
147
+ routeEvent(ctx?: RequestContext): bigint;
148
+ scaleTo(n: number): Promise<void>;
149
+ onNodeFailure(failedNodeId: bigint): Promise<number[]>;
150
+ onNodeRecovery(recoveredNodeId: bigint): void;
151
+ get health(): GroupHealth;
152
+ get parentOrigin(): bigint;
153
+ get forkSeq(): bigint;
154
+ get forkRecords(): ForkRecord[];
155
+ /** `true` iff every fork's `ForkRecord` verifies against its
156
+ * parent. Core performs the signature + sentinel checks. */
157
+ verifyLineage(): boolean;
158
+ get members(): GroupMemberInfo[];
159
+ get forkCount(): number;
160
+ get healthyCount(): number;
161
+ }
162
+ /**
163
+ * Active-passive replication. One active processes events; N−1
164
+ * standbys hold snapshots and catch up via {@link sync}. On
165
+ * active failure, {@link promote} (or automatic failover via
166
+ * {@link onNodeFailure}) picks the most-synced standby.
167
+ *
168
+ * **Automatic replay buffering.** The group installs a
169
+ * post-delivery observer on its active member's origin at spawn
170
+ * and re-points it on promote / failover. Every
171
+ * `runtime.deliver(group.activeOrigin, event)` automatically
172
+ * feeds the standby replay buffer — no paired
173
+ * `onEventDelivered` call required from the caller. The method
174
+ * remains on the class for test scenarios that simulate a gap
175
+ * without a live runtime; production code should ignore it.
176
+ */
177
+ export declare class StandbyGroup {
178
+ private readonly inner;
179
+ static spawn(runtime: DaemonRuntime, kind: string, config: StandbyGroupConfig): Promise<StandbyGroup>;
180
+ /** `origin_hash` of the current active. Target for inbound
181
+ * events; standbys don't process inputs. */
182
+ get activeOrigin(): bigint;
183
+ /** Snapshot the active and push to every standby. Returns the
184
+ * sequence number the sync caught up through. */
185
+ sync(): Promise<bigint>;
186
+ /** Promote the most-synced standby to active. Reuses the
187
+ * group's spawn kind — no external parameter, so callers
188
+ * can't accidentally promote with the wrong factory. Call
189
+ * manually for planned failover; {@link onNodeFailure} calls
190
+ * automatically when the active's node fails. */
191
+ promote(): Promise<bigint>;
192
+ /** Handle node failure. Returns the new active's `origin_hash`
193
+ * if the active was on `failedNodeId`; `null` if only standbys
194
+ * were affected. Reuses the group's spawn kind. */
195
+ onNodeFailure(failedNodeId: bigint): Promise<bigint | null>;
196
+ onNodeRecovery(recoveredNodeId: bigint): void;
197
+ get health(): GroupHealth;
198
+ get activeHealthy(): boolean;
199
+ get activeIndex(): number;
200
+ /** `"active"` | `"standby"` | `null` (out-of-range). */
201
+ memberRole(index: number): 'active' | 'standby' | null;
202
+ syncedThrough(index: number): bigint | null;
203
+ get bufferedEventCount(): number;
204
+ get groupId(): number;
205
+ get members(): GroupMemberInfo[];
206
+ get memberCount(): number;
207
+ get standbyCount(): number;
208
+ }
package/dist/groups.js ADDED
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ /**
3
+ * Groups surface — `ReplicaGroup` / `ForkGroup` / `StandbyGroup`.
4
+ *
5
+ * Stage 2 of `SDK_GROUPS_SURFACE_PLAN.md`. Thin wrappers over the
6
+ * NAPI classes that add:
7
+ *
8
+ * - Typed `GroupError` extending `DaemonError` with a `kind`
9
+ * discriminator parsed from the Rust side's stable
10
+ * `daemon: group: <kind>[: detail]` error prefix.
11
+ * - `Buffer | Uint8Array` interop for `groupSeed`.
12
+ * - `MigrationOptions`-style config shapes with camelCase fields.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { DaemonRuntime, Identity, ReplicaGroup } from '@net-mesh/sdk';
17
+ *
18
+ * // Register the factory the group will invoke for each member.
19
+ * rt.registerFactory('counter', () => new CounterDaemon());
20
+ *
21
+ * // Spawn a 3-member replica group. `spawn` is async — the
22
+ * // factory TSFN round-trip runs on a tokio worker so the Node
23
+ * // main thread stays free to execute JS factory callbacks.
24
+ * const group = await ReplicaGroup.spawn(rt, 'counter', {
25
+ * replicaCount: 3,
26
+ * groupSeed: Buffer.alloc(32, 0x11), // 32 bytes
27
+ * lbStrategy: 'round-robin',
28
+ * });
29
+ *
30
+ * // Route a request.
31
+ * const origin = group.routeEvent({ routingKey: 'user:42' });
32
+ * await rt.deliver(origin, someEvent);
33
+ * ```
34
+ *
35
+ * @packageDocumentation
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.StandbyGroup = exports.ForkGroup = exports.ReplicaGroup = exports.GroupError = void 0;
39
+ const _internal_js_1 = require("./_internal.js");
40
+ const compute_1 = require("./compute");
41
+ /**
42
+ * Typed group failure. Subclass of {@link DaemonError} so
43
+ * `catch (e: DaemonError)` still matches.
44
+ */
45
+ class GroupError extends compute_1.DaemonError {
46
+ kind;
47
+ /** Optional detail string carried by `placement-failed` /
48
+ * `registry-failed` / `invalid-config` / `daemon` variants. */
49
+ detail;
50
+ /** `factory-not-found` carries the requested kind name. */
51
+ requestedKind;
52
+ constructor(kind, message, extras = {}) {
53
+ super(message);
54
+ this.name = 'GroupError';
55
+ this.kind = kind;
56
+ this.detail = extras.detail;
57
+ this.requestedKind = extras.requestedKind;
58
+ Object.setPrototypeOf(this, GroupError.prototype);
59
+ }
60
+ }
61
+ exports.GroupError = GroupError;
62
+ /**
63
+ * Lift a caught unknown error into the right typed exception.
64
+ * The Rust side emits `daemon: group: <kind>[: detail]`; unknown
65
+ * kinds fall back to `kind: 'unknown'` with the raw body.
66
+ * Non-group errors pass through as plain `DaemonError` (or the
67
+ * original throw if the message doesn't start with `daemon:`).
68
+ */
69
+ function toGroupError(e) {
70
+ const msg = e?.message ?? String(e);
71
+ if (msg.startsWith('daemon:')) {
72
+ const body = msg.slice('daemon:'.length).trim();
73
+ if (body.startsWith('group:')) {
74
+ throw parseGroupError(body, msg.slice('daemon:'.length).trim());
75
+ }
76
+ throw new compute_1.DaemonError(body);
77
+ }
78
+ throw e;
79
+ }
80
+ function parseGroupError(body, fullMessage) {
81
+ // Body shape after `daemon: ` stripping:
82
+ // group: <kind>
83
+ // group: <kind>: <detail>
84
+ const afterPrefix = body.slice('group:'.length).trim();
85
+ const firstColon = afterPrefix.indexOf(':');
86
+ const kind = firstColon === -1 ? afterPrefix : afterPrefix.slice(0, firstColon).trim();
87
+ const rest = firstColon === -1 ? '' : afterPrefix.slice(firstColon + 1).trim();
88
+ switch (kind) {
89
+ case 'not-ready':
90
+ case 'no-healthy-member':
91
+ return new GroupError(kind, fullMessage);
92
+ case 'factory-not-found':
93
+ return new GroupError(kind, fullMessage, { requestedKind: rest });
94
+ case 'placement-failed':
95
+ case 'registry-failed':
96
+ case 'invalid-config':
97
+ case 'daemon':
98
+ return new GroupError(kind, fullMessage, { detail: rest });
99
+ default:
100
+ return new GroupError('unknown', fullMessage);
101
+ }
102
+ }
103
+ function toBuffer(seed) {
104
+ return Buffer.isBuffer(seed) ? seed : Buffer.from(seed);
105
+ }
106
+ // ----------------------------------------------------------------------------
107
+ // ReplicaGroup
108
+ // ----------------------------------------------------------------------------
109
+ /**
110
+ * N interchangeable copies of a daemon. Each replica has a
111
+ * deterministic identity derived from `groupSeed + index`;
112
+ * the group load-balances inbound events across healthy members
113
+ * and auto-replaces members on node failure.
114
+ */
115
+ class ReplicaGroup {
116
+ inner;
117
+ /** @internal */
118
+ constructor(inner) {
119
+ this.inner = inner;
120
+ }
121
+ /**
122
+ * Spawn a replica group bound to `runtime`. `kind` must have
123
+ * been registered via {@link DaemonRuntime.registerFactory};
124
+ * the group calls the factory once per replica at spawn and
125
+ * again on scale-up / failure replacement.
126
+ *
127
+ * Async because the underlying SDK `spawn` runs on a tokio
128
+ * worker — the factory TSFN round-trip needs the Node main
129
+ * thread free to execute the JS factory callback, so a sync
130
+ * main-thread spawn would deadlock.
131
+ *
132
+ * Throws {@link GroupError} on `not-ready`, `factory-not-found`,
133
+ * `placement-failed`, `invalid-config`, or `registry-failed`.
134
+ */
135
+ static async spawn(runtime, kind, config) {
136
+ try {
137
+ const napi = await (0, _internal_js_1.getNapiRuntime)(runtime).spawnReplicaGroup(kind, {
138
+ replicaCount: config.replicaCount,
139
+ groupSeed: toBuffer(config.groupSeed),
140
+ lbStrategy: config.lbStrategy,
141
+ hostConfig: config.hostConfig,
142
+ });
143
+ return new ReplicaGroup(napi);
144
+ }
145
+ catch (e) {
146
+ return toGroupError(e);
147
+ }
148
+ }
149
+ /** Route to the best-available replica; returns the target
150
+ * `origin_hash` which the caller feeds to `runtime.deliver`. */
151
+ routeEvent(ctx = {}) {
152
+ try {
153
+ return this.inner.routeEvent(ctx);
154
+ }
155
+ catch (e) {
156
+ return toGroupError(e);
157
+ }
158
+ }
159
+ /** Resize the group to `n` members. The kind is fixed at
160
+ * spawn time and not accepted here — see the class docstring
161
+ * for why. Async because growing invokes the factory (TSFN)
162
+ * once per new replica. */
163
+ async scaleTo(n) {
164
+ try {
165
+ await this.inner.scaleTo(n);
166
+ }
167
+ catch (e) {
168
+ toGroupError(e);
169
+ }
170
+ }
171
+ /** Replace all members on `failedNodeId` onto other nodes.
172
+ * Returns the indices of replicas that were respawned.
173
+ * Reuses the group's spawn kind. */
174
+ async onNodeFailure(failedNodeId) {
175
+ try {
176
+ return await this.inner.onNodeFailure(failedNodeId);
177
+ }
178
+ catch (e) {
179
+ return toGroupError(e);
180
+ }
181
+ }
182
+ onNodeRecovery(recoveredNodeId) {
183
+ try {
184
+ this.inner.onNodeRecovery(recoveredNodeId);
185
+ }
186
+ catch (e) {
187
+ toGroupError(e);
188
+ }
189
+ }
190
+ get health() {
191
+ return this.inner.health;
192
+ }
193
+ get groupId() {
194
+ return this.inner.groupId;
195
+ }
196
+ get replicas() {
197
+ return this.inner.replicas;
198
+ }
199
+ get replicaCount() {
200
+ return this.inner.replicaCount;
201
+ }
202
+ get healthyCount() {
203
+ return this.inner.healthyCount;
204
+ }
205
+ }
206
+ exports.ReplicaGroup = ReplicaGroup;
207
+ // ----------------------------------------------------------------------------
208
+ // ForkGroup
209
+ // ----------------------------------------------------------------------------
210
+ /**
211
+ * N independent daemons forked from a common parent at
212
+ * `forkSeq`. Unique identities, shared ancestry via `ForkRecord`.
213
+ */
214
+ class ForkGroup {
215
+ inner;
216
+ /** @internal */
217
+ constructor(inner) {
218
+ this.inner = inner;
219
+ }
220
+ /** Fork `config.forkCount` new daemons from `parentOrigin` at
221
+ * `forkSeq`. Each fork gets a fresh unique keypair + a
222
+ * `ForkRecord` linking it to the parent. Async for the same
223
+ * deadlock-avoidance reason as {@link ReplicaGroup.spawn}. */
224
+ static async fork(runtime, kind, parentOrigin, forkSeq, config) {
225
+ try {
226
+ const napi = await (0, _internal_js_1.getNapiRuntime)(runtime).spawnForkGroup(kind, parentOrigin, forkSeq, config);
227
+ return new ForkGroup(napi);
228
+ }
229
+ catch (e) {
230
+ return toGroupError(e);
231
+ }
232
+ }
233
+ routeEvent(ctx = {}) {
234
+ try {
235
+ return this.inner.routeEvent(ctx);
236
+ }
237
+ catch (e) {
238
+ return toGroupError(e);
239
+ }
240
+ }
241
+ async scaleTo(n) {
242
+ try {
243
+ await this.inner.scaleTo(n);
244
+ }
245
+ catch (e) {
246
+ toGroupError(e);
247
+ }
248
+ }
249
+ async onNodeFailure(failedNodeId) {
250
+ try {
251
+ return await this.inner.onNodeFailure(failedNodeId);
252
+ }
253
+ catch (e) {
254
+ return toGroupError(e);
255
+ }
256
+ }
257
+ onNodeRecovery(recoveredNodeId) {
258
+ try {
259
+ this.inner.onNodeRecovery(recoveredNodeId);
260
+ }
261
+ catch (e) {
262
+ toGroupError(e);
263
+ }
264
+ }
265
+ get health() {
266
+ return this.inner.health;
267
+ }
268
+ get parentOrigin() {
269
+ return this.inner.parentOrigin;
270
+ }
271
+ get forkSeq() {
272
+ return this.inner.forkSeq;
273
+ }
274
+ get forkRecords() {
275
+ return this.inner.forkRecords;
276
+ }
277
+ /** `true` iff every fork's `ForkRecord` verifies against its
278
+ * parent. Core performs the signature + sentinel checks. */
279
+ verifyLineage() {
280
+ return this.inner.verifyLineage();
281
+ }
282
+ get members() {
283
+ return this.inner.members;
284
+ }
285
+ get forkCount() {
286
+ return this.inner.forkCount;
287
+ }
288
+ get healthyCount() {
289
+ return this.inner.healthyCount;
290
+ }
291
+ }
292
+ exports.ForkGroup = ForkGroup;
293
+ // ----------------------------------------------------------------------------
294
+ // StandbyGroup
295
+ // ----------------------------------------------------------------------------
296
+ /**
297
+ * Active-passive replication. One active processes events; N−1
298
+ * standbys hold snapshots and catch up via {@link sync}. On
299
+ * active failure, {@link promote} (or automatic failover via
300
+ * {@link onNodeFailure}) picks the most-synced standby.
301
+ *
302
+ * **Automatic replay buffering.** The group installs a
303
+ * post-delivery observer on its active member's origin at spawn
304
+ * and re-points it on promote / failover. Every
305
+ * `runtime.deliver(group.activeOrigin, event)` automatically
306
+ * feeds the standby replay buffer — no paired
307
+ * `onEventDelivered` call required from the caller. The method
308
+ * remains on the class for test scenarios that simulate a gap
309
+ * without a live runtime; production code should ignore it.
310
+ */
311
+ class StandbyGroup {
312
+ inner;
313
+ /** @internal */
314
+ constructor(inner) {
315
+ this.inner = inner;
316
+ }
317
+ static async spawn(runtime, kind, config) {
318
+ try {
319
+ const napi = await (0, _internal_js_1.getNapiRuntime)(runtime).spawnStandbyGroup(kind, {
320
+ memberCount: config.memberCount,
321
+ groupSeed: toBuffer(config.groupSeed),
322
+ hostConfig: config.hostConfig,
323
+ });
324
+ return new StandbyGroup(napi);
325
+ }
326
+ catch (e) {
327
+ return toGroupError(e);
328
+ }
329
+ }
330
+ /** `origin_hash` of the current active. Target for inbound
331
+ * events; standbys don't process inputs. */
332
+ get activeOrigin() {
333
+ return this.inner.activeOrigin;
334
+ }
335
+ /** Snapshot the active and push to every standby. Returns the
336
+ * sequence number the sync caught up through. */
337
+ async sync() {
338
+ try {
339
+ return await this.inner.syncStandbys();
340
+ }
341
+ catch (e) {
342
+ return toGroupError(e);
343
+ }
344
+ }
345
+ /**
346
+ * **Test-only.** Manually push an event into the replay
347
+ * buffer. Production code does NOT need to call this — the
348
+ * post-delivery observer installed at `spawn` / `promote`
349
+ * automatically feeds the buffer on every
350
+ * `runtime.deliver(group.activeOrigin, event)`. Exposed so
351
+ * tests can simulate a gap between the last sync and a
352
+ * failure without driving a live runtime. Not part of the
353
+ * stable public API.
354
+ *
355
+ * @internal
356
+ */
357
+ onEventDelivered(event) {
358
+ try {
359
+ this.inner.onEventDelivered(event);
360
+ }
361
+ catch (e) {
362
+ toGroupError(e);
363
+ }
364
+ }
365
+ /** Promote the most-synced standby to active. Reuses the
366
+ * group's spawn kind — no external parameter, so callers
367
+ * can't accidentally promote with the wrong factory. Call
368
+ * manually for planned failover; {@link onNodeFailure} calls
369
+ * automatically when the active's node fails. */
370
+ async promote() {
371
+ try {
372
+ return await this.inner.promote();
373
+ }
374
+ catch (e) {
375
+ return toGroupError(e);
376
+ }
377
+ }
378
+ /** Handle node failure. Returns the new active's `origin_hash`
379
+ * if the active was on `failedNodeId`; `null` if only standbys
380
+ * were affected. Reuses the group's spawn kind. */
381
+ async onNodeFailure(failedNodeId) {
382
+ try {
383
+ const r = await this.inner.onNodeFailure(failedNodeId);
384
+ return r ?? null;
385
+ }
386
+ catch (e) {
387
+ return toGroupError(e);
388
+ }
389
+ }
390
+ onNodeRecovery(recoveredNodeId) {
391
+ try {
392
+ this.inner.onNodeRecovery(recoveredNodeId);
393
+ }
394
+ catch (e) {
395
+ toGroupError(e);
396
+ }
397
+ }
398
+ get health() {
399
+ return this.inner.health;
400
+ }
401
+ get activeHealthy() {
402
+ return this.inner.activeHealthy;
403
+ }
404
+ get activeIndex() {
405
+ return this.inner.activeIndex;
406
+ }
407
+ /** `"active"` | `"standby"` | `null` (out-of-range). */
408
+ memberRole(index) {
409
+ const r = this.inner.memberRole(index);
410
+ return r ?? null;
411
+ }
412
+ syncedThrough(index) {
413
+ return this.inner.syncedThrough(index) ?? null;
414
+ }
415
+ get bufferedEventCount() {
416
+ return this.inner.bufferedEventCount;
417
+ }
418
+ get groupId() {
419
+ return this.inner.groupId;
420
+ }
421
+ get members() {
422
+ return this.inner.members;
423
+ }
424
+ get memberCount() {
425
+ return this.inner.memberCount;
426
+ }
427
+ get standbyCount() {
428
+ return this.inner.standbyCount;
429
+ }
430
+ }
431
+ exports.StandbyGroup = StandbyGroup;