@xmoxmo/bncr 0.6.7 → 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/plugin/bridge-lifecycle.ts +13 -2
|
@@ -1,12 +1,35 @@
|
|
|
1
|
+
import { emitBncrLogLine } from '../core/logging.ts';
|
|
2
|
+
import {
|
|
3
|
+
activatePendingLifecycle,
|
|
4
|
+
beginLifecycleRetirement,
|
|
5
|
+
commitLifecycleAdoption,
|
|
6
|
+
createRegisterLifecycleState,
|
|
7
|
+
failLifecycle,
|
|
8
|
+
getBridgeGenerationKey,
|
|
9
|
+
getPendingLifecycle,
|
|
10
|
+
isLifecycleRegistryActive,
|
|
11
|
+
isLifecycleRegistryStoppable,
|
|
12
|
+
type LifecycleAdoptionDecision,
|
|
13
|
+
type LifecycleOwner,
|
|
14
|
+
type LifecyclePlanInput,
|
|
15
|
+
type LifecycleRetirementOutcome,
|
|
16
|
+
planLifecycleAdoption,
|
|
17
|
+
reactivateStoppedLifecycle,
|
|
18
|
+
settleLifecycleRetirement,
|
|
19
|
+
} from './register-lifecycle.ts';
|
|
1
20
|
import {
|
|
2
21
|
type BncrGatewayMethodName,
|
|
3
22
|
createBncrGatewayMethodRegistry,
|
|
4
23
|
} from './register-runtime-gateway.ts';
|
|
24
|
+
import type { BridgeOwner, BridgeRegisterStateCarrier } from './register-runtime-helpers.ts';
|
|
5
25
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
26
|
+
createRegistryRuntimeObservation,
|
|
27
|
+
evaluateRegistryRuntimeObservation,
|
|
28
|
+
normalizeRegistryRuntimeObservation,
|
|
29
|
+
REGISTRY_RUNTIME_OBSERVATION_POLL_MS,
|
|
30
|
+
type RegistryRuntimeObservation,
|
|
31
|
+
type RegistryRuntimeProbe,
|
|
32
|
+
} from './register-runtime-observation.ts';
|
|
10
33
|
import { createBncrBridgeSingletonManager } from './register-runtime-singleton.ts';
|
|
11
34
|
import {
|
|
12
35
|
type ChannelModule,
|
|
@@ -14,6 +37,7 @@ import {
|
|
|
14
37
|
loadBncrRuntimeSync,
|
|
15
38
|
pluginFile,
|
|
16
39
|
pluginRoot,
|
|
40
|
+
pluginVersion,
|
|
17
41
|
} from './runtime-loader.ts';
|
|
18
42
|
|
|
19
43
|
type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
|
|
@@ -42,9 +66,43 @@ type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
|
|
|
42
66
|
[registerMetaSymbol]?: RegisterMeta;
|
|
43
67
|
};
|
|
44
68
|
|
|
69
|
+
type RegistryDeclarationSlot = 'missing' | 'declared';
|
|
70
|
+
type RegistryDeclarationRegistration = 'pending' | 'complete' | 'failed';
|
|
71
|
+
|
|
72
|
+
type RegistryDeclaration = {
|
|
73
|
+
bridgeGenerationKey: string;
|
|
74
|
+
lifecycleGeneration: number;
|
|
75
|
+
service: RegistryDeclarationSlot;
|
|
76
|
+
channel: RegistryDeclarationSlot;
|
|
77
|
+
registration: RegistryDeclarationRegistration;
|
|
78
|
+
state: 'shadow' | 'active' | 'retired';
|
|
79
|
+
runtimeObservation?: RegistryRuntimeObservation;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type RegistryRuntimeObservationTimer = {
|
|
83
|
+
timer: NodeJS.Timeout;
|
|
84
|
+
bridgeGenerationKey: string;
|
|
85
|
+
lifecycleGeneration: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
45
88
|
type BncrGatewayRuntime = {
|
|
46
89
|
currentBridge?: BridgeSingletonWithState;
|
|
47
90
|
registeredMethodsByRegistry: Map<string, Set<BncrGatewayMethodName>>;
|
|
91
|
+
gatewayMethodDispatchers?: Partial<
|
|
92
|
+
Record<
|
|
93
|
+
BncrGatewayMethodName,
|
|
94
|
+
(
|
|
95
|
+
bridge: BridgeSingletonWithState,
|
|
96
|
+
opts: BridgeGatewayHandlerContext,
|
|
97
|
+
) => BridgeGatewayHandlerResult
|
|
98
|
+
>
|
|
99
|
+
>;
|
|
100
|
+
registryDeclarations: Map<string, RegistryDeclaration>;
|
|
101
|
+
runtimeObservationTimers: Map<string, RegistryRuntimeObservationTimer>;
|
|
102
|
+
lifecycle: ReturnType<typeof createRegisterLifecycleState>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type LegacyBncrGatewayRuntime = Partial<BncrGatewayRuntime> & {
|
|
48
106
|
serviceRegistered?: boolean;
|
|
49
107
|
channelRegistered?: boolean;
|
|
50
108
|
serviceOwnerApiInstanceId?: string;
|
|
@@ -55,6 +113,8 @@ const registerMetaSymbol = Symbol.for('bncr.register.meta');
|
|
|
55
113
|
const globalRegisterTraceSymbol = Symbol.for('bncr.global.register.trace');
|
|
56
114
|
const bridgeOwnerSymbol = Symbol.for('bncr.bridge.owner');
|
|
57
115
|
const gatewayRuntimeSymbol = Symbol.for('bncr.gateway.runtime');
|
|
116
|
+
const maxRegistryDeclarations = 6;
|
|
117
|
+
const maxRegisterTraceEntries = 64;
|
|
58
118
|
const moduleEpoch = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
59
119
|
const identityIds = new WeakMap<object, string>();
|
|
60
120
|
let identitySeq = 0;
|
|
@@ -82,6 +142,90 @@ const getProcessStore = () => {
|
|
|
82
142
|
return p;
|
|
83
143
|
};
|
|
84
144
|
|
|
145
|
+
const rememberRegisterTraceValue = (values: Set<string>, value: string) => {
|
|
146
|
+
// The trace is diagnostic only. Remember recency without retaining every
|
|
147
|
+
// registration fingerprint for the lifetime of the Gateway process.
|
|
148
|
+
values.delete(value);
|
|
149
|
+
values.add(value);
|
|
150
|
+
while (values.size > maxRegisterTraceEntries) {
|
|
151
|
+
const oldest = values.values().next().value;
|
|
152
|
+
if (oldest === undefined) break;
|
|
153
|
+
values.delete(oldest);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const normalizeRegistryDeclaration = (value: unknown): RegistryDeclaration => {
|
|
158
|
+
const legacy = (value || {}) as {
|
|
159
|
+
bridgeGenerationKey?: unknown;
|
|
160
|
+
lifecycleGeneration?: unknown;
|
|
161
|
+
generation?: unknown;
|
|
162
|
+
service?: unknown;
|
|
163
|
+
channel?: unknown;
|
|
164
|
+
registration?: unknown;
|
|
165
|
+
state?: unknown;
|
|
166
|
+
runtimeObservation?: unknown;
|
|
167
|
+
};
|
|
168
|
+
const service = legacy.service === 'declared' || legacy.service === true ? 'declared' : 'missing';
|
|
169
|
+
const channel = legacy.channel === 'declared' || legacy.channel === true ? 'declared' : 'missing';
|
|
170
|
+
const registration =
|
|
171
|
+
legacy.registration === 'pending' ||
|
|
172
|
+
legacy.registration === 'complete' ||
|
|
173
|
+
legacy.registration === 'failed'
|
|
174
|
+
? legacy.registration
|
|
175
|
+
: service === 'declared' && channel === 'declared'
|
|
176
|
+
? 'complete'
|
|
177
|
+
: 'failed';
|
|
178
|
+
const state = legacy.state === 'active' || legacy.state === 'retired' ? legacy.state : 'shadow';
|
|
179
|
+
const lifecycleGeneration =
|
|
180
|
+
typeof legacy.lifecycleGeneration === 'number'
|
|
181
|
+
? legacy.lifecycleGeneration
|
|
182
|
+
: typeof legacy.generation === 'number'
|
|
183
|
+
? legacy.generation
|
|
184
|
+
: 0;
|
|
185
|
+
return {
|
|
186
|
+
bridgeGenerationKey:
|
|
187
|
+
typeof legacy.bridgeGenerationKey === 'string' ? legacy.bridgeGenerationKey : '',
|
|
188
|
+
lifecycleGeneration,
|
|
189
|
+
service,
|
|
190
|
+
channel,
|
|
191
|
+
registration,
|
|
192
|
+
state,
|
|
193
|
+
runtimeObservation: normalizeRegistryRuntimeObservation(legacy.runtimeObservation),
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const normalizeRegistryDeclarationInPlace = (value: unknown): RegistryDeclaration => {
|
|
198
|
+
const normalized = normalizeRegistryDeclaration(value);
|
|
199
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return normalized;
|
|
200
|
+
|
|
201
|
+
const target = value as Record<string, unknown>;
|
|
202
|
+
delete target.generation;
|
|
203
|
+
Object.assign(target, normalized);
|
|
204
|
+
return value as RegistryDeclaration;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const pruneRegistryLedgers = (runtime: BncrGatewayRuntime) => {
|
|
208
|
+
const protectedRegistries = new Set(
|
|
209
|
+
[
|
|
210
|
+
runtime.lifecycle.active?.key.registryFingerprint,
|
|
211
|
+
runtime.lifecycle.pending?.key.registryFingerprint,
|
|
212
|
+
].filter((value): value is string => Boolean(value)),
|
|
213
|
+
);
|
|
214
|
+
for (const ledger of [runtime.registryDeclarations, runtime.registeredMethodsByRegistry]) {
|
|
215
|
+
for (const registryFingerprint of ledger.keys()) {
|
|
216
|
+
if (ledger.size <= maxRegistryDeclarations) break;
|
|
217
|
+
if (!protectedRegistries.has(registryFingerprint)) {
|
|
218
|
+
const timer = runtime.runtimeObservationTimers.get(registryFingerprint);
|
|
219
|
+
if (timer) {
|
|
220
|
+
clearTimeout(timer.timer);
|
|
221
|
+
runtime.runtimeObservationTimers.delete(registryFingerprint);
|
|
222
|
+
}
|
|
223
|
+
ledger.delete(registryFingerprint);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
85
229
|
export function createBncrRegisterRuntime() {
|
|
86
230
|
const gatewayMethodDispatchers: Record<
|
|
87
231
|
BncrGatewayMethodName,
|
|
@@ -138,11 +282,32 @@ export function createBncrRegisterRuntime() {
|
|
|
138
282
|
if (!p[gatewayRuntimeSymbol]) {
|
|
139
283
|
p[gatewayRuntimeSymbol] = {
|
|
140
284
|
registeredMethodsByRegistry: new Map<string, Set<BncrGatewayMethodName>>(),
|
|
141
|
-
|
|
142
|
-
|
|
285
|
+
registryDeclarations: new Map<string, RegistryDeclaration>(),
|
|
286
|
+
runtimeObservationTimers: new Map<string, RegistryRuntimeObservationTimer>(),
|
|
287
|
+
lifecycle: createRegisterLifecycleState(),
|
|
143
288
|
};
|
|
144
289
|
}
|
|
145
|
-
|
|
290
|
+
const runtime = p[gatewayRuntimeSymbol] as LegacyBncrGatewayRuntime;
|
|
291
|
+
if (!(runtime.registeredMethodsByRegistry instanceof Map)) {
|
|
292
|
+
runtime.registeredMethodsByRegistry = new Map<string, Set<BncrGatewayMethodName>>();
|
|
293
|
+
}
|
|
294
|
+
if (!(runtime.registryDeclarations instanceof Map)) {
|
|
295
|
+
runtime.registryDeclarations = new Map<string, RegistryDeclaration>();
|
|
296
|
+
} else {
|
|
297
|
+
for (const [registryFingerprint, declaration] of runtime.registryDeclarations) {
|
|
298
|
+
const normalized = normalizeRegistryDeclarationInPlace(declaration);
|
|
299
|
+
if (normalized !== declaration) {
|
|
300
|
+
runtime.registryDeclarations.set(registryFingerprint, normalized);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!(runtime.runtimeObservationTimers instanceof Map)) {
|
|
305
|
+
runtime.runtimeObservationTimers = new Map<string, RegistryRuntimeObservationTimer>();
|
|
306
|
+
}
|
|
307
|
+
if (!runtime.lifecycle) {
|
|
308
|
+
runtime.lifecycle = createRegisterLifecycleState();
|
|
309
|
+
}
|
|
310
|
+
return runtime as BncrGatewayRuntime;
|
|
146
311
|
};
|
|
147
312
|
|
|
148
313
|
const getBridgeRegisterStateCarrier = (bridge: BridgeSingleton): BridgeRegisterStateCarrier =>
|
|
@@ -154,6 +319,8 @@ export function createBncrRegisterRuntime() {
|
|
|
154
319
|
getGatewayRuntime,
|
|
155
320
|
gatewayMethodDispatchers,
|
|
156
321
|
getBridgeRegisterStateCarrier,
|
|
322
|
+
getRegistryRuntimeObservation: (registryFingerprint) =>
|
|
323
|
+
getRegistryRuntimeObservation(registryFingerprint),
|
|
157
324
|
});
|
|
158
325
|
|
|
159
326
|
const getBridgeOwner = (api: OpenClawPluginApi, loaded: LoadedRuntime): BridgeOwner => {
|
|
@@ -164,6 +331,9 @@ export function createBncrRegisterRuntime() {
|
|
|
164
331
|
apiInstanceId: meta.apiInstanceId || 'unknown',
|
|
165
332
|
registryFingerprint: meta.registryFingerprint || 'unknown',
|
|
166
333
|
registrationMode: meta.registrationMode,
|
|
334
|
+
pluginVersion,
|
|
335
|
+
pluginRoot,
|
|
336
|
+
pluginFile,
|
|
167
337
|
};
|
|
168
338
|
};
|
|
169
339
|
|
|
@@ -175,24 +345,757 @@ export function createBncrRegisterRuntime() {
|
|
|
175
345
|
getBridgeOwner,
|
|
176
346
|
});
|
|
177
347
|
|
|
348
|
+
const toBridgeOwner = (owner: LifecycleOwner): BridgeOwner => ({
|
|
349
|
+
moduleEpoch: owner.bridgeGeneration.moduleEpoch,
|
|
350
|
+
bridgeFactoryId: owner.bridgeGeneration.bridgeFactoryId,
|
|
351
|
+
apiInstanceId: owner.key.apiInstanceId,
|
|
352
|
+
registryFingerprint: owner.key.registryFingerprint,
|
|
353
|
+
registrationMode: owner.bridgeGeneration.registrationMode,
|
|
354
|
+
pluginVersion: owner.bridgeGeneration.pluginVersion,
|
|
355
|
+
pluginRoot: owner.bridgeGeneration.pluginRoot,
|
|
356
|
+
pluginFile: owner.bridgeGeneration.pluginFile,
|
|
357
|
+
});
|
|
358
|
+
|
|
178
359
|
const getCurrentBridge = (): BridgeSingletonWithState => {
|
|
179
360
|
const bridge = getGatewayRuntime().currentBridge;
|
|
180
361
|
if (!bridge) throw new Error('bncr current bridge unavailable');
|
|
181
362
|
return bridge;
|
|
182
363
|
};
|
|
183
364
|
|
|
365
|
+
const clearRegistryRuntimeObservationTimer = (registryFingerprint: string) => {
|
|
366
|
+
const runtime = getGatewayRuntime();
|
|
367
|
+
const entry = runtime.runtimeObservationTimers.get(registryFingerprint);
|
|
368
|
+
if (entry) {
|
|
369
|
+
clearTimeout(entry.timer);
|
|
370
|
+
runtime.runtimeObservationTimers.delete(registryFingerprint);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const readRegistryRuntimeProbe = (): RegistryRuntimeProbe => {
|
|
375
|
+
const bridge = getGatewayRuntime().currentBridge as
|
|
376
|
+
| (BridgeSingletonWithState & {
|
|
377
|
+
getRuntimeObservation?: () => RegistryRuntimeProbe | undefined;
|
|
378
|
+
})
|
|
379
|
+
| undefined;
|
|
380
|
+
const probe = bridge?.getRuntimeObservation?.();
|
|
381
|
+
return probe && typeof probe === 'object' ? probe : {};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const isRegistryRuntimeLifecycleActive = (
|
|
385
|
+
registryFingerprint: string,
|
|
386
|
+
lifecycleGeneration: number,
|
|
387
|
+
) => {
|
|
388
|
+
const active = getGatewayRuntime().lifecycle.active;
|
|
389
|
+
return Boolean(
|
|
390
|
+
active &&
|
|
391
|
+
active.key.registryFingerprint === registryFingerprint &&
|
|
392
|
+
active.key.generation === lifecycleGeneration &&
|
|
393
|
+
(active.phase === 'active' || active.phase === 'registering'),
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const probeRegistryRuntimeObservation = (
|
|
398
|
+
registryFingerprint: string,
|
|
399
|
+
bridgeGenerationKey: string,
|
|
400
|
+
lifecycleGeneration: number,
|
|
401
|
+
now = Date.now(),
|
|
402
|
+
): RegistryRuntimeObservation | null => {
|
|
403
|
+
try {
|
|
404
|
+
const runtime = getGatewayRuntime();
|
|
405
|
+
const declaration = runtime.registryDeclarations.get(registryFingerprint);
|
|
406
|
+
if (
|
|
407
|
+
!declaration ||
|
|
408
|
+
declaration.bridgeGenerationKey !== bridgeGenerationKey ||
|
|
409
|
+
declaration.lifecycleGeneration !== lifecycleGeneration ||
|
|
410
|
+
!declaration.runtimeObservation
|
|
411
|
+
) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const decision = evaluateRegistryRuntimeObservation({
|
|
416
|
+
observation: declaration.runtimeObservation,
|
|
417
|
+
now,
|
|
418
|
+
lifecycleActive:
|
|
419
|
+
declaration.registration === 'complete' &&
|
|
420
|
+
declaration.state === 'active' &&
|
|
421
|
+
isRegistryRuntimeLifecycleActive(registryFingerprint, lifecycleGeneration),
|
|
422
|
+
probe: readRegistryRuntimeProbe(),
|
|
423
|
+
});
|
|
424
|
+
declaration.runtimeObservation = decision.observation;
|
|
425
|
+
|
|
426
|
+
if (decision.transitionedToPolling) {
|
|
427
|
+
emitBncrLogLine(
|
|
428
|
+
'warn',
|
|
429
|
+
`[bncr] runtime observation timeout registry=${registryFingerprint} generation=${lifecycleGeneration} service=${decision.observation.serviceObservedAt !== null} channel=${decision.observation.channelObservedAt !== null} action=poll intervalMs=${REGISTRY_RUNTIME_OBSERVATION_POLL_MS}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (decision.transitionedToReady) {
|
|
433
|
+
emitBncrLogLine(
|
|
434
|
+
'info',
|
|
435
|
+
`[bncr] runtime observation ready registry=${registryFingerprint} generation=${lifecycleGeneration} probes=${decision.observation.probeCount}`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (decision.action === 'wait' || decision.action === 'poll') {
|
|
440
|
+
clearRegistryRuntimeObservationTimer(registryFingerprint);
|
|
441
|
+
const delay =
|
|
442
|
+
decision.action === 'poll'
|
|
443
|
+
? REGISTRY_RUNTIME_OBSERVATION_POLL_MS
|
|
444
|
+
: Math.max(0, decision.observation.deadlineAt - now);
|
|
445
|
+
const timer = setTimeout(() => {
|
|
446
|
+
const current = runtime.runtimeObservationTimers.get(registryFingerprint);
|
|
447
|
+
if (current?.timer !== timer) return;
|
|
448
|
+
runtime.runtimeObservationTimers.delete(registryFingerprint);
|
|
449
|
+
probeRegistryRuntimeObservation(
|
|
450
|
+
registryFingerprint,
|
|
451
|
+
bridgeGenerationKey,
|
|
452
|
+
lifecycleGeneration,
|
|
453
|
+
Date.now(),
|
|
454
|
+
);
|
|
455
|
+
}, delay);
|
|
456
|
+
timer.unref?.();
|
|
457
|
+
runtime.runtimeObservationTimers.set(registryFingerprint, {
|
|
458
|
+
timer,
|
|
459
|
+
bridgeGenerationKey,
|
|
460
|
+
lifecycleGeneration,
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
clearRegistryRuntimeObservationTimer(registryFingerprint);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { ...decision.observation };
|
|
467
|
+
} catch (error) {
|
|
468
|
+
try {
|
|
469
|
+
emitBncrLogLine(
|
|
470
|
+
'warn',
|
|
471
|
+
`[bncr] runtime observation probe failed registry=${registryFingerprint} generation=${lifecycleGeneration} error=${error instanceof Error ? error.message : String(error)}`,
|
|
472
|
+
);
|
|
473
|
+
} catch {
|
|
474
|
+
// Runtime observation is diagnostic-only and must never affect startup.
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const runtime = getGatewayRuntime();
|
|
478
|
+
const declaration = runtime.registryDeclarations.get(registryFingerprint);
|
|
479
|
+
if (
|
|
480
|
+
declaration?.bridgeGenerationKey === bridgeGenerationKey &&
|
|
481
|
+
declaration.lifecycleGeneration === lifecycleGeneration &&
|
|
482
|
+
declaration.registration === 'complete' &&
|
|
483
|
+
declaration.state === 'active'
|
|
484
|
+
) {
|
|
485
|
+
clearRegistryRuntimeObservationTimer(registryFingerprint);
|
|
486
|
+
const timer = setTimeout(() => {
|
|
487
|
+
const current = runtime.runtimeObservationTimers.get(registryFingerprint);
|
|
488
|
+
if (current?.timer !== timer) return;
|
|
489
|
+
runtime.runtimeObservationTimers.delete(registryFingerprint);
|
|
490
|
+
probeRegistryRuntimeObservation(
|
|
491
|
+
registryFingerprint,
|
|
492
|
+
bridgeGenerationKey,
|
|
493
|
+
lifecycleGeneration,
|
|
494
|
+
Date.now(),
|
|
495
|
+
);
|
|
496
|
+
}, REGISTRY_RUNTIME_OBSERVATION_POLL_MS);
|
|
497
|
+
timer.unref?.();
|
|
498
|
+
runtime.runtimeObservationTimers.set(registryFingerprint, {
|
|
499
|
+
timer,
|
|
500
|
+
bridgeGenerationKey,
|
|
501
|
+
lifecycleGeneration,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// A failed diagnostic probe must not affect service or channel startup.
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const startRegistryRuntimeObservation = (
|
|
512
|
+
registryFingerprint: string,
|
|
513
|
+
bridgeGenerationKey: string,
|
|
514
|
+
lifecycleGeneration: number,
|
|
515
|
+
now = Date.now(),
|
|
516
|
+
reset = false,
|
|
517
|
+
) => {
|
|
518
|
+
const runtime = getGatewayRuntime();
|
|
519
|
+
const declaration = runtime.registryDeclarations.get(registryFingerprint);
|
|
520
|
+
if (
|
|
521
|
+
!declaration ||
|
|
522
|
+
declaration.bridgeGenerationKey !== bridgeGenerationKey ||
|
|
523
|
+
declaration.lifecycleGeneration !== lifecycleGeneration ||
|
|
524
|
+
declaration.registration !== 'complete' ||
|
|
525
|
+
declaration.state !== 'active'
|
|
526
|
+
) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
if (!reset && declaration.runtimeObservation) {
|
|
530
|
+
return { ...declaration.runtimeObservation };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
clearRegistryRuntimeObservationTimer(registryFingerprint);
|
|
534
|
+
declaration.runtimeObservation = createRegistryRuntimeObservation(now);
|
|
535
|
+
return probeRegistryRuntimeObservation(
|
|
536
|
+
registryFingerprint,
|
|
537
|
+
bridgeGenerationKey,
|
|
538
|
+
lifecycleGeneration,
|
|
539
|
+
now,
|
|
540
|
+
);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const retireRegistryRuntimeObservation = (registryFingerprint: string, now = Date.now()) => {
|
|
544
|
+
clearRegistryRuntimeObservationTimer(registryFingerprint);
|
|
545
|
+
const declaration = getGatewayRuntime().registryDeclarations.get(registryFingerprint);
|
|
546
|
+
if (!declaration?.runtimeObservation) return;
|
|
547
|
+
declaration.runtimeObservation = {
|
|
548
|
+
...declaration.runtimeObservation,
|
|
549
|
+
phase: 'retired',
|
|
550
|
+
retiredAt: declaration.runtimeObservation.retiredAt ?? now,
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const getRegistryRuntimeObservation = (registryFingerprint: string) => {
|
|
555
|
+
const runtime = getGatewayRuntime();
|
|
556
|
+
const declaration = runtime.registryDeclarations.get(registryFingerprint);
|
|
557
|
+
if (declaration?.state === 'active' && declaration.runtimeObservation) {
|
|
558
|
+
return { ...declaration.runtimeObservation };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const activeRegistry = runtime.lifecycle.active?.key.registryFingerprint;
|
|
562
|
+
if (activeRegistry && activeRegistry !== registryFingerprint) {
|
|
563
|
+
const activeObservation =
|
|
564
|
+
runtime.registryDeclarations.get(activeRegistry)?.runtimeObservation;
|
|
565
|
+
if (activeObservation) return { ...activeObservation };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return declaration?.runtimeObservation ? { ...declaration.runtimeObservation } : null;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const planLifecycle = (api: OpenClawPluginApi, now = Date.now()): LifecycleAdoptionDecision => {
|
|
572
|
+
const loaded = loadBncrRuntimeSync();
|
|
573
|
+
const owner = getBridgeOwner(api, loaded);
|
|
574
|
+
const runtime = getGatewayRuntime();
|
|
575
|
+
const input: LifecyclePlanInput = {
|
|
576
|
+
apiInstanceId: owner.apiInstanceId,
|
|
577
|
+
registryFingerprint: owner.registryFingerprint,
|
|
578
|
+
bridgeGeneration: {
|
|
579
|
+
moduleEpoch: owner.moduleEpoch,
|
|
580
|
+
bridgeFactoryId: owner.bridgeFactoryId,
|
|
581
|
+
pluginVersion: owner.pluginVersion || 'unknown',
|
|
582
|
+
registrationMode: owner.registrationMode || 'unknown',
|
|
583
|
+
pluginRoot: owner.pluginRoot || pluginRoot,
|
|
584
|
+
pluginFile: owner.pluginFile || pluginFile,
|
|
585
|
+
},
|
|
586
|
+
now,
|
|
587
|
+
};
|
|
588
|
+
const decision = planLifecycleAdoption(runtime.lifecycle, input);
|
|
589
|
+
const declaration = runtime.registryDeclarations.get(owner.registryFingerprint);
|
|
590
|
+
if (!declaration) return decision;
|
|
591
|
+
|
|
592
|
+
const bridgeGenerationKey = getBridgeGenerationKey(input.bridgeGeneration);
|
|
593
|
+
if (!declaration.bridgeGenerationKey) {
|
|
594
|
+
// Legacy runtimes did not persist the bridge identity in the ledger.
|
|
595
|
+
declaration.bridgeGenerationKey = bridgeGenerationKey;
|
|
596
|
+
} else if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
|
|
597
|
+
return {
|
|
598
|
+
kind: 'reject',
|
|
599
|
+
owner: decision.owner,
|
|
600
|
+
reason: 'registration-generation-conflict',
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (declaration.registration === 'failed') {
|
|
604
|
+
return {
|
|
605
|
+
kind: 'reject',
|
|
606
|
+
owner: decision.owner,
|
|
607
|
+
reason: 'registration-failed',
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (declaration.registration === 'pending') {
|
|
611
|
+
return {
|
|
612
|
+
kind: 'reject',
|
|
613
|
+
owner: decision.owner,
|
|
614
|
+
reason: 'registration-in-progress',
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
return decision;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const commitLifecycle = (decision: LifecycleAdoptionDecision) => {
|
|
621
|
+
const runtime = getGatewayRuntime();
|
|
622
|
+
if (decision.kind === 'initialize' || decision.kind === 'takeover') {
|
|
623
|
+
runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
|
|
624
|
+
}
|
|
625
|
+
const owner = commitLifecycleAdoption(runtime.lifecycle, decision);
|
|
626
|
+
const requestedRegistry = decision.owner.key.registryFingerprint;
|
|
627
|
+
const requestedGeneration = decision.owner.key.generation;
|
|
628
|
+
const requestedBridgeGenerationKey = getBridgeGenerationKey(decision.owner.bridgeGeneration);
|
|
629
|
+
|
|
630
|
+
if (decision.kind === 'initialize' || decision.kind === 'takeover') {
|
|
631
|
+
for (const [registryFingerprint, declaration] of runtime.registryDeclarations) {
|
|
632
|
+
if (registryFingerprint !== owner?.key.registryFingerprint) {
|
|
633
|
+
declaration.state = 'retired';
|
|
634
|
+
retireRegistryRuntimeObservation(registryFingerprint);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const declaration = getRegistryDeclaration(
|
|
638
|
+
requestedRegistry,
|
|
639
|
+
requestedBridgeGenerationKey,
|
|
640
|
+
requestedGeneration,
|
|
641
|
+
);
|
|
642
|
+
if (
|
|
643
|
+
declaration.bridgeGenerationKey &&
|
|
644
|
+
declaration.bridgeGenerationKey !== requestedBridgeGenerationKey
|
|
645
|
+
) {
|
|
646
|
+
declaration.service = 'missing';
|
|
647
|
+
declaration.channel = 'missing';
|
|
648
|
+
}
|
|
649
|
+
declaration.bridgeGenerationKey = requestedBridgeGenerationKey;
|
|
650
|
+
declaration.lifecycleGeneration = requestedGeneration;
|
|
651
|
+
declaration.registration = 'pending';
|
|
652
|
+
declaration.state = 'active';
|
|
653
|
+
declaration.runtimeObservation = undefined;
|
|
654
|
+
} else if (decision.kind === 'defer') {
|
|
655
|
+
const declaration = getRegistryDeclaration(
|
|
656
|
+
requestedRegistry,
|
|
657
|
+
requestedBridgeGenerationKey,
|
|
658
|
+
requestedGeneration,
|
|
659
|
+
);
|
|
660
|
+
if (
|
|
661
|
+
declaration.bridgeGenerationKey &&
|
|
662
|
+
declaration.bridgeGenerationKey !== requestedBridgeGenerationKey
|
|
663
|
+
) {
|
|
664
|
+
declaration.service = 'missing';
|
|
665
|
+
declaration.channel = 'missing';
|
|
666
|
+
}
|
|
667
|
+
declaration.bridgeGenerationKey = requestedBridgeGenerationKey;
|
|
668
|
+
declaration.lifecycleGeneration = requestedGeneration;
|
|
669
|
+
declaration.registration = 'pending';
|
|
670
|
+
declaration.state = 'shadow';
|
|
671
|
+
declaration.runtimeObservation = undefined;
|
|
672
|
+
} else if (decision.kind === 'duplicate') {
|
|
673
|
+
const existing = runtime.registryDeclarations.get(requestedRegistry);
|
|
674
|
+
if (!existing) {
|
|
675
|
+
const declaration = getRegistryDeclaration(
|
|
676
|
+
requestedRegistry,
|
|
677
|
+
requestedBridgeGenerationKey,
|
|
678
|
+
requestedGeneration,
|
|
679
|
+
);
|
|
680
|
+
declaration.registration = 'pending';
|
|
681
|
+
declaration.state = 'shadow';
|
|
682
|
+
} else if (
|
|
683
|
+
!existing.bridgeGenerationKey ||
|
|
684
|
+
existing.bridgeGenerationKey === requestedBridgeGenerationKey
|
|
685
|
+
) {
|
|
686
|
+
existing.bridgeGenerationKey ||= requestedBridgeGenerationKey;
|
|
687
|
+
const activeRegistry = runtime.lifecycle.active?.key.registryFingerprint;
|
|
688
|
+
const pendingRegistry = runtime.lifecycle.pending?.key.registryFingerprint;
|
|
689
|
+
if (requestedRegistry === activeRegistry && runtime.lifecycle.active?.phase !== 'stopped') {
|
|
690
|
+
existing.state = 'active';
|
|
691
|
+
} else if (requestedRegistry === pendingRegistry) {
|
|
692
|
+
existing.state = 'shadow';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
pruneRegistryLedgers(runtime);
|
|
697
|
+
return owner;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const adoptLifecycleBridge = (
|
|
701
|
+
api: OpenClawPluginApi,
|
|
702
|
+
owner: LifecycleOwner,
|
|
703
|
+
mode: 'reuse' | 'replace' | 'create',
|
|
704
|
+
) => {
|
|
705
|
+
try {
|
|
706
|
+
const adopted = bridgeSingletonManager.adoptBridgeSingleton(api, toBridgeOwner(owner), mode);
|
|
707
|
+
getGatewayRuntime().currentBridge = adopted.bridge;
|
|
708
|
+
return adopted;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const lifecycle = getGatewayRuntime().lifecycle;
|
|
711
|
+
if (
|
|
712
|
+
lifecycle.active?.key.generation === owner.key.generation &&
|
|
713
|
+
lifecycle.active.key.registryFingerprint === owner.key.registryFingerprint
|
|
714
|
+
) {
|
|
715
|
+
failLifecycle(lifecycle, owner.key.registryFingerprint, error, owner.key.generation);
|
|
716
|
+
const declaration = getRegistryDeclaration(
|
|
717
|
+
owner.key.registryFingerprint,
|
|
718
|
+
getBridgeGenerationKey(owner.bridgeGeneration),
|
|
719
|
+
owner.key.generation,
|
|
720
|
+
);
|
|
721
|
+
declaration.registration = 'failed';
|
|
722
|
+
declaration.state = 'retired';
|
|
723
|
+
retireRegistryRuntimeObservation(owner.key.registryFingerprint);
|
|
724
|
+
}
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const beginRetirement = (
|
|
730
|
+
registryFingerprint: string,
|
|
731
|
+
expectedGeneration: number,
|
|
732
|
+
now = Date.now(),
|
|
733
|
+
) => {
|
|
734
|
+
const retirement = beginLifecycleRetirement(
|
|
735
|
+
getGatewayRuntime().lifecycle,
|
|
736
|
+
registryFingerprint,
|
|
737
|
+
now,
|
|
738
|
+
expectedGeneration,
|
|
739
|
+
);
|
|
740
|
+
if (retirement) {
|
|
741
|
+
retireRegistryRuntimeObservation(registryFingerprint, now);
|
|
742
|
+
}
|
|
743
|
+
return retirement;
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const settleRetirement = (
|
|
747
|
+
registryFingerprint: string,
|
|
748
|
+
outcome: LifecycleRetirementOutcome,
|
|
749
|
+
expectedGeneration: number,
|
|
750
|
+
now = Date.now(),
|
|
751
|
+
) => {
|
|
752
|
+
const settled = settleLifecycleRetirement(
|
|
753
|
+
getGatewayRuntime().lifecycle,
|
|
754
|
+
registryFingerprint,
|
|
755
|
+
outcome,
|
|
756
|
+
now,
|
|
757
|
+
expectedGeneration,
|
|
758
|
+
);
|
|
759
|
+
if (settled) {
|
|
760
|
+
const lifecycle = getGatewayRuntime().lifecycle;
|
|
761
|
+
const declaration = getRegistryDeclaration(registryFingerprint);
|
|
762
|
+
if (!outcome.ok) {
|
|
763
|
+
declaration.state = 'retired';
|
|
764
|
+
retireRegistryRuntimeObservation(registryFingerprint, now);
|
|
765
|
+
const pendingRegistry = lifecycle.pending?.key.registryFingerprint;
|
|
766
|
+
if (pendingRegistry && pendingRegistry !== registryFingerprint) {
|
|
767
|
+
getRegistryDeclaration(pendingRegistry).state = 'retired';
|
|
768
|
+
retireRegistryRuntimeObservation(pendingRegistry, now);
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
declaration.state =
|
|
772
|
+
lifecycle.pending?.key.registryFingerprint === registryFingerprint ? 'shadow' : 'retired';
|
|
773
|
+
retireRegistryRuntimeObservation(registryFingerprint, now);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return settled;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const getLifecycleBridgeForStart = async (
|
|
780
|
+
api: OpenClawPluginApi,
|
|
781
|
+
registryFingerprint: string,
|
|
782
|
+
expectedGeneration: number,
|
|
783
|
+
) => {
|
|
784
|
+
const runtime = getGatewayRuntime();
|
|
785
|
+
const state = runtime.lifecycle;
|
|
786
|
+
if (runtime.registryDeclarations.get(registryFingerprint)?.registration === 'failed') {
|
|
787
|
+
state.staleCallbackSuppressions += 1;
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration)) {
|
|
792
|
+
getRegistryDeclaration(registryFingerprint).state = 'active';
|
|
793
|
+
startRegistryRuntimeObservation(
|
|
794
|
+
registryFingerprint,
|
|
795
|
+
getBridgeGenerationKey(state.active!.bridgeGeneration),
|
|
796
|
+
expectedGeneration,
|
|
797
|
+
);
|
|
798
|
+
return runtime.currentBridge || null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const pending = getPendingLifecycle(state);
|
|
802
|
+
if (
|
|
803
|
+
pending?.key.registryFingerprint === registryFingerprint &&
|
|
804
|
+
pending.key.generation === expectedGeneration
|
|
805
|
+
) {
|
|
806
|
+
const pendingMode = state.pendingMode || 'reuse';
|
|
807
|
+
const retirement = state.retirement;
|
|
808
|
+
const outcome = retirement ? await retirement : { ok: true as const };
|
|
809
|
+
if (!outcome.ok) {
|
|
810
|
+
throw new Error(`bncr lifecycle predecessor stop failed: ${outcome.error}`);
|
|
811
|
+
}
|
|
812
|
+
const activated = activatePendingLifecycle(
|
|
813
|
+
state,
|
|
814
|
+
registryFingerprint,
|
|
815
|
+
Date.now(),
|
|
816
|
+
expectedGeneration,
|
|
817
|
+
);
|
|
818
|
+
if (activated) {
|
|
819
|
+
getRegistryDeclaration(registryFingerprint).state = 'active';
|
|
820
|
+
const bridge = adoptLifecycleBridge(api, activated, pendingMode).bridge;
|
|
821
|
+
runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
|
|
822
|
+
startRegistryRuntimeObservation(
|
|
823
|
+
registryFingerprint,
|
|
824
|
+
getBridgeGenerationKey(activated.bridgeGeneration),
|
|
825
|
+
expectedGeneration,
|
|
826
|
+
Date.now(),
|
|
827
|
+
true,
|
|
828
|
+
);
|
|
829
|
+
return bridge;
|
|
830
|
+
}
|
|
831
|
+
// Another concurrent start for this same pending generation may already
|
|
832
|
+
// have won the activation race while this call awaited the barrier.
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const reactivated = reactivateStoppedLifecycle(
|
|
836
|
+
state,
|
|
837
|
+
registryFingerprint,
|
|
838
|
+
Date.now(),
|
|
839
|
+
expectedGeneration,
|
|
840
|
+
);
|
|
841
|
+
if (reactivated) {
|
|
842
|
+
getRegistryDeclaration(registryFingerprint).state = 'active';
|
|
843
|
+
const bridge = adoptLifecycleBridge(api, reactivated, 'reuse').bridge;
|
|
844
|
+
runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
|
|
845
|
+
startRegistryRuntimeObservation(
|
|
846
|
+
registryFingerprint,
|
|
847
|
+
getBridgeGenerationKey(reactivated.bridgeGeneration),
|
|
848
|
+
expectedGeneration,
|
|
849
|
+
Date.now(),
|
|
850
|
+
true,
|
|
851
|
+
);
|
|
852
|
+
return bridge;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Re-read ownership after every await/activation attempt: a concurrent
|
|
856
|
+
// start for the same generation can legitimately have activated it first.
|
|
857
|
+
if (isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration)) {
|
|
858
|
+
getRegistryDeclaration(registryFingerprint).state = 'active';
|
|
859
|
+
startRegistryRuntimeObservation(
|
|
860
|
+
registryFingerprint,
|
|
861
|
+
getBridgeGenerationKey(state.active!.bridgeGeneration),
|
|
862
|
+
expectedGeneration,
|
|
863
|
+
);
|
|
864
|
+
return runtime.currentBridge || null;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
state.staleCallbackSuppressions += 1;
|
|
868
|
+
return null;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const failLifecycleStart = (
|
|
872
|
+
registryFingerprint: string,
|
|
873
|
+
error: unknown,
|
|
874
|
+
expectedGeneration: number,
|
|
875
|
+
) => {
|
|
876
|
+
const failed = failLifecycle(
|
|
877
|
+
getGatewayRuntime().lifecycle,
|
|
878
|
+
registryFingerprint,
|
|
879
|
+
error,
|
|
880
|
+
expectedGeneration,
|
|
881
|
+
);
|
|
882
|
+
if (failed) {
|
|
883
|
+
getRegistryDeclaration(registryFingerprint).state = 'retired';
|
|
884
|
+
retireRegistryRuntimeObservation(registryFingerprint);
|
|
885
|
+
}
|
|
886
|
+
return failed;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
/*
|
|
890
|
+
* Best-effort repair for a registration that committed its lifecycle but
|
|
891
|
+
* failed before finishing its service/channel declarations. Nothing of this
|
|
892
|
+
* generation has started yet, so returning to a normal state is safe and
|
|
893
|
+
* lets the next registration recover instead of being suppressed forever.
|
|
894
|
+
*/
|
|
895
|
+
const recoverLifecycleRegistration = (decision: LifecycleAdoptionDecision, error: unknown) => {
|
|
896
|
+
const runtime = getGatewayRuntime();
|
|
897
|
+
const state = runtime.lifecycle;
|
|
898
|
+
const owner = decision.owner;
|
|
899
|
+
const ownsActive = Boolean(
|
|
900
|
+
state.active &&
|
|
901
|
+
state.active.key.registryFingerprint === owner.key.registryFingerprint &&
|
|
902
|
+
state.active.key.generation === owner.key.generation,
|
|
903
|
+
);
|
|
904
|
+
const ownsPending = Boolean(
|
|
905
|
+
state.pending &&
|
|
906
|
+
state.pending.key.registryFingerprint === owner.key.registryFingerprint &&
|
|
907
|
+
state.pending.key.generation === owner.key.generation,
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
let outcome: 'stopped' | 'pending-dropped' | 'failed' | 'none' = 'none';
|
|
911
|
+
if (ownsActive && (decision.kind === 'initialize' || decision.kind === 'takeover')) {
|
|
912
|
+
// Keep the generation bound but inert. Clearing `active` outright would
|
|
913
|
+
// re-open the "no declaration + no active lifecycle" dispatch fallback
|
|
914
|
+
// for stale gateway method handlers; `stopped` is recoverable by the
|
|
915
|
+
// next takeover and keeps dispatch gated.
|
|
916
|
+
if (state.active) {
|
|
917
|
+
state.active.phase = 'stopped';
|
|
918
|
+
state.active.stopRequestedAt = null;
|
|
919
|
+
state.active.stoppedAt = Date.now();
|
|
920
|
+
state.active.failure = null;
|
|
921
|
+
}
|
|
922
|
+
state.pending = undefined;
|
|
923
|
+
state.pendingMode = undefined;
|
|
924
|
+
state.retirement = undefined;
|
|
925
|
+
state.resolveRetirement = undefined;
|
|
926
|
+
outcome = 'stopped';
|
|
927
|
+
} else if (ownsPending && decision.kind === 'defer') {
|
|
928
|
+
state.pending = undefined;
|
|
929
|
+
state.pendingMode = undefined;
|
|
930
|
+
outcome = 'pending-dropped';
|
|
931
|
+
} else if (ownsActive) {
|
|
932
|
+
failLifecycle(state, owner.key.registryFingerprint, error, owner.key.generation);
|
|
933
|
+
outcome = 'failed';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const declaration = runtime.registryDeclarations.get(owner.key.registryFingerprint);
|
|
937
|
+
const ownsDeclaration = Boolean(
|
|
938
|
+
declaration &&
|
|
939
|
+
declaration.bridgeGenerationKey === getBridgeGenerationKey(owner.bridgeGeneration) &&
|
|
940
|
+
declaration.lifecycleGeneration === owner.key.generation,
|
|
941
|
+
);
|
|
942
|
+
if (ownsDeclaration && declaration) {
|
|
943
|
+
// The registration call did not complete successfully, even if it had
|
|
944
|
+
// already published all declarations. Keep the whole scope fail-closed;
|
|
945
|
+
// otherwise a stopped lifecycle can leave an active gateway declaration.
|
|
946
|
+
declaration.registration = 'failed';
|
|
947
|
+
declaration.state = 'retired';
|
|
948
|
+
retireRegistryRuntimeObservation(owner.key.registryFingerprint);
|
|
949
|
+
}
|
|
950
|
+
return outcome;
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const canStopLifecycleRegistry = (registryFingerprint: string, expectedGeneration: number) => {
|
|
954
|
+
const state = getGatewayRuntime().lifecycle;
|
|
955
|
+
const allowed = isLifecycleRegistryStoppable(state, registryFingerprint, expectedGeneration);
|
|
956
|
+
if (!allowed) {
|
|
957
|
+
state.staleCallbackSuppressions += 1;
|
|
958
|
+
}
|
|
959
|
+
return allowed;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const canStartLifecycleRegistry = (registryFingerprint: string, expectedGeneration: number) => {
|
|
963
|
+
const state = getGatewayRuntime().lifecycle;
|
|
964
|
+
const allowed =
|
|
965
|
+
getGatewayRuntime().registryDeclarations.get(registryFingerprint)?.registration !==
|
|
966
|
+
'failed' && isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration);
|
|
967
|
+
if (!allowed) {
|
|
968
|
+
state.staleCallbackSuppressions += 1;
|
|
969
|
+
}
|
|
970
|
+
return allowed;
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const isLifecycleRegistryCurrent = (registryFingerprint: string, expectedGeneration: number) =>
|
|
974
|
+
isLifecycleRegistryActive(
|
|
975
|
+
getGatewayRuntime().lifecycle,
|
|
976
|
+
registryFingerprint,
|
|
977
|
+
expectedGeneration,
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const getRegistryDeclaration = (
|
|
981
|
+
registryFingerprint: string,
|
|
982
|
+
bridgeGenerationKey = '',
|
|
983
|
+
lifecycleGeneration = 0,
|
|
984
|
+
) => {
|
|
985
|
+
const runtime = getGatewayRuntime();
|
|
986
|
+
let declaration = runtime.registryDeclarations.get(registryFingerprint);
|
|
987
|
+
if (!declaration) {
|
|
988
|
+
declaration = {
|
|
989
|
+
bridgeGenerationKey,
|
|
990
|
+
lifecycleGeneration,
|
|
991
|
+
service: 'missing',
|
|
992
|
+
channel: 'missing',
|
|
993
|
+
registration: 'pending',
|
|
994
|
+
state: 'shadow',
|
|
995
|
+
};
|
|
996
|
+
runtime.registryDeclarations.set(registryFingerprint, declaration);
|
|
997
|
+
} else {
|
|
998
|
+
declaration = normalizeRegistryDeclarationInPlace(declaration);
|
|
999
|
+
if (!declaration.bridgeGenerationKey) {
|
|
1000
|
+
declaration.bridgeGenerationKey = bridgeGenerationKey;
|
|
1001
|
+
}
|
|
1002
|
+
if (!declaration.lifecycleGeneration) {
|
|
1003
|
+
declaration.lifecycleGeneration = lifecycleGeneration;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return declaration;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const markRegistryServiceDeclared = (
|
|
1010
|
+
registryFingerprint: string,
|
|
1011
|
+
bridgeGenerationKey: string,
|
|
1012
|
+
lifecycleGeneration: number,
|
|
1013
|
+
) => {
|
|
1014
|
+
const declaration = getRegistryDeclaration(
|
|
1015
|
+
registryFingerprint,
|
|
1016
|
+
bridgeGenerationKey,
|
|
1017
|
+
lifecycleGeneration,
|
|
1018
|
+
);
|
|
1019
|
+
if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
|
|
1020
|
+
throw new Error(`bncr service declaration scope mismatch: ${registryFingerprint}`);
|
|
1021
|
+
}
|
|
1022
|
+
declaration.lifecycleGeneration = lifecycleGeneration;
|
|
1023
|
+
declaration.service = 'declared';
|
|
1024
|
+
return declaration;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const markRegistryChannelDeclared = (
|
|
1028
|
+
registryFingerprint: string,
|
|
1029
|
+
bridgeGenerationKey: string,
|
|
1030
|
+
lifecycleGeneration: number,
|
|
1031
|
+
) => {
|
|
1032
|
+
const declaration = getRegistryDeclaration(
|
|
1033
|
+
registryFingerprint,
|
|
1034
|
+
bridgeGenerationKey,
|
|
1035
|
+
lifecycleGeneration,
|
|
1036
|
+
);
|
|
1037
|
+
if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
|
|
1038
|
+
throw new Error(`bncr channel declaration scope mismatch: ${registryFingerprint}`);
|
|
1039
|
+
}
|
|
1040
|
+
declaration.lifecycleGeneration = lifecycleGeneration;
|
|
1041
|
+
declaration.channel = 'declared';
|
|
1042
|
+
return declaration;
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const markRegistryRegistrationComplete = (
|
|
1046
|
+
registryFingerprint: string,
|
|
1047
|
+
bridgeGenerationKey: string,
|
|
1048
|
+
lifecycleGeneration: number,
|
|
1049
|
+
) => {
|
|
1050
|
+
const declaration = getRegistryDeclaration(
|
|
1051
|
+
registryFingerprint,
|
|
1052
|
+
bridgeGenerationKey,
|
|
1053
|
+
lifecycleGeneration,
|
|
1054
|
+
);
|
|
1055
|
+
if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
|
|
1056
|
+
throw new Error(`bncr register scope mismatch: ${registryFingerprint}`);
|
|
1057
|
+
}
|
|
1058
|
+
// Duplicate registrations do not redeclare callbacks. Keep the generation
|
|
1059
|
+
// that owns the existing service/channel callbacks instead of advancing the
|
|
1060
|
+
// declaration to a new planning generation.
|
|
1061
|
+
declaration.lifecycleGeneration ||= lifecycleGeneration;
|
|
1062
|
+
declaration.registration = 'complete';
|
|
1063
|
+
if (declaration.state === 'active') {
|
|
1064
|
+
startRegistryRuntimeObservation(
|
|
1065
|
+
registryFingerprint,
|
|
1066
|
+
bridgeGenerationKey,
|
|
1067
|
+
declaration.lifecycleGeneration,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return declaration;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
184
1073
|
return {
|
|
185
1074
|
getRegisterMeta,
|
|
186
1075
|
getGlobalRegisterTrace,
|
|
1076
|
+
noteRegisterTraceValue: rememberRegisterTraceValue,
|
|
1077
|
+
getBridgeGenerationKey,
|
|
187
1078
|
getGatewayRuntime,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1079
|
+
planLifecycle,
|
|
1080
|
+
commitLifecycle,
|
|
1081
|
+
adoptLifecycleBridge,
|
|
1082
|
+
beginRetirement,
|
|
1083
|
+
settleRetirement,
|
|
1084
|
+
getLifecycleBridgeForStart,
|
|
1085
|
+
failLifecycleStart,
|
|
1086
|
+
recoverLifecycleRegistration,
|
|
1087
|
+
canStopLifecycleRegistry,
|
|
1088
|
+
canStartLifecycleRegistry,
|
|
1089
|
+
isLifecycleRegistryCurrent,
|
|
1090
|
+
getRegistryDeclaration,
|
|
1091
|
+
getRegistryRuntimeObservation,
|
|
1092
|
+
startRegistryRuntimeObservation,
|
|
1093
|
+
probeRegistryRuntimeObservation,
|
|
1094
|
+
retireRegistryRuntimeObservation,
|
|
1095
|
+
markRegistryServiceDeclared,
|
|
1096
|
+
markRegistryChannelDeclared,
|
|
1097
|
+
markRegistryRegistrationComplete,
|
|
1098
|
+
pruneRegistryLedgers: () => pruneRegistryLedgers(getGatewayRuntime()),
|
|
196
1099
|
ensureGatewayMethodRegistered: gatewayMethodRegistry.ensureGatewayMethodRegistered,
|
|
197
1100
|
getBridgeSingleton: bridgeSingletonManager.getBridgeSingleton,
|
|
198
1101
|
getBridgeOwnerFromBridge: bridgeSingletonManager.getBridgeOwnerFromBridge,
|