@xmoxmo/bncr 0.6.6 → 0.6.8
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/dist/index.js +1386 -206
- package/index.ts +291 -131
- package/package.json +1 -1
- package/scripts/check-pack.mjs +2 -0
- package/scripts/selfcheck.mjs +2 -0
- package/src/bootstrap/channel-plugin-runtime.ts +28 -7
- package/src/bootstrap/register-lifecycle.ts +492 -0
- package/src/bootstrap/register-runtime-gateway.ts +64 -3
- package/src/bootstrap/register-runtime-helpers.ts +8 -44
- package/src/bootstrap/register-runtime-observation.ts +170 -0
- package/src/bootstrap/register-runtime-singleton.ts +44 -40
- package/src/bootstrap/register-runtime.ts +918 -15
- package/src/channel.ts +25 -2
- package/src/messaging/inbound/commands.ts +75 -65
- package/src/messaging/inbound/conversation-history.ts +28 -1
- package/src/plugin/bridge-lifecycle.ts +13 -2
- package/src/plugin/config.ts +18 -0
- package/src/plugin/inbound-handlers.ts +11 -5
- package/src/plugin/sqlite-state.ts +6 -23
- package/src/plugin/state-store.ts +3 -30
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
export type LifecyclePhase = 'registering' | 'active' | 'stopping' | 'stopped' | 'failed';
|
|
2
|
+
|
|
3
|
+
export type LifecycleKey = {
|
|
4
|
+
generation: number;
|
|
5
|
+
apiInstanceId: string;
|
|
6
|
+
registryFingerprint: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type BridgeGeneration = {
|
|
10
|
+
moduleEpoch: string;
|
|
11
|
+
bridgeFactoryId: string;
|
|
12
|
+
pluginVersion: string;
|
|
13
|
+
registrationMode: string;
|
|
14
|
+
pluginRoot: string;
|
|
15
|
+
pluginFile: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LifecycleOwner = {
|
|
19
|
+
key: LifecycleKey;
|
|
20
|
+
bridgeGeneration: BridgeGeneration;
|
|
21
|
+
phase: LifecyclePhase;
|
|
22
|
+
startedAt: number | null;
|
|
23
|
+
stopRequestedAt: number | null;
|
|
24
|
+
stoppedAt: number | null;
|
|
25
|
+
failure: string | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LifecycleRetirementOutcome =
|
|
29
|
+
| { ok: true }
|
|
30
|
+
| {
|
|
31
|
+
ok: false;
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type LifecycleAdoptionDecision =
|
|
36
|
+
| {
|
|
37
|
+
kind: 'initialize';
|
|
38
|
+
owner: LifecycleOwner;
|
|
39
|
+
reason: 'no-active-lifecycle';
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
kind: 'duplicate';
|
|
43
|
+
owner: LifecycleOwner;
|
|
44
|
+
reason:
|
|
45
|
+
| 'active-owner'
|
|
46
|
+
| 'pending-successor'
|
|
47
|
+
| 'lifecycle-stopping'
|
|
48
|
+
| 'same-lifecycle-stopped';
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
kind: 'takeover';
|
|
52
|
+
owner: LifecycleOwner;
|
|
53
|
+
mode: 'reuse' | 'replace';
|
|
54
|
+
reason: 'bridge-generation-reused' | 'bridge-generation-changed';
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
kind: 'defer';
|
|
58
|
+
owner: LifecycleOwner;
|
|
59
|
+
predecessorGeneration: number;
|
|
60
|
+
mode: 'reuse' | 'replace';
|
|
61
|
+
reason: 'predecessor-stopping';
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
kind: 'reject';
|
|
65
|
+
owner: LifecycleOwner;
|
|
66
|
+
reason:
|
|
67
|
+
| 'predecessor-failed'
|
|
68
|
+
| 'pending-successor-conflict'
|
|
69
|
+
| 'registration-generation-conflict'
|
|
70
|
+
| 'registration-failed'
|
|
71
|
+
| 'registration-in-progress';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type RegisterLifecycleState = {
|
|
75
|
+
nextGeneration: number;
|
|
76
|
+
active?: LifecycleOwner;
|
|
77
|
+
pending?: LifecycleOwner;
|
|
78
|
+
pendingMode?: 'reuse' | 'replace';
|
|
79
|
+
retirement?: Promise<LifecycleRetirementOutcome>;
|
|
80
|
+
resolveRetirement?: (outcome: LifecycleRetirementOutcome) => void;
|
|
81
|
+
staleCallbackSuppressions: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type LifecyclePlanInput = {
|
|
85
|
+
apiInstanceId: string;
|
|
86
|
+
registryFingerprint: string;
|
|
87
|
+
bridgeGeneration: BridgeGeneration;
|
|
88
|
+
now: number;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function normalizeString(value: unknown, fallback = 'unknown') {
|
|
92
|
+
const text = typeof value === 'string' ? value.trim() : '';
|
|
93
|
+
return text || fallback;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createRegisterLifecycleState(): RegisterLifecycleState {
|
|
97
|
+
return {
|
|
98
|
+
nextGeneration: 1,
|
|
99
|
+
staleCallbackSuppressions: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function sameLifecycleRegistration(left?: LifecycleKey, right?: LifecycleKey) {
|
|
104
|
+
if (!left || !right) return false;
|
|
105
|
+
return (
|
|
106
|
+
left.apiInstanceId === right.apiInstanceId &&
|
|
107
|
+
left.registryFingerprint === right.registryFingerprint
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function sameBridgeGeneration(left?: BridgeGeneration, right?: BridgeGeneration) {
|
|
112
|
+
if (!left || !right) return false;
|
|
113
|
+
return (
|
|
114
|
+
left.moduleEpoch === right.moduleEpoch &&
|
|
115
|
+
left.bridgeFactoryId === right.bridgeFactoryId &&
|
|
116
|
+
left.pluginVersion === right.pluginVersion &&
|
|
117
|
+
left.registrationMode === right.registrationMode &&
|
|
118
|
+
left.pluginRoot === right.pluginRoot &&
|
|
119
|
+
left.pluginFile === right.pluginFile
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getBridgeGenerationKey(generation: BridgeGeneration) {
|
|
124
|
+
return [
|
|
125
|
+
generation.moduleEpoch,
|
|
126
|
+
generation.bridgeFactoryId,
|
|
127
|
+
generation.pluginVersion,
|
|
128
|
+
generation.registrationMode,
|
|
129
|
+
generation.pluginRoot,
|
|
130
|
+
generation.pluginFile,
|
|
131
|
+
].join('\u0000');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createLifecycleOwner(
|
|
135
|
+
input: LifecyclePlanInput,
|
|
136
|
+
generation: number,
|
|
137
|
+
phase: LifecyclePhase,
|
|
138
|
+
): LifecycleOwner {
|
|
139
|
+
return {
|
|
140
|
+
key: {
|
|
141
|
+
generation,
|
|
142
|
+
apiInstanceId: normalizeString(input.apiInstanceId, 'unknown-api'),
|
|
143
|
+
registryFingerprint: normalizeString(input.registryFingerprint, 'unknown-registry'),
|
|
144
|
+
},
|
|
145
|
+
bridgeGeneration: {
|
|
146
|
+
moduleEpoch: normalizeString(input.bridgeGeneration.moduleEpoch),
|
|
147
|
+
bridgeFactoryId: normalizeString(input.bridgeGeneration.bridgeFactoryId),
|
|
148
|
+
pluginVersion: normalizeString(input.bridgeGeneration.pluginVersion),
|
|
149
|
+
registrationMode: normalizeString(input.bridgeGeneration.registrationMode),
|
|
150
|
+
pluginRoot: normalizeString(input.bridgeGeneration.pluginRoot),
|
|
151
|
+
pluginFile: normalizeString(input.bridgeGeneration.pluginFile),
|
|
152
|
+
},
|
|
153
|
+
phase,
|
|
154
|
+
startedAt: phase === 'active' ? input.now : null,
|
|
155
|
+
stopRequestedAt: null,
|
|
156
|
+
stoppedAt: null,
|
|
157
|
+
failure: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function planLifecycleAdoption(
|
|
162
|
+
state: RegisterLifecycleState,
|
|
163
|
+
input: LifecyclePlanInput,
|
|
164
|
+
): LifecycleAdoptionDecision {
|
|
165
|
+
const owner = createLifecycleOwner(input, state.nextGeneration, 'active');
|
|
166
|
+
const active = state.active;
|
|
167
|
+
|
|
168
|
+
if (!active) {
|
|
169
|
+
return {
|
|
170
|
+
kind: 'initialize',
|
|
171
|
+
owner,
|
|
172
|
+
reason: 'no-active-lifecycle',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (active.phase === 'failed') {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'reject',
|
|
179
|
+
owner,
|
|
180
|
+
reason: 'predecessor-failed',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (state.pending) {
|
|
185
|
+
const sameRegistration = sameLifecycleRegistration(state.pending.key, owner.key);
|
|
186
|
+
const sameGeneration = sameBridgeGeneration(
|
|
187
|
+
state.pending.bridgeGeneration,
|
|
188
|
+
owner.bridgeGeneration,
|
|
189
|
+
);
|
|
190
|
+
if (sameRegistration && sameGeneration) {
|
|
191
|
+
return {
|
|
192
|
+
kind: 'duplicate',
|
|
193
|
+
owner,
|
|
194
|
+
reason: 'pending-successor',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (sameRegistration) {
|
|
198
|
+
return {
|
|
199
|
+
kind: 'reject',
|
|
200
|
+
owner,
|
|
201
|
+
reason: 'registration-generation-conflict',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: 'reject',
|
|
206
|
+
owner,
|
|
207
|
+
reason: 'pending-successor-conflict',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (active.phase === 'active' || active.phase === 'registering') {
|
|
212
|
+
if (
|
|
213
|
+
sameLifecycleRegistration(active.key, owner.key) &&
|
|
214
|
+
!sameBridgeGeneration(active.bridgeGeneration, owner.bridgeGeneration)
|
|
215
|
+
) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'reject',
|
|
218
|
+
owner,
|
|
219
|
+
reason: 'registration-generation-conflict',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
kind: 'duplicate',
|
|
224
|
+
owner,
|
|
225
|
+
reason: 'active-owner',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (active.phase === 'stopping') {
|
|
230
|
+
const sameRegistration = sameLifecycleRegistration(active.key, owner.key);
|
|
231
|
+
const sameGeneration = sameBridgeGeneration(active.bridgeGeneration, owner.bridgeGeneration);
|
|
232
|
+
if (sameRegistration && sameGeneration) {
|
|
233
|
+
return {
|
|
234
|
+
kind: 'duplicate',
|
|
235
|
+
owner,
|
|
236
|
+
reason: 'lifecycle-stopping',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (sameRegistration) {
|
|
240
|
+
return {
|
|
241
|
+
kind: 'reject',
|
|
242
|
+
owner,
|
|
243
|
+
reason: 'registration-generation-conflict',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
kind: 'defer',
|
|
248
|
+
owner,
|
|
249
|
+
predecessorGeneration: active.key.generation,
|
|
250
|
+
mode: sameBridgeGeneration(active.bridgeGeneration, owner.bridgeGeneration)
|
|
251
|
+
? 'reuse'
|
|
252
|
+
: 'replace',
|
|
253
|
+
reason: 'predecessor-stopping',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (sameLifecycleRegistration(active.key, owner.key)) {
|
|
258
|
+
if (!sameBridgeGeneration(active.bridgeGeneration, owner.bridgeGeneration)) {
|
|
259
|
+
return {
|
|
260
|
+
kind: 'reject',
|
|
261
|
+
owner,
|
|
262
|
+
reason: 'registration-generation-conflict',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: 'duplicate',
|
|
267
|
+
owner,
|
|
268
|
+
reason: 'same-lifecycle-stopped',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (sameBridgeGeneration(active.bridgeGeneration, owner.bridgeGeneration)) {
|
|
273
|
+
return {
|
|
274
|
+
kind: 'takeover',
|
|
275
|
+
owner,
|
|
276
|
+
mode: 'reuse',
|
|
277
|
+
reason: 'bridge-generation-reused',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
kind: 'takeover',
|
|
283
|
+
owner,
|
|
284
|
+
mode: 'replace',
|
|
285
|
+
reason: 'bridge-generation-changed',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function commitLifecycleAdoption(
|
|
290
|
+
state: RegisterLifecycleState,
|
|
291
|
+
decision: LifecycleAdoptionDecision,
|
|
292
|
+
) {
|
|
293
|
+
if (decision.kind === 'duplicate' || decision.kind === 'reject') {
|
|
294
|
+
return state.active;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
state.nextGeneration = Math.max(state.nextGeneration + 1, decision.owner.key.generation + 1);
|
|
298
|
+
|
|
299
|
+
if (decision.kind === 'defer') {
|
|
300
|
+
state.pending = decision.owner;
|
|
301
|
+
state.pendingMode = decision.mode;
|
|
302
|
+
return state.active;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
state.active = decision.owner;
|
|
306
|
+
state.pending = undefined;
|
|
307
|
+
state.pendingMode = undefined;
|
|
308
|
+
state.retirement = undefined;
|
|
309
|
+
state.resolveRetirement = undefined;
|
|
310
|
+
return state.active;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function beginLifecycleRetirement(
|
|
314
|
+
state: RegisterLifecycleState,
|
|
315
|
+
registryFingerprint: string,
|
|
316
|
+
now: number,
|
|
317
|
+
expectedGeneration?: number,
|
|
318
|
+
) {
|
|
319
|
+
const active = state.active;
|
|
320
|
+
if (
|
|
321
|
+
!active ||
|
|
322
|
+
active.key.registryFingerprint !== registryFingerprint ||
|
|
323
|
+
(expectedGeneration !== undefined && active.key.generation !== expectedGeneration) ||
|
|
324
|
+
(active.phase !== 'active' && active.phase !== 'registering')
|
|
325
|
+
) {
|
|
326
|
+
state.staleCallbackSuppressions += 1;
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let resolveRetirement!: (outcome: LifecycleRetirementOutcome) => void;
|
|
331
|
+
state.retirement = new Promise<LifecycleRetirementOutcome>((resolve) => {
|
|
332
|
+
resolveRetirement = resolve;
|
|
333
|
+
});
|
|
334
|
+
state.resolveRetirement = resolveRetirement;
|
|
335
|
+
active.phase = 'stopping';
|
|
336
|
+
active.stopRequestedAt = now;
|
|
337
|
+
return state.retirement;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function settleLifecycleRetirement(
|
|
341
|
+
state: RegisterLifecycleState,
|
|
342
|
+
registryFingerprint: string,
|
|
343
|
+
outcome: LifecycleRetirementOutcome,
|
|
344
|
+
now: number,
|
|
345
|
+
expectedGeneration?: number,
|
|
346
|
+
) {
|
|
347
|
+
const active = state.active;
|
|
348
|
+
if (
|
|
349
|
+
!active ||
|
|
350
|
+
active.key.registryFingerprint !== registryFingerprint ||
|
|
351
|
+
(expectedGeneration !== undefined && active.key.generation !== expectedGeneration) ||
|
|
352
|
+
active.phase !== 'stopping'
|
|
353
|
+
) {
|
|
354
|
+
state.staleCallbackSuppressions += 1;
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const resolveRetirement = state.resolveRetirement;
|
|
359
|
+
state.resolveRetirement = undefined;
|
|
360
|
+
active.phase = outcome.ok ? 'stopped' : 'failed';
|
|
361
|
+
active.stoppedAt = outcome.ok ? now : null;
|
|
362
|
+
active.failure = outcome.ok ? null : outcome.error;
|
|
363
|
+
resolveRetirement?.(outcome);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function activatePendingLifecycle(
|
|
368
|
+
state: RegisterLifecycleState,
|
|
369
|
+
registryFingerprint: string,
|
|
370
|
+
now: number,
|
|
371
|
+
expectedGeneration?: number,
|
|
372
|
+
) {
|
|
373
|
+
const pending = state.pending;
|
|
374
|
+
if (
|
|
375
|
+
!pending ||
|
|
376
|
+
pending.key.registryFingerprint !== registryFingerprint ||
|
|
377
|
+
(expectedGeneration !== undefined && pending.key.generation !== expectedGeneration)
|
|
378
|
+
) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (state.active?.phase !== 'stopped') {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
pending.phase = 'active';
|
|
386
|
+
pending.startedAt = now;
|
|
387
|
+
state.active = pending;
|
|
388
|
+
state.pending = undefined;
|
|
389
|
+
state.pendingMode = undefined;
|
|
390
|
+
state.retirement = undefined;
|
|
391
|
+
state.resolveRetirement = undefined;
|
|
392
|
+
return state.active;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function reactivateStoppedLifecycle(
|
|
396
|
+
state: RegisterLifecycleState,
|
|
397
|
+
registryFingerprint: string,
|
|
398
|
+
now: number,
|
|
399
|
+
expectedGeneration?: number,
|
|
400
|
+
) {
|
|
401
|
+
const active = state.active;
|
|
402
|
+
if (
|
|
403
|
+
state.pending ||
|
|
404
|
+
!active ||
|
|
405
|
+
active.key.registryFingerprint !== registryFingerprint ||
|
|
406
|
+
(expectedGeneration !== undefined && active.key.generation !== expectedGeneration) ||
|
|
407
|
+
active.phase !== 'stopped'
|
|
408
|
+
) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
active.phase = 'active';
|
|
412
|
+
active.startedAt = now;
|
|
413
|
+
active.stopRequestedAt = null;
|
|
414
|
+
active.stoppedAt = null;
|
|
415
|
+
active.failure = null;
|
|
416
|
+
return active;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function failLifecycle(
|
|
420
|
+
state: RegisterLifecycleState,
|
|
421
|
+
registryFingerprint: string,
|
|
422
|
+
error: unknown,
|
|
423
|
+
expectedGeneration?: number,
|
|
424
|
+
) {
|
|
425
|
+
const active = state.active;
|
|
426
|
+
if (
|
|
427
|
+
!active ||
|
|
428
|
+
active.key.registryFingerprint !== registryFingerprint ||
|
|
429
|
+
(expectedGeneration !== undefined && active.key.generation !== expectedGeneration)
|
|
430
|
+
) {
|
|
431
|
+
state.staleCallbackSuppressions += 1;
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
if (active.phase !== 'active' && active.phase !== 'registering') {
|
|
435
|
+
state.staleCallbackSuppressions += 1;
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
active.phase = 'failed';
|
|
439
|
+
active.stoppedAt = null;
|
|
440
|
+
active.failure = error instanceof Error ? error.message : String(error);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function getActiveLifecycle(state: RegisterLifecycleState) {
|
|
445
|
+
return state.active;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function getPendingLifecycle(state: RegisterLifecycleState) {
|
|
449
|
+
return state.pending;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function isLifecycleRegistryActive(
|
|
453
|
+
state: RegisterLifecycleState,
|
|
454
|
+
registryFingerprint: string,
|
|
455
|
+
expectedGeneration?: number,
|
|
456
|
+
) {
|
|
457
|
+
return Boolean(
|
|
458
|
+
state.active?.key.registryFingerprint === registryFingerprint &&
|
|
459
|
+
(expectedGeneration === undefined || state.active.key.generation === expectedGeneration) &&
|
|
460
|
+
(state.active.phase === 'active' || state.active.phase === 'registering'),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function isLifecycleRegistryStoppable(
|
|
465
|
+
state: RegisterLifecycleState,
|
|
466
|
+
registryFingerprint: string,
|
|
467
|
+
expectedGeneration?: number,
|
|
468
|
+
) {
|
|
469
|
+
const active = state.active;
|
|
470
|
+
return Boolean(
|
|
471
|
+
active?.key.registryFingerprint === registryFingerprint &&
|
|
472
|
+
(expectedGeneration === undefined || active.key.generation === expectedGeneration) &&
|
|
473
|
+
(active.phase === 'registering' || active.phase === 'active' || active.phase === 'stopping'),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function dumpLifecycleOwner(owner?: LifecycleOwner) {
|
|
478
|
+
if (!owner) return null;
|
|
479
|
+
return {
|
|
480
|
+
generation: owner.key.generation,
|
|
481
|
+
apiInstanceId: owner.key.apiInstanceId,
|
|
482
|
+
registryFingerprint: owner.key.registryFingerprint,
|
|
483
|
+
phase: owner.phase,
|
|
484
|
+
moduleEpoch: owner.bridgeGeneration.moduleEpoch,
|
|
485
|
+
bridgeFactoryId: owner.bridgeGeneration.bridgeFactoryId,
|
|
486
|
+
pluginVersion: owner.bridgeGeneration.pluginVersion,
|
|
487
|
+
startedAt: owner.startedAt,
|
|
488
|
+
stopRequestedAt: owner.stopRequestedAt,
|
|
489
|
+
stoppedAt: owner.stoppedAt,
|
|
490
|
+
failure: owner.failure,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitBncrLogLine } from '../core/logging.ts';
|
|
2
|
+
import type { RegisterLifecycleState } from './register-lifecycle.ts';
|
|
2
3
|
import type { BridgeRegisterStateCarrier } from './register-runtime-helpers.ts';
|
|
3
4
|
import type { ChannelModule } from './runtime-loader.ts';
|
|
4
5
|
|
|
@@ -39,24 +40,76 @@ export function createBncrGatewayMethodRegistry(runtime: {
|
|
|
39
40
|
getGatewayRuntime: () => {
|
|
40
41
|
currentBridge?: BridgeSingleton;
|
|
41
42
|
registeredMethodsByRegistry: Map<string, Set<BncrGatewayMethodName>>;
|
|
43
|
+
gatewayMethodDispatchers?: Partial<
|
|
44
|
+
Record<
|
|
45
|
+
BncrGatewayMethodName,
|
|
46
|
+
(bridge: BridgeSingleton, opts: BridgeGatewayHandlerContext) => BridgeGatewayHandlerResult
|
|
47
|
+
>
|
|
48
|
+
>;
|
|
49
|
+
registryDeclarations?: Map<
|
|
50
|
+
string,
|
|
51
|
+
{
|
|
52
|
+
state: 'shadow' | 'active' | 'retired';
|
|
53
|
+
registration?: 'pending' | 'complete' | 'failed';
|
|
54
|
+
}
|
|
55
|
+
>;
|
|
56
|
+
lifecycle?: RegisterLifecycleState;
|
|
42
57
|
};
|
|
43
58
|
gatewayMethodDispatchers: Record<
|
|
44
59
|
BncrGatewayMethodName,
|
|
45
60
|
(bridge: BridgeSingleton, opts: BridgeGatewayHandlerContext) => BridgeGatewayHandlerResult
|
|
46
61
|
>;
|
|
47
62
|
getBridgeRegisterStateCarrier: (bridge: BridgeSingleton) => BridgeRegisterStateCarrier;
|
|
63
|
+
getRegistryRuntimeObservation?: (registryFingerprint: string) => unknown;
|
|
48
64
|
}) {
|
|
49
65
|
const dispatchGatewayMethod = (
|
|
50
66
|
name: BncrGatewayMethodName,
|
|
51
67
|
opts: BridgeGatewayHandlerContext,
|
|
68
|
+
registryFingerprint?: string,
|
|
52
69
|
) => {
|
|
53
70
|
const gatewayRuntime = runtime.getGatewayRuntime();
|
|
71
|
+
const declaration = registryFingerprint
|
|
72
|
+
? gatewayRuntime.registryDeclarations?.get(registryFingerprint)
|
|
73
|
+
: undefined;
|
|
74
|
+
const lifecycleFailed = gatewayRuntime.lifecycle?.active?.phase === 'failed';
|
|
75
|
+
const declarationFailed = declaration?.registration === 'failed';
|
|
76
|
+
const dispatchAllowed = declaration
|
|
77
|
+
? declaration.state !== 'retired' && !declarationFailed && !lifecycleFailed
|
|
78
|
+
: !gatewayRuntime.lifecycle?.active;
|
|
79
|
+
if (registryFingerprint && !dispatchAllowed) {
|
|
80
|
+
throw new Error(`bncr lifecycle registry is inactive for ${name}`);
|
|
81
|
+
}
|
|
54
82
|
const bridge = gatewayRuntime.currentBridge;
|
|
55
83
|
if (!bridge) {
|
|
56
84
|
throw new Error(`bncr gateway runtime unavailable for ${name}`);
|
|
57
85
|
}
|
|
86
|
+
const dispatcherOpts =
|
|
87
|
+
name === 'bncr.diagnostics' &&
|
|
88
|
+
registryFingerprint &&
|
|
89
|
+
runtime.getRegistryRuntimeObservation &&
|
|
90
|
+
typeof opts.respond === 'function'
|
|
91
|
+
? {
|
|
92
|
+
...opts,
|
|
93
|
+
respond: (...response: Parameters<typeof opts.respond>) => {
|
|
94
|
+
const [ok, payload, error, meta] = response;
|
|
95
|
+
let observation: unknown = null;
|
|
96
|
+
try {
|
|
97
|
+
observation = runtime.getRegistryRuntimeObservation?.(registryFingerprint) ?? null;
|
|
98
|
+
} catch {
|
|
99
|
+
observation = null;
|
|
100
|
+
}
|
|
101
|
+
const nextPayload =
|
|
102
|
+
ok && payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
103
|
+
? { ...payload, lifecycleObservation: observation }
|
|
104
|
+
: payload;
|
|
105
|
+
opts.respond(ok, nextPayload, error, meta);
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
: opts;
|
|
58
109
|
try {
|
|
59
|
-
|
|
110
|
+
const dispatcher =
|
|
111
|
+
gatewayRuntime.gatewayMethodDispatchers?.[name] || runtime.gatewayMethodDispatchers[name];
|
|
112
|
+
return dispatcher(bridge, dispatcherOpts);
|
|
60
113
|
} catch (error) {
|
|
61
114
|
const state = runtime.getBridgeRegisterStateCarrier(bridge) as BridgeStateReader;
|
|
62
115
|
const detail =
|
|
@@ -88,7 +141,15 @@ export function createBncrGatewayMethodRegistry(runtime: {
|
|
|
88
141
|
) => {
|
|
89
142
|
if (!Array.isArray(api?.methods)) return;
|
|
90
143
|
if (api.methods.some((item) => item?.name === name)) return;
|
|
91
|
-
api.methods.push({
|
|
144
|
+
api.methods.push({
|
|
145
|
+
name,
|
|
146
|
+
handler: (opts) =>
|
|
147
|
+
dispatchGatewayMethod(
|
|
148
|
+
name,
|
|
149
|
+
opts,
|
|
150
|
+
runtime.getRegisterMeta(api).registryFingerprint || runtime.getRegistryFingerprint(api),
|
|
151
|
+
),
|
|
152
|
+
});
|
|
92
153
|
};
|
|
93
154
|
|
|
94
155
|
const ensureGatewayMethodRegistered = (
|
|
@@ -115,7 +176,7 @@ export function createBncrGatewayMethodRegistry(runtime: {
|
|
|
115
176
|
return;
|
|
116
177
|
}
|
|
117
178
|
api.registerGatewayMethod(name, (opts: BridgeGatewayHandlerContext) =>
|
|
118
|
-
dispatchGatewayMethod(name, opts),
|
|
179
|
+
dispatchGatewayMethod(name, opts, registryFingerprint),
|
|
119
180
|
);
|
|
120
181
|
mirrorGatewayMethodForMockApi(api, name);
|
|
121
182
|
registryMethods.add(name);
|
|
@@ -4,6 +4,9 @@ export type BridgeOwner = {
|
|
|
4
4
|
apiInstanceId: string;
|
|
5
5
|
registryFingerprint: string;
|
|
6
6
|
registrationMode?: string;
|
|
7
|
+
pluginVersion?: string;
|
|
8
|
+
pluginRoot?: string;
|
|
9
|
+
pluginFile?: string;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
export type BridgeRegisterStateSnapshot = {
|
|
@@ -35,20 +38,17 @@ export type BridgeRegisterStateCarrier = {
|
|
|
35
38
|
gatewayPid?: number;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
|
-
export function getProcessOwnerApiInstanceId(args: {
|
|
39
|
-
serviceOwnerApiInstanceId?: string;
|
|
40
|
-
channelOwnerApiInstanceId?: string;
|
|
41
|
-
}) {
|
|
42
|
-
return args.serviceOwnerApiInstanceId || args.channelOwnerApiInstanceId || undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
export function sameBridgeOwner(left?: BridgeOwner, right?: BridgeOwner) {
|
|
46
42
|
if (!left || !right) return false;
|
|
47
43
|
return (
|
|
48
44
|
left.moduleEpoch === right.moduleEpoch &&
|
|
49
45
|
left.bridgeFactoryId === right.bridgeFactoryId &&
|
|
50
46
|
left.apiInstanceId === right.apiInstanceId &&
|
|
51
|
-
left.registryFingerprint === right.registryFingerprint
|
|
47
|
+
left.registryFingerprint === right.registryFingerprint &&
|
|
48
|
+
left.registrationMode === right.registrationMode &&
|
|
49
|
+
left.pluginVersion === right.pluginVersion &&
|
|
50
|
+
left.pluginRoot === right.pluginRoot &&
|
|
51
|
+
left.pluginFile === right.pluginFile
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -102,39 +102,3 @@ export function hydrateBridgeRegisterState<T extends BridgeRegisterStateCarrier>
|
|
|
102
102
|
bridge.registerTraceRecent = snapshot.registerTraceRecent.map((trace) => ({ ...trace }));
|
|
103
103
|
return bridge;
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
export function shouldAdoptProcessOwner(args: {
|
|
107
|
-
apiInstanceId: string;
|
|
108
|
-
serviceRegistered?: boolean;
|
|
109
|
-
channelRegistered?: boolean;
|
|
110
|
-
serviceOwnerApiInstanceId?: string;
|
|
111
|
-
channelOwnerApiInstanceId?: string;
|
|
112
|
-
}) {
|
|
113
|
-
const existingOwnerApiInstanceId = getProcessOwnerApiInstanceId({
|
|
114
|
-
serviceOwnerApiInstanceId: args.serviceOwnerApiInstanceId,
|
|
115
|
-
channelOwnerApiInstanceId: args.channelOwnerApiInstanceId,
|
|
116
|
-
});
|
|
117
|
-
const hasSingletonOwner = Boolean(args.serviceRegistered) || Boolean(args.channelRegistered);
|
|
118
|
-
|
|
119
|
-
if (!hasSingletonOwner) {
|
|
120
|
-
return {
|
|
121
|
-
adoptOwner: true,
|
|
122
|
-
existingOwnerApiInstanceId,
|
|
123
|
-
reason: 'no-singleton-owner',
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (existingOwnerApiInstanceId && existingOwnerApiInstanceId === args.apiInstanceId) {
|
|
128
|
-
return {
|
|
129
|
-
adoptOwner: true,
|
|
130
|
-
existingOwnerApiInstanceId,
|
|
131
|
-
reason: 'same-owner-api',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
adoptOwner: false,
|
|
137
|
-
existingOwnerApiInstanceId,
|
|
138
|
-
reason: 'singleton-owned-by-other-api',
|
|
139
|
-
};
|
|
140
|
-
}
|