@xmoxmo/bncr 0.5.6 → 0.5.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 +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -8
- package/scripts/selfcheck.mjs +0 -10
- package/src/bootstrap/runtime-loader.ts +1 -1
- package/src/channel.ts +118 -171
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +2 -6
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/logging.ts +1 -1
- package/src/core/outbox-entry-builders.ts +78 -84
- package/src/core/outbox-file-transfer-failure.ts +5 -0
- package/src/core/outbox-file-transfer-guards.ts +6 -0
- package/src/core/outbox-file-transfer-prep.ts +8 -5
- package/src/core/outbox-file-transfer-success.ts +5 -0
- package/src/core/outbox-queue.ts +1 -7
- package/src/core/outbox-summary.ts +10 -56
- package/src/core/permissions.ts +2 -10
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +4 -7
- package/src/core/status-meta.ts +1 -9
- package/src/core/status.ts +1 -21
- package/src/core/targets.ts +52 -70
- package/src/core/types.ts +1 -17
- package/src/core/value-sanitize.ts +38 -1
- package/src/messaging/inbound/commands.ts +2 -24
- package/src/messaging/inbound/dispatch-prep.ts +1 -1
- package/src/messaging/inbound/gate.ts +2 -31
- package/src/messaging/inbound/group-history.ts +0 -75
- package/src/messaging/inbound/parse.ts +2 -32
- package/src/messaging/inbound/scene-admin.ts +8 -8
- package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
- package/src/messaging/outbound/diagnostics.ts +1 -7
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +277 -28
- package/src/messaging/outbound/media-dedupe.ts +3 -10
- package/src/messaging/outbound/media.ts +1 -6
- package/src/messaging/outbound/normalize-outbound-send.ts +329 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
- package/src/messaging/outbound/reply-enqueue.ts +313 -175
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/media-runtime.ts +1 -1
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-media-facade.ts +0 -4
- package/src/plugin/bridge-outbox-facade.ts +0 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +52 -65
- package/src/plugin/channel-runtime-builders-delivery.ts +3 -362
- package/src/plugin/channel-runtime-builders-status.ts +0 -160
- package/src/plugin/channel-runtime-builders.ts +3 -17
- package/src/plugin/channel-runtime-constants.ts +1 -1
- package/src/plugin/channel-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +206 -221
- package/src/plugin/channel-utils.ts +8 -29
- package/src/plugin/connection-handlers-helpers.ts +0 -15
- package/src/plugin/connection-handlers.ts +6 -5
- package/src/plugin/file-inbound-abort.ts +5 -0
- package/src/plugin/file-inbound-chunk.ts +5 -0
- package/src/plugin/file-inbound-complete.ts +5 -0
- package/src/plugin/file-transfer-orchestrator.ts +39 -2
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/messaging.ts +3 -3
- package/src/plugin/outbound.ts +6 -26
- package/src/plugin/outbox-drain-loop.ts +2 -2
- package/src/plugin/outbox-push.ts +1 -52
- package/src/plugin/outbox-unified-push-flow.ts +307 -0
- package/src/plugin/scene-registry.ts +1 -1
- package/src/plugin/target-runtime.ts +1 -6
- package/src/runtime/log-dedupe.ts +3 -3
- package/src/runtime/outbound-ack-timeout.ts +3 -12
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-snapshots.ts +2 -2
- package/src/runtime/status-worker.ts +3 -15
- package/src/core/outbox-text-push-prep.ts +0 -36
- package/src/messaging/outbound/build-send-action.ts +0 -130
- package/src/messaging/outbound/durable-message-adapter.ts +0 -121
- package/src/messaging/outbound/send-params.ts +0 -114
- package/src/messaging/outbound/send.ts +0 -127
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/bridge-file-transfer-push-facade.ts +0 -142
- package/src/plugin/channel-send-runtime-group.ts +0 -37
- package/src/plugin/outbox-file-push-flow.ts +0 -69
- package/src/plugin/outbox-text-push-flow.ts +0 -90
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildFileTransferPushOkArgs,
|
|
3
|
+
buildFileTransferPushSuccessArgs,
|
|
4
|
+
} from '../core/outbox-file-transfer-bookkeeping.ts';
|
|
5
|
+
import { resolveFileTransferGuard } from '../core/outbox-file-transfer-guards.ts';
|
|
6
|
+
import {
|
|
7
|
+
prepareRouteSelection,
|
|
8
|
+
prepareRouteSelection as prepareTextPushRouteSelection,
|
|
9
|
+
} from '../core/outbox-file-transfer-prep.ts';
|
|
10
|
+
import {
|
|
11
|
+
buildFileTransferBroadcastPayload,
|
|
12
|
+
buildFileTransferRouteSelectArgs,
|
|
13
|
+
} from '../core/outbox-file-transfer-success.ts';
|
|
14
|
+
import { resolveTextPushGuard } from '../core/outbox-text-push-guards.ts';
|
|
15
|
+
import {
|
|
16
|
+
buildTextPushBroadcastPayload,
|
|
17
|
+
buildTextPushOkArgs,
|
|
18
|
+
buildTextPushRouteSelectArgs,
|
|
19
|
+
buildTextPushSuccessArgs,
|
|
20
|
+
} from '../core/outbox-text-push-success.ts';
|
|
21
|
+
import type { BncrConnection, BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
22
|
+
import { isPlainObject } from '../core/value-sanitize.ts';
|
|
23
|
+
import {
|
|
24
|
+
selectOutboxFileTransferRouteCandidates,
|
|
25
|
+
selectOutboxRouteCandidates,
|
|
26
|
+
} from '../messaging/outbound/queue-selectors.ts';
|
|
27
|
+
|
|
28
|
+
type FileTransferGuardFailure = {
|
|
29
|
+
reason: 'media-url-missing' | 'no-gateway-context' | 'no-active-connection';
|
|
30
|
+
lastError: string;
|
|
31
|
+
recentInboundReachable?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RunBncrOutboxPushArgs = {
|
|
35
|
+
entry: OutboxEntry;
|
|
36
|
+
gatewayContext: unknown;
|
|
37
|
+
owner: BncrConnection | null;
|
|
38
|
+
resolvePushConnIds: (accountId: string) => Set<string>;
|
|
39
|
+
resolveRecentInboundConnIds: (accountId: string) => Set<string>;
|
|
40
|
+
hasRecentInboundReachability: (accountId: string) => boolean;
|
|
41
|
+
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
42
|
+
// Route + guard failure
|
|
43
|
+
recordOutboxPrePushFailure: (args: {
|
|
44
|
+
entry: OutboxEntry;
|
|
45
|
+
lastError: string;
|
|
46
|
+
persist?: boolean;
|
|
47
|
+
}) => void;
|
|
48
|
+
logOutboxPushSkip: (args: {
|
|
49
|
+
messageId: string;
|
|
50
|
+
accountId: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
recentInboundReachable?: boolean;
|
|
53
|
+
routeReason?: string;
|
|
54
|
+
connIds?: Iterable<string>;
|
|
55
|
+
ownerConnId?: string;
|
|
56
|
+
ownerClientId?: string;
|
|
57
|
+
}) => void;
|
|
58
|
+
handleFileTransferPushGuardFailure: (args: {
|
|
59
|
+
entry: OutboxEntry;
|
|
60
|
+
guard: FileTransferGuardFailure & { ok: false };
|
|
61
|
+
}) => void;
|
|
62
|
+
// Broadcast + bookkeeping (unified)
|
|
63
|
+
pushEvent: string;
|
|
64
|
+
gatewayBroadcastToConnIds: (
|
|
65
|
+
event: string,
|
|
66
|
+
payload: unknown,
|
|
67
|
+
connIds: ReadonlySet<string>,
|
|
68
|
+
) => void;
|
|
69
|
+
recordOutboxPushSuccess: (args: {
|
|
70
|
+
entry: OutboxEntry;
|
|
71
|
+
connIds: Iterable<string>;
|
|
72
|
+
ownerConnId?: string;
|
|
73
|
+
ownerClientId?: string;
|
|
74
|
+
}) => void;
|
|
75
|
+
logOutboxRouteSelect: (args: {
|
|
76
|
+
messageId: string;
|
|
77
|
+
accountId: string;
|
|
78
|
+
kind?: 'file-transfer';
|
|
79
|
+
routeReason: string;
|
|
80
|
+
connIds: Iterable<string>;
|
|
81
|
+
ownerConnId: string;
|
|
82
|
+
ownerClientId: string;
|
|
83
|
+
recentInboundReachable: boolean;
|
|
84
|
+
event: string;
|
|
85
|
+
}) => void;
|
|
86
|
+
logOutboxPushOkSummary: (messageId: string) => void;
|
|
87
|
+
logOutboundSummary: (entry: OutboxEntry) => void;
|
|
88
|
+
logOutboxPushOk: (args: {
|
|
89
|
+
messageId: string;
|
|
90
|
+
accountId: string;
|
|
91
|
+
kind?: 'file-transfer';
|
|
92
|
+
connIds: Iterable<string>;
|
|
93
|
+
ownerConnId: string;
|
|
94
|
+
ownerClientId: string;
|
|
95
|
+
recentInboundReachable: boolean;
|
|
96
|
+
event: string;
|
|
97
|
+
}) => void;
|
|
98
|
+
// Failure handlers
|
|
99
|
+
handleTextPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
100
|
+
handleFileTransferPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
101
|
+
// Media-specific (optional)
|
|
102
|
+
transferMediaToBncrClient?: (params: {
|
|
103
|
+
accountId: string;
|
|
104
|
+
sessionKey: string;
|
|
105
|
+
route: BncrRoute;
|
|
106
|
+
mediaUrl: string;
|
|
107
|
+
mediaLocalRoots?: readonly string[];
|
|
108
|
+
downloadMedia?: boolean;
|
|
109
|
+
}) => Promise<{
|
|
110
|
+
mode: 'base64' | 'chunk';
|
|
111
|
+
mimeType?: string;
|
|
112
|
+
fileName?: string;
|
|
113
|
+
base64?: string;
|
|
114
|
+
path?: string;
|
|
115
|
+
}>;
|
|
116
|
+
buildFileTransferOutboundFrame?: (params: {
|
|
117
|
+
entry: OutboxEntry;
|
|
118
|
+
msg: Record<string, unknown>;
|
|
119
|
+
media: { fileName?: string; mimeType?: string; path?: string; base64?: string; type?: string };
|
|
120
|
+
mediaUrl: string;
|
|
121
|
+
}) => Record<string, unknown>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export async function runBncrOutboxPush(args: RunBncrOutboxPushArgs): Promise<boolean> {
|
|
125
|
+
const entry = args.entry;
|
|
126
|
+
const msg =
|
|
127
|
+
isPlainObject(entry.payload) &&
|
|
128
|
+
isPlainObject((entry.payload as Record<string, unknown>).message)
|
|
129
|
+
? ((entry.payload as Record<string, unknown>).message as Record<string, unknown>)
|
|
130
|
+
: null;
|
|
131
|
+
const transferMode = msg?.transferMode === 'media' ? 'media' : 'text';
|
|
132
|
+
|
|
133
|
+
// Route selection
|
|
134
|
+
const selection =
|
|
135
|
+
transferMode === 'media'
|
|
136
|
+
? prepareRouteSelection({
|
|
137
|
+
entry,
|
|
138
|
+
owner: args.owner,
|
|
139
|
+
resolvePushConnIds: args.resolvePushConnIds,
|
|
140
|
+
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
141
|
+
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
142
|
+
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
143
|
+
selectRouteCandidates: selectOutboxFileTransferRouteCandidates,
|
|
144
|
+
})
|
|
145
|
+
: prepareTextPushRouteSelection({
|
|
146
|
+
entry,
|
|
147
|
+
owner: args.owner,
|
|
148
|
+
resolvePushConnIds: args.resolvePushConnIds,
|
|
149
|
+
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
150
|
+
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
151
|
+
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
152
|
+
selectRouteCandidates: selectOutboxRouteCandidates,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Guard + dispatch
|
|
156
|
+
if (transferMode === 'media') {
|
|
157
|
+
const guard = resolveFileTransferGuard({
|
|
158
|
+
gatewayContext: args.gatewayContext,
|
|
159
|
+
entry,
|
|
160
|
+
owner: args.owner,
|
|
161
|
+
routeSelection: selection,
|
|
162
|
+
mediaUrl: String(msg!.mediaUrl || '').trim(),
|
|
163
|
+
});
|
|
164
|
+
if (!guard.ok) {
|
|
165
|
+
args.handleFileTransferPushGuardFailure({
|
|
166
|
+
entry,
|
|
167
|
+
guard: guard as FileTransferGuardFailure & { ok: false },
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Media success path (inline) ──
|
|
173
|
+
try {
|
|
174
|
+
const media = await args.transferMediaToBncrClient!({
|
|
175
|
+
accountId: entry.accountId,
|
|
176
|
+
sessionKey: entry.sessionKey,
|
|
177
|
+
route: entry.route,
|
|
178
|
+
mediaUrl: guard.mediaUrl,
|
|
179
|
+
mediaLocalRoots: Array.isArray(msg!.mediaLocalRoots)
|
|
180
|
+
? (msg!.mediaLocalRoots as readonly string[])
|
|
181
|
+
: undefined,
|
|
182
|
+
downloadMedia: msg!.downloadMedia as boolean | undefined,
|
|
183
|
+
});
|
|
184
|
+
const frame = args.buildFileTransferOutboundFrame!({
|
|
185
|
+
entry,
|
|
186
|
+
msg: msg!,
|
|
187
|
+
media,
|
|
188
|
+
mediaUrl: guard.mediaUrl,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
args.gatewayBroadcastToConnIds(
|
|
192
|
+
args.pushEvent,
|
|
193
|
+
buildFileTransferBroadcastPayload({ frame, messageId: entry.messageId }),
|
|
194
|
+
new Set(guard.connIds),
|
|
195
|
+
);
|
|
196
|
+
args.logOutboxRouteSelect(
|
|
197
|
+
buildFileTransferRouteSelectArgs({
|
|
198
|
+
entry,
|
|
199
|
+
connIds: guard.connIds,
|
|
200
|
+
routeReason: guard.routeReason,
|
|
201
|
+
recentInboundReachable: guard.recentInboundReachable,
|
|
202
|
+
owner: args.owner,
|
|
203
|
+
event: args.pushEvent,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
args.recordOutboxPushSuccess(
|
|
207
|
+
buildFileTransferPushSuccessArgs({
|
|
208
|
+
entry,
|
|
209
|
+
connIds: guard.connIds,
|
|
210
|
+
owner: args.owner,
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
args.logOutboxPushOkSummary(entry.messageId);
|
|
214
|
+
// Sync entry type from the resolved frame
|
|
215
|
+
const frameMsg = frame?.message as Record<string, unknown> | undefined;
|
|
216
|
+
if (frameMsg?.type !== undefined) {
|
|
217
|
+
(msg! as Record<string, unknown>).type = frameMsg.type;
|
|
218
|
+
}
|
|
219
|
+
args.logOutboundSummary(entry);
|
|
220
|
+
args.logOutboxPushOk(
|
|
221
|
+
buildFileTransferPushOkArgs({
|
|
222
|
+
entry,
|
|
223
|
+
connIds: guard.connIds,
|
|
224
|
+
recentInboundReachable: guard.recentInboundReachable,
|
|
225
|
+
event: args.pushEvent,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
args.handleFileTransferPushFailure({ entry, error });
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Text ──
|
|
236
|
+
const textGuard = resolveTextPushGuard({
|
|
237
|
+
gatewayContext: args.gatewayContext,
|
|
238
|
+
entry,
|
|
239
|
+
routeSelection: selection,
|
|
240
|
+
});
|
|
241
|
+
if (!textGuard.ok) {
|
|
242
|
+
args.recordOutboxPrePushFailure({
|
|
243
|
+
entry,
|
|
244
|
+
lastError:
|
|
245
|
+
textGuard.reason === 'no-gateway-context'
|
|
246
|
+
? 'gateway context unavailable'
|
|
247
|
+
: 'no active bncr client',
|
|
248
|
+
persist: true,
|
|
249
|
+
});
|
|
250
|
+
args.logOutboxPushSkip({
|
|
251
|
+
messageId: entry.messageId,
|
|
252
|
+
accountId: entry.accountId,
|
|
253
|
+
reason: textGuard.reason,
|
|
254
|
+
recentInboundReachable:
|
|
255
|
+
textGuard.reason === 'no-active-connection' ? textGuard.recentInboundReachable : undefined,
|
|
256
|
+
routeReason: selection.routeReason,
|
|
257
|
+
connIds: selection.connIds,
|
|
258
|
+
ownerConnId: selection.ownerConnId,
|
|
259
|
+
ownerClientId: args.owner?.clientId,
|
|
260
|
+
});
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Text success path (inline) ──
|
|
265
|
+
try {
|
|
266
|
+
args.gatewayBroadcastToConnIds(
|
|
267
|
+
args.pushEvent,
|
|
268
|
+
buildTextPushBroadcastPayload({
|
|
269
|
+
payload: entry.payload,
|
|
270
|
+
messageId: entry.messageId,
|
|
271
|
+
}),
|
|
272
|
+
new Set(textGuard.connIds),
|
|
273
|
+
);
|
|
274
|
+
args.logOutboxRouteSelect(
|
|
275
|
+
buildTextPushRouteSelectArgs({
|
|
276
|
+
entry,
|
|
277
|
+
connIds: textGuard.connIds,
|
|
278
|
+
routeReason: textGuard.routeReason,
|
|
279
|
+
recentInboundReachable: textGuard.recentInboundReachable,
|
|
280
|
+
owner: args.owner,
|
|
281
|
+
event: args.pushEvent,
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
args.recordOutboxPushSuccess(
|
|
285
|
+
buildTextPushSuccessArgs({
|
|
286
|
+
entry,
|
|
287
|
+
connIds: textGuard.connIds,
|
|
288
|
+
ownerConnId: textGuard.ownerConnId,
|
|
289
|
+
ownerClientId: textGuard.ownerConnId ? args.owner?.clientId : undefined,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
args.logOutboxPushOkSummary(entry.messageId);
|
|
293
|
+
args.logOutboundSummary(entry);
|
|
294
|
+
args.logOutboxPushOk(
|
|
295
|
+
buildTextPushOkArgs({
|
|
296
|
+
entry,
|
|
297
|
+
connIds: textGuard.connIds,
|
|
298
|
+
recentInboundReachable: textGuard.recentInboundReachable,
|
|
299
|
+
event: args.pushEvent,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
args.handleTextPushFailure({ entry, error });
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -36,7 +36,7 @@ export function buildSceneKey(parsed: ParsedInbound): string {
|
|
|
36
36
|
: `${parsed.platform}:${parsed.userId}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
function buildSceneRecord(args: {
|
|
40
40
|
parsed: ParsedInbound;
|
|
41
41
|
sceneKey: string;
|
|
42
42
|
kind: BncrSceneKind;
|
|
@@ -6,15 +6,10 @@ import {
|
|
|
6
6
|
routeKey,
|
|
7
7
|
} from '../core/targets.ts';
|
|
8
8
|
import type { BncrRoute } from '../core/types.ts';
|
|
9
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
9
10
|
import { getOpenClawRuntimeConfigOrDefault } from '../openclaw/config-runtime.ts';
|
|
10
11
|
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
11
12
|
|
|
12
|
-
function asString(v: unknown, fallback = ''): string {
|
|
13
|
-
if (typeof v === 'string') return v;
|
|
14
|
-
if (v == null) return fallback;
|
|
15
|
-
return String(v);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
export function createBncrTargetRuntime(runtime: {
|
|
19
14
|
api: Parameters<typeof getOpenClawRuntimeConfigOrDefault>[0];
|
|
20
15
|
channelId: string;
|
|
@@ -5,10 +5,10 @@ export type LogDedupeStateEntry = {
|
|
|
5
5
|
|
|
6
6
|
export type LogDedupeState = Map<string, LogDedupeStateEntry>;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const LOG_DEDUPE_STATE_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const LOG_DEDUPE_STATE_MAX_ENTRIES = 1_000;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
function pruneLogDedupeState(
|
|
12
12
|
state: LogDedupeState,
|
|
13
13
|
currentTime: number,
|
|
14
14
|
options?: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BncrAckObservability, BncrAckStrategy } from '../core/types.ts';
|
|
2
|
+
import { asString, finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
|
|
3
4
|
type ComputeBncrRecommendedAckTimeoutArgs = {
|
|
4
5
|
lateAckOkCount: number;
|
|
@@ -41,9 +42,7 @@ function isAdaptiveAckRecovered(args: {
|
|
|
41
42
|
);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
args: ComputeBncrRecommendedAckTimeoutReasonArgs,
|
|
46
|
-
) {
|
|
45
|
+
function computeBncrRecommendedAckTimeoutReason(args: ComputeBncrRecommendedAckTimeoutReasonArgs) {
|
|
47
46
|
if (args.recentAckTimeoutCount <= 0) return 'no-timeout-evidence';
|
|
48
47
|
if (args.lateAckOkCount <= 0) return 'no-late-ack-evidence';
|
|
49
48
|
if (typeof args.lastLateAckPushLatencyMs !== 'number') return 'missing-latency';
|
|
@@ -53,7 +52,7 @@ export function computeBncrRecommendedAckTimeoutReason(
|
|
|
53
52
|
return 'late-ack-observed';
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
function computeBncrRecommendedAckTimeoutMs(args: ComputeBncrRecommendedAckTimeoutArgs) {
|
|
57
56
|
if (
|
|
58
57
|
args.lateAckOkCount <= 0 ||
|
|
59
58
|
args.recentAckTimeoutCount <= 0 ||
|
|
@@ -76,14 +75,6 @@ export function resolveBncrRuntimeAckTimeoutDecision(args: ComputeBncrRecommende
|
|
|
76
75
|
return { timeoutMs, reason };
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
function finiteNumberOr(value: unknown, fallback: number) {
|
|
80
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function asString(value: unknown, fallback = '') {
|
|
84
|
-
return typeof value === 'string' ? value : fallback;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
78
|
export function buildBncrRuntimeAckStrategy(args: {
|
|
88
79
|
ackObservability: BncrAckObservability;
|
|
89
80
|
defaultAckTimeoutMs: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OutboxEntry } from '../core/types.ts';
|
|
2
|
+
import { finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
import type {
|
|
3
4
|
PushFailureDecision,
|
|
4
5
|
RetryRerouteDecision,
|
|
@@ -83,10 +84,6 @@ export function buildBncrOutboxPushSuccessEntryPatch(args: {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
87
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
87
|
export function buildBncrAckOkTelemetryPatch(args: {
|
|
91
88
|
entry: OutboxEntry;
|
|
92
89
|
ackAt: number;
|
|
@@ -34,7 +34,7 @@ function getCounter(map: Map<string, number>, accountId: string): number {
|
|
|
34
34
|
return map.get(normalizeAccountId(accountId)) || 0;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function buildRuntimeQueueSnapshot(args: {
|
|
38
38
|
accountId: string;
|
|
39
39
|
outboxEntries: Iterable<OutboxEntry>;
|
|
40
40
|
deadLetterEntries: Iterable<OutboxEntry>;
|
|
@@ -59,7 +59,7 @@ export function buildRuntimeQueueSnapshot(args: {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
function buildRuntimeEventCounters(args: {
|
|
63
63
|
accountId: string;
|
|
64
64
|
connectEventsByAccount: Map<string, number>;
|
|
65
65
|
inboundEventsByAccount: Map<string, number>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
2
|
import type { BncrDiagnosticsSummary } from '../core/types.ts';
|
|
3
|
+
import { finiteNumberOr, nonNegativeFiniteNumberOr } from '../core/value-sanitize.ts';
|
|
3
4
|
|
|
4
5
|
type StatusRuntimeMeta = {
|
|
5
6
|
pending?: number;
|
|
@@ -48,7 +49,7 @@ export type ChannelAccountWorkerHandle = {
|
|
|
48
49
|
healthLogState?: HealthStatusLogState;
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
const HEALTH_STATUS_STABLE_WINDOW_MS = 10_000;
|
|
52
53
|
|
|
53
54
|
export type HealthStatusLogState = {
|
|
54
55
|
emittedSig: string | null;
|
|
@@ -60,15 +61,6 @@ export function createHealthStatusLogState(): HealthStatusLogState {
|
|
|
60
61
|
return { emittedSig: null, pendingSig: null, pendingSince: 0 };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
64
|
-
const n = Number(value);
|
|
65
|
-
return Number.isFinite(n) ? n : fallback;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
69
|
-
return Math.max(0, finiteNumberOr(value, fallback));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
64
|
export function updateHealthStatusLogState(args: {
|
|
73
65
|
state: HealthStatusLogState;
|
|
74
66
|
sig: string;
|
|
@@ -120,11 +112,7 @@ type StatusWorkerRuntime = {
|
|
|
120
112
|
hooks: StatusWorkerHooks;
|
|
121
113
|
};
|
|
122
114
|
|
|
123
|
-
|
|
124
|
-
runtime: StatusWorkerRuntime,
|
|
125
|
-
accountId: string,
|
|
126
|
-
reason: string,
|
|
127
|
-
) {
|
|
115
|
+
function clearBncrStatusWorker(runtime: StatusWorkerRuntime, accountId: string, reason: string) {
|
|
128
116
|
const worker = runtime.workers.get(accountId);
|
|
129
117
|
if (!worker) return false;
|
|
130
118
|
worker.finish(reason);
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
-
|
|
3
|
-
export function prepareTextPushRouteSelection(args: {
|
|
4
|
-
entry: OutboxEntry;
|
|
5
|
-
owner: BncrConnection | null;
|
|
6
|
-
resolvePushConnIds: (accountId: string) => Iterable<string>;
|
|
7
|
-
resolveRecentInboundConnIds: (accountId: string) => Iterable<string>;
|
|
8
|
-
hasRecentInboundReachability: (accountId: string) => boolean;
|
|
9
|
-
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
10
|
-
selectOutboxRouteCandidates: (args: {
|
|
11
|
-
routeCandidates: Iterable<string>;
|
|
12
|
-
attemptedConnIds: string[];
|
|
13
|
-
recentInboundConnIds: Iterable<string>;
|
|
14
|
-
ownerConnId?: string;
|
|
15
|
-
recentInboundReachable: boolean;
|
|
16
|
-
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
17
|
-
}) => {
|
|
18
|
-
connIds: Iterable<string>;
|
|
19
|
-
recentInboundReachable: boolean;
|
|
20
|
-
routeReason: string;
|
|
21
|
-
ownerConnId?: string;
|
|
22
|
-
};
|
|
23
|
-
}) {
|
|
24
|
-
const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
|
|
25
|
-
? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
26
|
-
: [];
|
|
27
|
-
|
|
28
|
-
return args.selectOutboxRouteCandidates({
|
|
29
|
-
routeCandidates: args.resolvePushConnIds(args.entry.accountId),
|
|
30
|
-
attemptedConnIds,
|
|
31
|
-
recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
|
|
32
|
-
ownerConnId: args.owner?.connId,
|
|
33
|
-
recentInboundReachable: args.hasRecentInboundReachability(args.entry.accountId),
|
|
34
|
-
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
type MinimalBncrSendInput = {
|
|
4
|
-
channel?: string;
|
|
5
|
-
action?: string;
|
|
6
|
-
idempotencyKey?: string;
|
|
7
|
-
accountId?: string;
|
|
8
|
-
to?: string;
|
|
9
|
-
message?: string;
|
|
10
|
-
caption?: string;
|
|
11
|
-
path?: string;
|
|
12
|
-
media?: string;
|
|
13
|
-
filePath?: string;
|
|
14
|
-
mediaUrl?: string;
|
|
15
|
-
mediaUrls?: string[];
|
|
16
|
-
asVoice?: boolean;
|
|
17
|
-
audioAsVoice?: boolean;
|
|
18
|
-
params?: Record<string, unknown>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type BuiltBncrMessageAction = {
|
|
22
|
-
channel: string;
|
|
23
|
-
action: string;
|
|
24
|
-
idempotencyKey: string;
|
|
25
|
-
accountId?: string;
|
|
26
|
-
params: Record<string, unknown>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function asString(v: unknown, fallback = ''): string {
|
|
30
|
-
if (typeof v === 'string') return v;
|
|
31
|
-
if (v == null) return fallback;
|
|
32
|
-
return String(v);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pickFirstString(...values: unknown[]): string | undefined {
|
|
40
|
-
for (const value of values) {
|
|
41
|
-
if (typeof value !== 'string') continue;
|
|
42
|
-
if (value.length === 0) return '';
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function pickFirstBoolean(...values: unknown[]): boolean | undefined {
|
|
49
|
-
for (const value of values) {
|
|
50
|
-
if (typeof value === 'boolean') return value;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMessageAction {
|
|
56
|
-
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
57
|
-
|
|
58
|
-
const channel = asString(input.channel || 'bncr').trim() || 'bncr';
|
|
59
|
-
const action = asString(input.action || 'send').trim() || 'send';
|
|
60
|
-
const idempotencyKey = asString(input.idempotencyKey || '').trim() || `bncr-${randomUUID()}`;
|
|
61
|
-
const accountId =
|
|
62
|
-
asString(pickFirstString(paramsObj.accountId, input.accountId) || '').trim() || undefined;
|
|
63
|
-
|
|
64
|
-
const to = asString(pickFirstString(paramsObj.to, input.to) || '').trim();
|
|
65
|
-
if (!to) throw new Error('bncr send requires to');
|
|
66
|
-
|
|
67
|
-
const mediaPath = pickFirstString(
|
|
68
|
-
paramsObj.media,
|
|
69
|
-
paramsObj.path,
|
|
70
|
-
paramsObj.filePath,
|
|
71
|
-
paramsObj.mediaUrl,
|
|
72
|
-
input.media,
|
|
73
|
-
input.path,
|
|
74
|
-
input.filePath,
|
|
75
|
-
input.mediaUrl,
|
|
76
|
-
);
|
|
77
|
-
const rawMediaUrls = Array.isArray(paramsObj.mediaUrls)
|
|
78
|
-
? paramsObj.mediaUrls
|
|
79
|
-
: Array.isArray(input.mediaUrls)
|
|
80
|
-
? input.mediaUrls
|
|
81
|
-
: undefined;
|
|
82
|
-
const mediaUrls = rawMediaUrls?.map((value) => asString(value || '').trim()).filter(Boolean);
|
|
83
|
-
|
|
84
|
-
const message = pickFirstString(paramsObj.message, input.message) ?? '';
|
|
85
|
-
const explicitCaption = pickFirstString(paramsObj.caption, input.caption) ?? '';
|
|
86
|
-
const asVoice = pickFirstBoolean(paramsObj.asVoice, input.asVoice);
|
|
87
|
-
const audioAsVoice = pickFirstBoolean(paramsObj.audioAsVoice, input.audioAsVoice);
|
|
88
|
-
|
|
89
|
-
if ((asVoice === true || audioAsVoice === true) && !mediaPath && !mediaUrls?.length) {
|
|
90
|
-
throw new Error('bncr voice send requires media path');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const normalizedParams: Record<string, unknown> = {
|
|
94
|
-
...paramsObj,
|
|
95
|
-
to,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (mediaPath) {
|
|
99
|
-
normalizedParams.path = mediaPath;
|
|
100
|
-
const finalCaption = explicitCaption || message;
|
|
101
|
-
if (finalCaption) normalizedParams.caption = finalCaption;
|
|
102
|
-
delete normalizedParams.message;
|
|
103
|
-
if (mediaUrls?.length) normalizedParams.mediaUrls = mediaUrls;
|
|
104
|
-
} else {
|
|
105
|
-
if (mediaUrls?.length) {
|
|
106
|
-
normalizedParams.mediaUrls = mediaUrls;
|
|
107
|
-
const finalCaption = explicitCaption || message;
|
|
108
|
-
if (finalCaption) normalizedParams.caption = finalCaption;
|
|
109
|
-
delete normalizedParams.message;
|
|
110
|
-
delete normalizedParams.path;
|
|
111
|
-
} else {
|
|
112
|
-
const finalMessage = message || explicitCaption;
|
|
113
|
-
if (!finalMessage.trim()) throw new Error('bncr send requires message or media');
|
|
114
|
-
normalizedParams.message = finalMessage;
|
|
115
|
-
delete normalizedParams.caption;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (asVoice === true) normalizedParams.asVoice = true;
|
|
120
|
-
if (audioAsVoice === true) normalizedParams.audioAsVoice = true;
|
|
121
|
-
if (accountId) normalizedParams.accountId = accountId;
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
channel,
|
|
125
|
-
action,
|
|
126
|
-
idempotencyKey,
|
|
127
|
-
...(accountId ? { accountId } : {}),
|
|
128
|
-
params: normalizedParams,
|
|
129
|
-
};
|
|
130
|
-
}
|