@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/groups.d.ts
ADDED
|
@@ -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;
|