@xmoxmo/bncr 0.3.5 → 0.3.7
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 +5 -0
- package/dist/index.js +28 -5
- package/index.ts +55 -721
- package/package.json +8 -4
- package/scripts/check-pack.mjs +93 -18
- package/scripts/check-register-drift.mjs +35 -13
- package/scripts/selfcheck.mjs +80 -11
- package/src/bootstrap/channel-plugin-runtime.ts +81 -0
- package/src/bootstrap/cli.ts +97 -0
- package/src/bootstrap/register-runtime-gateway.ts +129 -0
- package/src/bootstrap/register-runtime-helpers.ts +140 -0
- package/src/bootstrap/register-runtime-singleton.ts +137 -0
- package/src/bootstrap/register-runtime.ts +201 -0
- package/src/bootstrap/runtime-discovery.ts +187 -0
- package/src/bootstrap/runtime-loader.ts +54 -0
- package/src/channel.ts +1590 -4967
- package/src/core/accounts.ts +23 -4
- package/src/core/dead-letter-diagnostics.ts +37 -5
- package/src/core/diagnostics.ts +31 -15
- package/src/core/downlink-health.ts +3 -11
- package/src/core/extended-diagnostics.ts +78 -36
- package/src/core/file-transfer-payloads.ts +1 -1
- package/src/core/logging.ts +1 -0
- package/src/core/outbox-enqueue.ts +13 -2
- package/src/core/outbox-entry-builders.ts +2 -0
- package/src/core/outbox-summary.ts +75 -3
- package/src/core/permissions.ts +15 -2
- package/src/core/persisted-outbox-entry.ts +21 -6
- package/src/core/policy.ts +45 -4
- package/src/core/probe.ts +3 -15
- package/src/core/register-trace.ts +3 -3
- package/src/core/status.ts +43 -4
- package/src/core/targets.ts +216 -205
- package/src/core/types.ts +221 -0
- package/src/core/value-sanitize.ts +29 -0
- package/src/messaging/inbound/commands.ts +147 -172
- package/src/messaging/inbound/context-facts.ts +4 -2
- package/src/messaging/inbound/contracts.ts +70 -0
- package/src/messaging/inbound/dispatch-prep.ts +303 -0
- package/src/messaging/inbound/dispatch.ts +49 -462
- package/src/messaging/inbound/gate.ts +18 -5
- package/src/messaging/inbound/last-route.ts +10 -4
- package/src/messaging/inbound/media-url-download.ts +109 -0
- package/src/messaging/inbound/native-command-runtime.ts +225 -0
- package/src/messaging/inbound/parse.ts +2 -1
- package/src/messaging/inbound/remote-media.ts +49 -0
- package/src/messaging/inbound/reply-config.ts +16 -4
- package/src/messaging/inbound/reply-dispatch.ts +162 -0
- package/src/messaging/inbound/runtime-compat.ts +31 -10
- package/src/messaging/inbound/session-label.ts +15 -7
- package/src/messaging/inbound/turn-context.ts +131 -0
- package/src/messaging/outbound/actions.ts +24 -10
- package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
- package/src/messaging/outbound/diagnostics.ts +31 -355
- package/src/messaging/outbound/durable-message-adapter.ts +20 -16
- package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
- package/src/messaging/outbound/media.ts +24 -13
- package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
- package/src/messaging/outbound/reply-enqueue.ts +57 -134
- package/src/messaging/outbound/send-params.ts +3 -0
- package/src/messaging/outbound/send.ts +19 -10
- package/src/messaging/outbound/session-route.ts +18 -3
- package/src/openclaw/channel-runtime-contracts.ts +76 -0
- package/src/openclaw/config-runtime.ts +13 -7
- package/src/openclaw/inbound-session-runtime.ts +7 -3
- package/src/openclaw/ingress-runtime.ts +17 -27
- package/src/openclaw/reply-runtime.ts +54 -59
- package/src/openclaw/routing-runtime.ts +35 -18
- package/src/openclaw/runtime-surface.ts +156 -12
- package/src/openclaw/sdk-helpers.ts +8 -1
- package/src/openclaw/session-route-runtime.ts +12 -12
- package/src/plugin/ack-outbox-runtime-group.ts +264 -0
- package/src/plugin/bridge-ack-facade.ts +137 -0
- package/src/plugin/bridge-connection-facade.ts +111 -0
- package/src/plugin/bridge-diagnostics-facade.ts +23 -0
- package/src/plugin/bridge-drain-facade.ts +98 -0
- package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
- package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
- package/src/plugin/bridge-lifecycle.ts +156 -0
- package/src/plugin/bridge-media-facade.ts +241 -0
- package/src/plugin/bridge-outbox-facade.ts +182 -0
- package/src/plugin/bridge-runtime-helpers.ts +266 -0
- package/src/plugin/bridge-runtime-snapshots.ts +104 -0
- package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
- package/src/plugin/bridge-status-facade.ts +76 -0
- package/src/plugin/bridge-status-worker-facade.ts +72 -0
- package/src/plugin/bridge-support-runtime.ts +137 -0
- package/src/plugin/bridge-surface-handlers-group.ts +242 -0
- package/src/plugin/bridge-surface-helpers.ts +28 -0
- package/src/plugin/capabilities.ts +1 -3
- package/src/plugin/channel-components.ts +289 -0
- package/src/plugin/channel-inbound-helpers.ts +149 -0
- package/src/plugin/channel-plugin-bridge-group.ts +129 -0
- package/src/plugin/channel-plugin-surface-group.ts +202 -0
- package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
- package/src/plugin/channel-runtime-builders-status.ts +331 -0
- package/src/plugin/channel-runtime-builders.ts +25 -0
- package/src/plugin/channel-runtime-constants.ts +40 -0
- package/src/plugin/channel-runtime-types.ts +146 -0
- package/src/plugin/channel-send-runtime-group.ts +37 -0
- package/src/plugin/channel-send.ts +226 -0
- package/src/plugin/channel-utils.ts +102 -0
- package/src/plugin/config.ts +24 -3
- package/src/plugin/connection-handlers-helpers.ts +254 -0
- package/src/plugin/connection-handlers.ts +440 -0
- package/src/plugin/connection-state-helpers.ts +159 -0
- package/src/plugin/connection-state-runtime-group.ts +51 -0
- package/src/plugin/connection-state.ts +527 -0
- package/src/plugin/diagnostics-handlers.ts +211 -0
- package/src/plugin/error-message.ts +15 -0
- package/src/plugin/file-ack-runtime.ts +284 -0
- package/src/plugin/file-inbound-abort.ts +112 -0
- package/src/plugin/file-inbound-chunk.ts +146 -0
- package/src/plugin/file-inbound-complete.ts +153 -0
- package/src/plugin/file-inbound-handlers.ts +19 -0
- package/src/plugin/file-inbound-init.ts +122 -0
- package/src/plugin/file-inbound-runtime.ts +51 -0
- package/src/plugin/file-inbound-state.ts +62 -0
- package/src/plugin/file-transfer-logs.ts +227 -0
- package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
- package/src/plugin/file-transfer-orchestrator.ts +304 -0
- package/src/plugin/file-transfer-runtime-group.ts +102 -0
- package/src/plugin/file-transfer-send.ts +89 -0
- package/src/plugin/file-transfer-setup.ts +206 -0
- package/src/plugin/gateway-event-context.ts +41 -0
- package/src/plugin/gateway-runtime.ts +14 -4
- package/src/plugin/inbound-acceptance.ts +107 -0
- package/src/plugin/inbound-handlers.ts +248 -0
- package/src/plugin/inbound-surface-handlers-group.ts +152 -0
- package/src/plugin/media-dedupe-runtime.ts +90 -0
- package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
- package/src/plugin/message-ack-runtime.ts +284 -0
- package/src/plugin/message-send.ts +16 -6
- package/src/plugin/messaging.ts +98 -36
- package/src/plugin/outbound.ts +50 -8
- package/src/plugin/outbox-ack-logs.ts +136 -0
- package/src/plugin/outbox-ack-outcome.ts +128 -0
- package/src/plugin/outbox-drain-ack.ts +145 -0
- package/src/plugin/outbox-drain-failure.ts +84 -0
- package/src/plugin/outbox-drain-loop.ts +554 -0
- package/src/plugin/outbox-drain-post-push.ts +159 -0
- package/src/plugin/outbox-drain-runtime.ts +141 -0
- package/src/plugin/outbox-drain-schedule.ts +116 -0
- package/src/plugin/outbox-file-push-flow.ts +69 -0
- package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
- package/src/plugin/outbox-push.ts +267 -0
- package/src/plugin/outbox-route.ts +181 -0
- package/src/plugin/outbox-text-push-flow.ts +90 -0
- package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
- package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
- package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
- package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
- package/src/plugin/setup.ts +33 -6
- package/src/plugin/state-store.ts +249 -0
- package/src/plugin/state-transient-runtime-group.ts +105 -0
- package/src/plugin/status-runtime.ts +251 -0
- package/src/plugin/status.ts +33 -7
- package/src/plugin/target-runtime.ts +141 -0
- package/src/plugin/target-status-runtime-group.ts +130 -0
- package/src/plugin/transient-state-runtime.ts +82 -0
- package/src/runtime/outbound-ack-timeout.ts +5 -3
- package/src/runtime/outbound-flags.ts +24 -8
- package/src/runtime/status-snapshots.ts +36 -7
- package/src/runtime/status-worker.ts +34 -4
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
import { normalizeAccountId } from '../core/accounts.ts';
|
|
3
|
+
import {
|
|
4
|
+
applyOutboundCapability,
|
|
5
|
+
buildCapabilitySnapshot,
|
|
6
|
+
clearOutboundCapability,
|
|
7
|
+
findCapabilityConnection,
|
|
8
|
+
} from '../core/connection-capability.ts';
|
|
9
|
+
import {
|
|
10
|
+
getRevalidatedAttemptReason,
|
|
11
|
+
hasAlternativeLiveConnection as hasAlternativeLiveConnectionFromRuntime,
|
|
12
|
+
hasRecentInboundReachability as hasRecentInboundReachabilityFromRuntime,
|
|
13
|
+
isRecentlyReachableConn as isRecentlyReachableConnFromRuntime,
|
|
14
|
+
resolveRecentInboundConnIds as resolveRecentInboundConnIdsFromRuntime,
|
|
15
|
+
} from '../core/connection-reachability.ts';
|
|
16
|
+
import type { BncrConnection, OutboxEntry } from '../core/types.ts';
|
|
17
|
+
import {
|
|
18
|
+
buildConnectionCapabilityDebugPayload,
|
|
19
|
+
buildConnectionCapabilityDebugSig,
|
|
20
|
+
buildConnectionDegradePayload,
|
|
21
|
+
buildConnectionDegradeSkipPayload,
|
|
22
|
+
buildConnectionPromotePayload,
|
|
23
|
+
buildSeenConnection,
|
|
24
|
+
resolveSeenConnectionPromoteReason,
|
|
25
|
+
} from './connection-state-helpers.ts';
|
|
26
|
+
|
|
27
|
+
export type BncrActiveConnectionDebugEntry = {
|
|
28
|
+
accountId: string;
|
|
29
|
+
connId: string;
|
|
30
|
+
clientId?: string;
|
|
31
|
+
connectedAt: number;
|
|
32
|
+
lastSeenAt: number;
|
|
33
|
+
outboundReadyUntil?: number | null;
|
|
34
|
+
preferredForOutboundUntil?: number | null;
|
|
35
|
+
inboundOnly?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type BncrCapabilityConnection = BncrConnection & {
|
|
39
|
+
outboundReadyUntil?: number;
|
|
40
|
+
preferredForOutboundUntil?: number;
|
|
41
|
+
inboundOnly?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// This module owns the live connection model used by outbound routing:
|
|
45
|
+
// seen/active state, outbound capability, recent inbound reachability, and
|
|
46
|
+
// degradation when ACK / push signals prove a connection is no longer usable.
|
|
47
|
+
|
|
48
|
+
function logSeenConnectionPromotion(args: {
|
|
49
|
+
runtime: Pick<
|
|
50
|
+
BncrConnectionStateRuntime,
|
|
51
|
+
'bridgeId' | 'buildActiveConnectionDebugList' | 'logInfo'
|
|
52
|
+
>;
|
|
53
|
+
accountId: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
previousActiveKey: string | null;
|
|
56
|
+
previousActiveConn: BncrConnection | null;
|
|
57
|
+
nextActiveKey: string;
|
|
58
|
+
nextActiveConn: BncrConnection;
|
|
59
|
+
}) {
|
|
60
|
+
args.runtime.logInfo(
|
|
61
|
+
'connection',
|
|
62
|
+
`seen:promote ${JSON.stringify(
|
|
63
|
+
buildConnectionPromotePayload({
|
|
64
|
+
bridgeId: args.runtime.bridgeId,
|
|
65
|
+
accountId: args.accountId,
|
|
66
|
+
reason: args.reason,
|
|
67
|
+
previousActiveKey: args.previousActiveKey,
|
|
68
|
+
previousActiveConn: args.previousActiveConn,
|
|
69
|
+
nextActiveKey: args.nextActiveKey,
|
|
70
|
+
nextActiveConn: args.nextActiveConn,
|
|
71
|
+
activeConnections: args.runtime.buildActiveConnectionDebugList(args.accountId, {
|
|
72
|
+
includeOutboundState: true,
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
)}`,
|
|
76
|
+
{ debugOnly: true },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TransferOwnerState = {
|
|
81
|
+
ownerConnId?: string;
|
|
82
|
+
ownerClientId?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type BncrConnectionStateRuntime = {
|
|
86
|
+
bridgeId: string;
|
|
87
|
+
now: () => number;
|
|
88
|
+
asString: (value: unknown, fallback?: string) => string;
|
|
89
|
+
connectTtlMs: number;
|
|
90
|
+
recentInboundSendWindowMs: number;
|
|
91
|
+
outboundReadyTtlMs: number;
|
|
92
|
+
preferredOutboundTtlMs: number;
|
|
93
|
+
connections: Map<string, BncrConnection>;
|
|
94
|
+
activeConnectionByAccount: Map<string, string>;
|
|
95
|
+
lastInboundByAccount: Map<string, number>;
|
|
96
|
+
lastActivityByAccount: Map<string, number>;
|
|
97
|
+
gcTransientState: () => void;
|
|
98
|
+
connectionKey: (accountId: string, clientId?: string) => string;
|
|
99
|
+
buildActiveConnectionDebugList: (
|
|
100
|
+
accountId: string,
|
|
101
|
+
options?: { includeOutboundState?: boolean },
|
|
102
|
+
) => BncrActiveConnectionDebugEntry[];
|
|
103
|
+
rememberGatewayContext: (context: GatewayRequestHandlerOptions['context']) => void;
|
|
104
|
+
markActivity: (accountId: string, at?: number) => void;
|
|
105
|
+
logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
|
|
106
|
+
logInfoDedupJson: (
|
|
107
|
+
scope: string,
|
|
108
|
+
label: string,
|
|
109
|
+
payload: unknown,
|
|
110
|
+
options?: { key?: string; sig?: string; debugOnly?: boolean },
|
|
111
|
+
) => void;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function createBncrConnectionState(runtime: BncrConnectionStateRuntime) {
|
|
115
|
+
// 1) Reachability/read helpers
|
|
116
|
+
// 2) Live-state refresh entrypoints
|
|
117
|
+
// 3) Canonical live-connection mutations
|
|
118
|
+
// 4) Outbound capability degradation / online probes
|
|
119
|
+
//
|
|
120
|
+
// Keep this order stable. The later mutation paths depend on the earlier
|
|
121
|
+
// read-model helpers, and that dependency direction makes the file easier to
|
|
122
|
+
// scan than grouping everything by event type.
|
|
123
|
+
|
|
124
|
+
// Reachability helpers are grouped first because they are consumed by both
|
|
125
|
+
// ownership adoption and outbound route-selection decisions later in the file.
|
|
126
|
+
|
|
127
|
+
const hasRecentInboundReachability = (accountId: string): boolean => {
|
|
128
|
+
const acc = normalizeAccountId(accountId);
|
|
129
|
+
return hasRecentInboundReachabilityFromRuntime({
|
|
130
|
+
now: runtime.now(),
|
|
131
|
+
windowMs: runtime.recentInboundSendWindowMs,
|
|
132
|
+
lastInboundAt: runtime.lastInboundByAccount.get(acc) || 0,
|
|
133
|
+
lastActivityAt: runtime.lastActivityByAccount.get(acc) || 0,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const resolveRecentInboundConnIds = (accountId: string): Set<string> => {
|
|
138
|
+
const acc = normalizeAccountId(accountId);
|
|
139
|
+
return resolveRecentInboundConnIdsFromRuntime({
|
|
140
|
+
accountId: acc,
|
|
141
|
+
now: runtime.now(),
|
|
142
|
+
connectTtlMs: runtime.connectTtlMs,
|
|
143
|
+
recentInboundReachable: hasRecentInboundReachability(acc),
|
|
144
|
+
connections: runtime.connections.values(),
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const isRecentlyReachableConn = (
|
|
149
|
+
accountId: string,
|
|
150
|
+
connId?: string,
|
|
151
|
+
clientId?: string,
|
|
152
|
+
): boolean => {
|
|
153
|
+
const acc = normalizeAccountId(accountId);
|
|
154
|
+
const activeKey = runtime.activeConnectionByAccount.get(acc);
|
|
155
|
+
const active = activeKey ? runtime.connections.get(activeKey) || null : null;
|
|
156
|
+
return isRecentlyReachableConnFromRuntime({
|
|
157
|
+
accountId: acc,
|
|
158
|
+
connId,
|
|
159
|
+
clientId,
|
|
160
|
+
recentConnIds: resolveRecentInboundConnIds(acc),
|
|
161
|
+
activeConnection: active,
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const isRevalidatedAttemptedConn = (entry: OutboxEntry, connId: string): boolean => {
|
|
166
|
+
const acc = normalizeAccountId(entry.accountId);
|
|
167
|
+
const revalidated = getRevalidatedAttemptReason({
|
|
168
|
+
entry,
|
|
169
|
+
connId,
|
|
170
|
+
accountId: acc,
|
|
171
|
+
now: runtime.now(),
|
|
172
|
+
connectTtlMs: runtime.connectTtlMs,
|
|
173
|
+
recentInboundReachable: hasRecentInboundReachability(acc),
|
|
174
|
+
connections: runtime.connections.values(),
|
|
175
|
+
});
|
|
176
|
+
if (!revalidated) return false;
|
|
177
|
+
|
|
178
|
+
runtime.logInfo(
|
|
179
|
+
'outbox',
|
|
180
|
+
`revalidated-retry ${JSON.stringify({
|
|
181
|
+
messageId: entry.messageId,
|
|
182
|
+
accountId: acc,
|
|
183
|
+
connId: String(connId || '').trim(),
|
|
184
|
+
...revalidated,
|
|
185
|
+
})}`,
|
|
186
|
+
{ debugOnly: true },
|
|
187
|
+
);
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const tryAdoptTransferOwner = (args: {
|
|
192
|
+
accountId: string;
|
|
193
|
+
transfer: TransferOwnerState | undefined;
|
|
194
|
+
connId: string;
|
|
195
|
+
clientId?: string;
|
|
196
|
+
}): boolean => {
|
|
197
|
+
const { accountId, transfer, connId, clientId } = args;
|
|
198
|
+
if (!transfer) return false;
|
|
199
|
+
if (!hasRecentInboundReachability(accountId)) return false;
|
|
200
|
+
if (!isRecentlyReachableConn(accountId, connId, clientId)) return false;
|
|
201
|
+
|
|
202
|
+
transfer.ownerConnId = connId;
|
|
203
|
+
transfer.ownerClientId = runtime.asString(clientId || '').trim() || undefined;
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Live-state refresh entrypoints -----------------------------------------
|
|
208
|
+
// These are the external-facing refresh paths used by connect/activity/file
|
|
209
|
+
// transfer acceptance before deeper capability or routing decisions.
|
|
210
|
+
|
|
211
|
+
// Live-state refresh is the steady-state entrypoint used by connection and
|
|
212
|
+
// activity events before any explicit routing/capability mutations happen.
|
|
213
|
+
|
|
214
|
+
const refreshAcceptedFileTransferLiveState = (args: {
|
|
215
|
+
accountId: string;
|
|
216
|
+
connId: string;
|
|
217
|
+
clientId?: string;
|
|
218
|
+
context: GatewayRequestHandlerOptions['context'];
|
|
219
|
+
}) => {
|
|
220
|
+
const { accountId, connId, clientId, context } = args;
|
|
221
|
+
runtime.rememberGatewayContext(context);
|
|
222
|
+
markSeen(accountId, connId, clientId);
|
|
223
|
+
runtime.markActivity(accountId);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const refreshLiveConnectionState = (args: {
|
|
227
|
+
accountId: string;
|
|
228
|
+
connId: string;
|
|
229
|
+
clientId?: string;
|
|
230
|
+
outboundReady: boolean;
|
|
231
|
+
preferredForOutbound: boolean;
|
|
232
|
+
inboundOnly: boolean;
|
|
233
|
+
context: GatewayRequestHandlerOptions['context'];
|
|
234
|
+
}) => {
|
|
235
|
+
const {
|
|
236
|
+
accountId,
|
|
237
|
+
connId,
|
|
238
|
+
clientId,
|
|
239
|
+
outboundReady,
|
|
240
|
+
preferredForOutbound,
|
|
241
|
+
inboundOnly,
|
|
242
|
+
context,
|
|
243
|
+
} = args;
|
|
244
|
+
refreshAcceptedFileTransferLiveState({
|
|
245
|
+
accountId,
|
|
246
|
+
connId,
|
|
247
|
+
clientId,
|
|
248
|
+
context,
|
|
249
|
+
});
|
|
250
|
+
markOutboundCapability({
|
|
251
|
+
accountId,
|
|
252
|
+
connId,
|
|
253
|
+
clientId,
|
|
254
|
+
outboundReady,
|
|
255
|
+
preferredForOutbound,
|
|
256
|
+
inboundOnly,
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// The remaining helpers mutate the canonical live-connection model used by
|
|
261
|
+
// route selection, ownership recovery, and capability degradation.
|
|
262
|
+
|
|
263
|
+
// Canonical live-connection mutations ------------------------------------
|
|
264
|
+
|
|
265
|
+
const markSeen = (accountId: string, connId: string, clientId?: string) => {
|
|
266
|
+
runtime.gcTransientState();
|
|
267
|
+
|
|
268
|
+
const acc = normalizeAccountId(accountId);
|
|
269
|
+
const key = runtime.connectionKey(acc, clientId);
|
|
270
|
+
const t = runtime.now();
|
|
271
|
+
const prev = runtime.connections.get(key) as BncrCapabilityConnection | undefined;
|
|
272
|
+
const previousActiveKey = runtime.activeConnectionByAccount.get(acc) || null;
|
|
273
|
+
const previousActiveConn = previousActiveKey
|
|
274
|
+
? runtime.connections.get(previousActiveKey) || null
|
|
275
|
+
: null;
|
|
276
|
+
|
|
277
|
+
const nextConn = buildSeenConnection({
|
|
278
|
+
accountId: acc,
|
|
279
|
+
connId,
|
|
280
|
+
clientId: runtime.asString(clientId || '').trim() || undefined,
|
|
281
|
+
nowMs: t,
|
|
282
|
+
previous: prev || null,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
runtime.connections.set(key, nextConn);
|
|
286
|
+
const connectionSeenPayload = {
|
|
287
|
+
bridge: runtime.bridgeId,
|
|
288
|
+
accountId: acc,
|
|
289
|
+
connId,
|
|
290
|
+
clientId: nextConn.clientId,
|
|
291
|
+
connectedAt: nextConn.connectedAt,
|
|
292
|
+
lastSeenAt: nextConn.lastSeenAt,
|
|
293
|
+
outboundReadyUntil: nextConn.outboundReadyUntil || null,
|
|
294
|
+
preferredForOutboundUntil: nextConn.preferredForOutboundUntil || null,
|
|
295
|
+
inboundOnly: nextConn.inboundOnly === true,
|
|
296
|
+
};
|
|
297
|
+
const connectionSeenSig = JSON.stringify({
|
|
298
|
+
bridge: runtime.bridgeId,
|
|
299
|
+
accountId: acc,
|
|
300
|
+
connId,
|
|
301
|
+
clientId: nextConn.clientId || null,
|
|
302
|
+
inboundOnly: nextConn.inboundOnly === true,
|
|
303
|
+
outboundReadyActive: Number(nextConn.outboundReadyUntil || 0) > t,
|
|
304
|
+
preferredForOutboundActive: Number(nextConn.preferredForOutboundUntil || 0) > t,
|
|
305
|
+
});
|
|
306
|
+
runtime.logInfoDedupJson('connection', 'seen', connectionSeenPayload, {
|
|
307
|
+
key: `connection-seen:${acc}:${nextConn.clientId || connId}`,
|
|
308
|
+
sig: connectionSeenSig,
|
|
309
|
+
debugOnly: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const current = runtime.activeConnectionByAccount.get(acc) || null;
|
|
313
|
+
const curConn = current ? runtime.connections.get(current) || null : null;
|
|
314
|
+
const promoteReason = resolveSeenConnectionPromoteReason({
|
|
315
|
+
currentActiveKey: current,
|
|
316
|
+
currentConnection: curConn,
|
|
317
|
+
nowMs: t,
|
|
318
|
+
connectTtlMs: runtime.connectTtlMs,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (promoteReason === 'no-current-active') {
|
|
322
|
+
runtime.activeConnectionByAccount.set(acc, key);
|
|
323
|
+
logSeenConnectionPromotion({
|
|
324
|
+
runtime,
|
|
325
|
+
accountId: acc,
|
|
326
|
+
reason: promoteReason,
|
|
327
|
+
previousActiveKey,
|
|
328
|
+
previousActiveConn,
|
|
329
|
+
nextActiveKey: key,
|
|
330
|
+
nextActiveConn: nextConn,
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (promoteReason) {
|
|
336
|
+
runtime.activeConnectionByAccount.set(acc, key);
|
|
337
|
+
logSeenConnectionPromotion({
|
|
338
|
+
runtime,
|
|
339
|
+
accountId: acc,
|
|
340
|
+
reason: promoteReason,
|
|
341
|
+
previousActiveKey,
|
|
342
|
+
previousActiveConn,
|
|
343
|
+
nextActiveKey: key,
|
|
344
|
+
nextActiveConn: nextConn,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Capability updates keep the live routing model in sync with what the
|
|
350
|
+
// client claims it can currently do for outbound delivery.
|
|
351
|
+
|
|
352
|
+
// Outbound capability / degradation --------------------------------------
|
|
353
|
+
|
|
354
|
+
const markOutboundCapability = (args: {
|
|
355
|
+
accountId: string;
|
|
356
|
+
connId: string;
|
|
357
|
+
clientId?: string;
|
|
358
|
+
outboundReady?: boolean;
|
|
359
|
+
preferredForOutbound?: boolean;
|
|
360
|
+
inboundOnly?: boolean;
|
|
361
|
+
at?: number;
|
|
362
|
+
}) => {
|
|
363
|
+
const acc = normalizeAccountId(args.accountId);
|
|
364
|
+
const key = runtime.connectionKey(acc, args.clientId);
|
|
365
|
+
const t = Number(args.at || runtime.now());
|
|
366
|
+
const current = runtime.connections.get(key) as BncrCapabilityConnection | undefined;
|
|
367
|
+
if (!current || current.connId !== args.connId) return;
|
|
368
|
+
|
|
369
|
+
const next = applyOutboundCapability({
|
|
370
|
+
connection: current,
|
|
371
|
+
at: t,
|
|
372
|
+
outboundReadyTtlMs: runtime.outboundReadyTtlMs,
|
|
373
|
+
preferredOutboundTtlMs: runtime.preferredOutboundTtlMs,
|
|
374
|
+
outboundReady: args.outboundReady,
|
|
375
|
+
preferredForOutbound: args.preferredForOutbound,
|
|
376
|
+
inboundOnly: args.inboundOnly,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
runtime.connections.set(key, next);
|
|
380
|
+
const { payload: connectionCapabilityPayload, snapshot } =
|
|
381
|
+
buildConnectionCapabilityDebugPayload({
|
|
382
|
+
bridgeId: runtime.bridgeId,
|
|
383
|
+
accountId: acc,
|
|
384
|
+
connection: next,
|
|
385
|
+
outboundReady: args.outboundReady === true,
|
|
386
|
+
preferredForOutbound: args.preferredForOutbound === true,
|
|
387
|
+
});
|
|
388
|
+
const connectionCapabilitySig = buildConnectionCapabilityDebugSig({
|
|
389
|
+
bridgeId: runtime.bridgeId,
|
|
390
|
+
accountId: acc,
|
|
391
|
+
connection: next,
|
|
392
|
+
outboundReady: args.outboundReady === true,
|
|
393
|
+
preferredForOutbound: args.preferredForOutbound === true,
|
|
394
|
+
snapshot,
|
|
395
|
+
nowMs: t,
|
|
396
|
+
});
|
|
397
|
+
runtime.logInfoDedupJson('connection', 'capability', connectionCapabilityPayload, {
|
|
398
|
+
key: `connection-capability:${acc}:${next.clientId || next.connId}`,
|
|
399
|
+
sig: connectionCapabilitySig,
|
|
400
|
+
debugOnly: true,
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const hasAlternativeLiveConnection = (
|
|
405
|
+
accountId: string,
|
|
406
|
+
currentConnId?: string,
|
|
407
|
+
currentClientId?: string,
|
|
408
|
+
): boolean => {
|
|
409
|
+
const acc = normalizeAccountId(accountId);
|
|
410
|
+
return hasAlternativeLiveConnectionFromRuntime({
|
|
411
|
+
accountId: acc,
|
|
412
|
+
now: runtime.now(),
|
|
413
|
+
connectTtlMs: runtime.connectTtlMs,
|
|
414
|
+
currentConnId,
|
|
415
|
+
currentClientId,
|
|
416
|
+
connections: runtime.connections.values(),
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Degradation is the corrective path after push/ACK evidence says the current
|
|
421
|
+
// outbound capability should no longer be trusted for routing decisions.
|
|
422
|
+
|
|
423
|
+
const degradeOutboundCapability = (args: {
|
|
424
|
+
accountId: string;
|
|
425
|
+
connId?: string;
|
|
426
|
+
clientId?: string;
|
|
427
|
+
reason: string;
|
|
428
|
+
at?: number;
|
|
429
|
+
}) => {
|
|
430
|
+
const acc = normalizeAccountId(args.accountId);
|
|
431
|
+
const t = Number(args.at || runtime.now());
|
|
432
|
+
const hasAlternative = hasAlternativeLiveConnection(acc, args.connId, args.clientId);
|
|
433
|
+
const currentKey = runtime.activeConnectionByAccount.get(acc) || null;
|
|
434
|
+
const matched = findCapabilityConnection({
|
|
435
|
+
accountId: acc,
|
|
436
|
+
connId: args.connId,
|
|
437
|
+
clientId: args.clientId,
|
|
438
|
+
connections: runtime.connections.entries(),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!matched) return;
|
|
442
|
+
|
|
443
|
+
const before = buildCapabilitySnapshot(matched.connection);
|
|
444
|
+
|
|
445
|
+
if (!hasAlternative) {
|
|
446
|
+
runtime.logInfo(
|
|
447
|
+
'connection',
|
|
448
|
+
`outbound-degrade skip ${JSON.stringify(
|
|
449
|
+
buildConnectionDegradeSkipPayload({
|
|
450
|
+
bridgeId: runtime.bridgeId,
|
|
451
|
+
accountId: acc,
|
|
452
|
+
connection: matched.connection,
|
|
453
|
+
reason: args.reason,
|
|
454
|
+
at: t,
|
|
455
|
+
currentActiveKey: currentKey,
|
|
456
|
+
degradedKey: matched.key,
|
|
457
|
+
before,
|
|
458
|
+
}),
|
|
459
|
+
)}`,
|
|
460
|
+
{ debugOnly: true },
|
|
461
|
+
);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const next = clearOutboundCapability(matched.connection);
|
|
466
|
+
runtime.connections.set(matched.key, next);
|
|
467
|
+
|
|
468
|
+
runtime.logInfo(
|
|
469
|
+
'connection',
|
|
470
|
+
`outbound-degrade ${JSON.stringify(
|
|
471
|
+
buildConnectionDegradePayload({
|
|
472
|
+
bridgeId: runtime.bridgeId,
|
|
473
|
+
accountId: acc,
|
|
474
|
+
connection: next,
|
|
475
|
+
reason: args.reason,
|
|
476
|
+
at: t,
|
|
477
|
+
currentActiveKey: currentKey,
|
|
478
|
+
degradedKey: matched.key,
|
|
479
|
+
before,
|
|
480
|
+
after: buildCapabilitySnapshot(next),
|
|
481
|
+
}),
|
|
482
|
+
)}`,
|
|
483
|
+
{ debugOnly: true },
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Online/read-model helpers ----------------------------------------------
|
|
488
|
+
// These stay at the end because they are pure projections over the runtime
|
|
489
|
+
// maps after all mutation logic above has established the current state.
|
|
490
|
+
|
|
491
|
+
const isOnline = (accountId: string): boolean => {
|
|
492
|
+
const acc = normalizeAccountId(accountId);
|
|
493
|
+
const t = runtime.now();
|
|
494
|
+
for (const c of runtime.connections.values()) {
|
|
495
|
+
if (c.accountId !== acc) continue;
|
|
496
|
+
if (t - c.lastSeenAt <= runtime.connectTtlMs) return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const activeConnectionCount = (accountId: string): number => {
|
|
502
|
+
const acc = normalizeAccountId(accountId);
|
|
503
|
+
const t = runtime.now();
|
|
504
|
+
let n = 0;
|
|
505
|
+
for (const c of runtime.connections.values()) {
|
|
506
|
+
if (c.accountId !== acc) continue;
|
|
507
|
+
if (t - c.lastSeenAt <= runtime.connectTtlMs) n += 1;
|
|
508
|
+
}
|
|
509
|
+
return n;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
hasRecentInboundReachability,
|
|
514
|
+
resolveRecentInboundConnIds,
|
|
515
|
+
isRecentlyReachableConn,
|
|
516
|
+
isRevalidatedAttemptedConn,
|
|
517
|
+
tryAdoptTransferOwner,
|
|
518
|
+
refreshAcceptedFileTransferLiveState,
|
|
519
|
+
refreshLiveConnectionState,
|
|
520
|
+
markSeen,
|
|
521
|
+
markOutboundCapability,
|
|
522
|
+
hasAlternativeLiveConnection,
|
|
523
|
+
degradeOutboundCapability,
|
|
524
|
+
isOnline,
|
|
525
|
+
activeConnectionCount,
|
|
526
|
+
};
|
|
527
|
+
}
|