@xmoxmo/bncr 0.2.4 → 0.2.5
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 +67 -4
- package/package.json +1 -1
- package/src/channel.ts +2274 -1548
- package/src/core/connection-capability.ts +70 -0
- package/src/core/connection-reachability.ts +141 -0
- package/src/core/diagnostics.ts +49 -0
- package/src/core/downlink-health.ts +56 -0
- package/src/core/extended-diagnostics.ts +65 -0
- package/src/core/lease-state.ts +94 -0
- package/src/core/outbox-enqueue.ts +22 -0
- package/src/core/outbox-entry-builders.ts +91 -0
- package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
- package/src/core/outbox-file-transfer-failure.ts +25 -0
- package/src/core/outbox-file-transfer-guards.ts +66 -0
- package/src/core/outbox-file-transfer-prep.ts +31 -0
- package/src/core/outbox-file-transfer-success.ts +34 -0
- package/src/core/outbox-push-args.ts +67 -0
- package/src/core/outbox-queue.ts +69 -0
- package/src/core/outbox-summary.ts +14 -0
- package/src/core/outbox-text-push-failure.ts +10 -0
- package/src/core/outbox-text-push-guards.ts +51 -0
- package/src/core/outbox-text-push-prep.ts +36 -0
- package/src/core/outbox-text-push-success.ts +62 -0
- package/src/core/register-trace.ts +110 -0
- package/src/core/status.ts +52 -0
- package/src/messaging/inbound/dispatch.ts +86 -48
- package/src/messaging/outbound/diagnostics.ts +246 -0
- package/src/messaging/outbound/media-dedupe.ts +51 -0
- package/src/messaging/outbound/queue-selectors.ts +186 -0
- package/src/messaging/outbound/reasons.ts +48 -0
- package/src/messaging/outbound/reply-enqueue.ts +329 -0
- package/src/messaging/outbound/retry-policy.ts +133 -0
- package/src/messaging/outbound/session-route.ts +34 -5
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export type FileTransferGuardResult =
|
|
4
|
+
| { ok: false; reason: 'no-gateway-context'; lastError: 'gateway context unavailable' }
|
|
5
|
+
| {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'no-active-connection';
|
|
8
|
+
lastError: 'no active bncr client for file chunk transfer';
|
|
9
|
+
recentInboundReachable: boolean;
|
|
10
|
+
}
|
|
11
|
+
| { ok: false; reason: 'media-url-missing'; lastError: 'file transfer mediaUrl missing' }
|
|
12
|
+
| {
|
|
13
|
+
ok: true;
|
|
14
|
+
owner: BncrConnection | null;
|
|
15
|
+
connIds: Set<string>;
|
|
16
|
+
recentInboundReachable: boolean;
|
|
17
|
+
routeReason: string;
|
|
18
|
+
mediaUrl: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function resolveFileTransferGuard(args: {
|
|
22
|
+
gatewayContext: unknown;
|
|
23
|
+
entry: OutboxEntry;
|
|
24
|
+
owner: BncrConnection | null;
|
|
25
|
+
routeSelection: {
|
|
26
|
+
connIds: Iterable<string>;
|
|
27
|
+
recentInboundReachable: boolean;
|
|
28
|
+
routeReason: string;
|
|
29
|
+
};
|
|
30
|
+
mediaUrl: string;
|
|
31
|
+
}): FileTransferGuardResult {
|
|
32
|
+
if (!args.gatewayContext) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'no-gateway-context',
|
|
36
|
+
lastError: 'gateway context unavailable',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const connIds = new Set(args.routeSelection.connIds);
|
|
41
|
+
if (!connIds.size) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
reason: 'no-active-connection',
|
|
45
|
+
lastError: 'no active bncr client for file chunk transfer',
|
|
46
|
+
recentInboundReachable: args.routeSelection.recentInboundReachable,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!args.mediaUrl) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'media-url-missing',
|
|
54
|
+
lastError: 'file transfer mediaUrl missing',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
owner: args.owner,
|
|
61
|
+
connIds,
|
|
62
|
+
recentInboundReachable: args.routeSelection.recentInboundReachable,
|
|
63
|
+
routeReason: args.routeSelection.routeReason,
|
|
64
|
+
mediaUrl: args.mediaUrl,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function prepareFileTransferRouteSelection(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
|
+
selectOutboxFileTransferRouteCandidates: (args: {
|
|
11
|
+
routeCandidates: Iterable<string>;
|
|
12
|
+
attemptedConnIds: string[];
|
|
13
|
+
recentInboundConnIds: Iterable<string>;
|
|
14
|
+
ownerConnId?: string;
|
|
15
|
+
recentInboundReachable: boolean;
|
|
16
|
+
isRevalidatedAttemptedConn: (connId: string) => boolean;
|
|
17
|
+
}) => { connIds: Iterable<string>; recentInboundReachable: boolean; routeReason: string };
|
|
18
|
+
}) {
|
|
19
|
+
const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
|
|
20
|
+
? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
|
|
21
|
+
: [];
|
|
22
|
+
|
|
23
|
+
return args.selectOutboxFileTransferRouteCandidates({
|
|
24
|
+
routeCandidates: args.resolvePushConnIds(args.entry.accountId),
|
|
25
|
+
attemptedConnIds,
|
|
26
|
+
recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
|
|
27
|
+
ownerConnId: args.owner?.connId,
|
|
28
|
+
recentInboundReachable: args.hasRecentInboundReachability(args.entry.accountId),
|
|
29
|
+
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
+
import {
|
|
3
|
+
buildPushBroadcastPayload,
|
|
4
|
+
buildPushRouteSelectArgs,
|
|
5
|
+
} from './outbox-push-args.ts';
|
|
6
|
+
|
|
7
|
+
export function buildFileTransferBroadcastPayload(args: {
|
|
8
|
+
frame: Record<string, unknown>;
|
|
9
|
+
messageId: string;
|
|
10
|
+
}) {
|
|
11
|
+
return buildPushBroadcastPayload({
|
|
12
|
+
payload: args.frame,
|
|
13
|
+
messageId: args.messageId,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildFileTransferRouteSelectArgs(args: {
|
|
18
|
+
entry: OutboxEntry;
|
|
19
|
+
connIds: Iterable<string>;
|
|
20
|
+
routeReason: string;
|
|
21
|
+
recentInboundReachable: boolean;
|
|
22
|
+
owner: BncrConnection | null;
|
|
23
|
+
event: string;
|
|
24
|
+
}) {
|
|
25
|
+
return buildPushRouteSelectArgs({
|
|
26
|
+
entry: args.entry,
|
|
27
|
+
connIds: args.connIds,
|
|
28
|
+
routeReason: args.routeReason,
|
|
29
|
+
recentInboundReachable: args.recentInboundReachable,
|
|
30
|
+
owner: args.owner,
|
|
31
|
+
event: args.event,
|
|
32
|
+
kind: 'file-transfer',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function buildPushBroadcastPayload(args: {
|
|
4
|
+
payload: Record<string, unknown>;
|
|
5
|
+
messageId: string;
|
|
6
|
+
}) {
|
|
7
|
+
return {
|
|
8
|
+
...args.payload,
|
|
9
|
+
idempotencyKey: args.messageId,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildPushRouteSelectArgs(args: {
|
|
14
|
+
entry: OutboxEntry;
|
|
15
|
+
connIds: Iterable<string>;
|
|
16
|
+
routeReason: string;
|
|
17
|
+
recentInboundReachable: boolean;
|
|
18
|
+
owner: BncrConnection | null;
|
|
19
|
+
event: string;
|
|
20
|
+
kind?: 'file-transfer';
|
|
21
|
+
}) {
|
|
22
|
+
return {
|
|
23
|
+
messageId: args.entry.messageId,
|
|
24
|
+
accountId: args.entry.accountId,
|
|
25
|
+
kind: args.kind,
|
|
26
|
+
routeReason: args.routeReason,
|
|
27
|
+
connIds: args.connIds,
|
|
28
|
+
ownerConnId: args.owner?.connId || '',
|
|
29
|
+
ownerClientId: args.owner?.clientId || '',
|
|
30
|
+
recentInboundReachable: args.recentInboundReachable,
|
|
31
|
+
event: args.event,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildPushOkArgs(args: {
|
|
36
|
+
entry: OutboxEntry;
|
|
37
|
+
connIds: Iterable<string>;
|
|
38
|
+
recentInboundReachable: boolean;
|
|
39
|
+
event: string;
|
|
40
|
+
kind?: 'file-transfer';
|
|
41
|
+
}) {
|
|
42
|
+
return {
|
|
43
|
+
messageId: args.entry.messageId,
|
|
44
|
+
accountId: args.entry.accountId,
|
|
45
|
+
kind: args.kind,
|
|
46
|
+
connIds: args.connIds,
|
|
47
|
+
ownerConnId: args.entry.lastPushConnId || '',
|
|
48
|
+
ownerClientId: args.entry.lastPushClientId || '',
|
|
49
|
+
recentInboundReachable: args.recentInboundReachable,
|
|
50
|
+
event: args.event,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildPushFailureArgs(args: {
|
|
55
|
+
entry: OutboxEntry;
|
|
56
|
+
retryable?: boolean;
|
|
57
|
+
kind?: 'file-transfer';
|
|
58
|
+
}) {
|
|
59
|
+
return {
|
|
60
|
+
messageId: args.entry.messageId,
|
|
61
|
+
accountId: args.entry.accountId,
|
|
62
|
+
retryCount: args.entry.retryCount,
|
|
63
|
+
kind: args.kind,
|
|
64
|
+
retryable: args.retryable,
|
|
65
|
+
lastError: args.entry.lastError,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function buildDeadLetterEntry(entry: OutboxEntry, reason: string): OutboxEntry {
|
|
4
|
+
return {
|
|
5
|
+
...entry,
|
|
6
|
+
lastError: reason,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function appendDeadLetter(args: {
|
|
11
|
+
deadLetter: OutboxEntry[];
|
|
12
|
+
entry: OutboxEntry;
|
|
13
|
+
maxEntries: number;
|
|
14
|
+
}): OutboxEntry[] {
|
|
15
|
+
const next = [...args.deadLetter, args.entry];
|
|
16
|
+
if (next.length <= args.maxEntries) return next;
|
|
17
|
+
return next.slice(-args.maxEntries);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function collectDueOutboxEntries(args: {
|
|
21
|
+
outbox: Iterable<OutboxEntry>;
|
|
22
|
+
accountId: string;
|
|
23
|
+
now: number;
|
|
24
|
+
maxBatch: number;
|
|
25
|
+
maxRetry: number;
|
|
26
|
+
backoffMs: (retryCount: number) => number;
|
|
27
|
+
}): {
|
|
28
|
+
duePayloads: Array<Record<string, unknown>>;
|
|
29
|
+
updatedEntries: OutboxEntry[];
|
|
30
|
+
deadLetterEntries: OutboxEntry[];
|
|
31
|
+
} {
|
|
32
|
+
const duePayloads: Array<Record<string, unknown>> = [];
|
|
33
|
+
const updatedEntries: OutboxEntry[] = [];
|
|
34
|
+
const deadLetterEntries: OutboxEntry[] = [];
|
|
35
|
+
|
|
36
|
+
for (const originalEntry of args.outbox) {
|
|
37
|
+
if (originalEntry.accountId !== args.accountId) continue;
|
|
38
|
+
if (originalEntry.nextAttemptAt > args.now) continue;
|
|
39
|
+
|
|
40
|
+
const nextAttempt = originalEntry.retryCount + 1;
|
|
41
|
+
if (nextAttempt > args.maxRetry) {
|
|
42
|
+
deadLetterEntries.push(buildDeadLetterEntry(originalEntry, 'retry-limit'));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const updatedEntry: OutboxEntry = {
|
|
47
|
+
...originalEntry,
|
|
48
|
+
retryCount: nextAttempt,
|
|
49
|
+
lastAttemptAt: args.now,
|
|
50
|
+
nextAttemptAt: args.now + args.backoffMs(nextAttempt),
|
|
51
|
+
};
|
|
52
|
+
updatedEntries.push(updatedEntry);
|
|
53
|
+
duePayloads.push({
|
|
54
|
+
...updatedEntry.payload,
|
|
55
|
+
_meta: {
|
|
56
|
+
retryCount: updatedEntry.retryCount,
|
|
57
|
+
nextAttemptAt: updatedEntry.nextAttemptAt,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (duePayloads.length >= args.maxBatch) break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
duePayloads,
|
|
66
|
+
updatedEntries,
|
|
67
|
+
deadLetterEntries,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export function summarizeOutboxEntry(args: {
|
|
4
|
+
entry: OutboxEntry;
|
|
5
|
+
asString: (value: unknown) => string;
|
|
6
|
+
formatDisplayScope: (route: OutboxEntry['route']) => string;
|
|
7
|
+
summarizeTextPreview: (raw: string, limit?: number) => string;
|
|
8
|
+
}) {
|
|
9
|
+
const msg = (args.entry.payload as any)?.message || {};
|
|
10
|
+
const type = args.asString(msg.type || (args.entry.payload as any)?.type || 'unknown');
|
|
11
|
+
const text = args.asString(msg.msg || '');
|
|
12
|
+
const preview = args.summarizeTextPreview(text);
|
|
13
|
+
return [type, args.formatDisplayScope(args.entry.route), preview].join('|');
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { OutboxEntry } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export type TextPushGuardResult =
|
|
4
|
+
| { ok: false; reason: 'no-gateway-context' }
|
|
5
|
+
| {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'no-active-connection';
|
|
8
|
+
recentInboundReachable: boolean;
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
ok: true;
|
|
12
|
+
connIds: Set<string>;
|
|
13
|
+
recentInboundReachable: boolean;
|
|
14
|
+
routeReason: string;
|
|
15
|
+
ownerConnId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function resolveTextPushGuard(args: {
|
|
19
|
+
gatewayContext: unknown;
|
|
20
|
+
entry: OutboxEntry;
|
|
21
|
+
routeSelection: {
|
|
22
|
+
connIds: Iterable<string>;
|
|
23
|
+
recentInboundReachable: boolean;
|
|
24
|
+
routeReason: string;
|
|
25
|
+
ownerConnId?: string;
|
|
26
|
+
};
|
|
27
|
+
}): TextPushGuardResult {
|
|
28
|
+
if (!args.gatewayContext) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: 'no-gateway-context',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const connIds = new Set(args.routeSelection.connIds);
|
|
36
|
+
if (!connIds.size) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: 'no-active-connection',
|
|
40
|
+
recentInboundReachable: args.routeSelection.recentInboundReachable,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
connIds,
|
|
47
|
+
recentInboundReachable: args.routeSelection.recentInboundReachable,
|
|
48
|
+
routeReason: args.routeSelection.routeReason,
|
|
49
|
+
ownerConnId: args.routeSelection.ownerConnId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from './types.ts';
|
|
2
|
+
import {
|
|
3
|
+
buildPushBroadcastPayload,
|
|
4
|
+
buildPushOkArgs,
|
|
5
|
+
buildPushRouteSelectArgs,
|
|
6
|
+
} from './outbox-push-args.ts';
|
|
7
|
+
|
|
8
|
+
export function buildTextPushBroadcastPayload(args: {
|
|
9
|
+
payload: Record<string, unknown>;
|
|
10
|
+
messageId: string;
|
|
11
|
+
}) {
|
|
12
|
+
return buildPushBroadcastPayload({
|
|
13
|
+
payload: args.payload,
|
|
14
|
+
messageId: args.messageId,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildTextPushRouteSelectArgs(args: {
|
|
19
|
+
entry: OutboxEntry;
|
|
20
|
+
connIds: Iterable<string>;
|
|
21
|
+
routeReason: string;
|
|
22
|
+
recentInboundReachable: boolean;
|
|
23
|
+
owner: BncrConnection | null;
|
|
24
|
+
event: string;
|
|
25
|
+
}) {
|
|
26
|
+
return buildPushRouteSelectArgs({
|
|
27
|
+
entry: args.entry,
|
|
28
|
+
connIds: args.connIds,
|
|
29
|
+
routeReason: args.routeReason,
|
|
30
|
+
recentInboundReachable: args.recentInboundReachable,
|
|
31
|
+
owner: args.owner,
|
|
32
|
+
event: args.event,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildTextPushSuccessArgs(args: {
|
|
37
|
+
entry: OutboxEntry;
|
|
38
|
+
connIds: Iterable<string>;
|
|
39
|
+
ownerConnId?: string;
|
|
40
|
+
ownerClientId?: string;
|
|
41
|
+
}) {
|
|
42
|
+
return {
|
|
43
|
+
entry: args.entry,
|
|
44
|
+
connIds: args.connIds,
|
|
45
|
+
ownerConnId: args.ownerConnId,
|
|
46
|
+
ownerClientId: args.ownerClientId,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildTextPushOkArgs(args: {
|
|
51
|
+
entry: OutboxEntry;
|
|
52
|
+
connIds: Iterable<string>;
|
|
53
|
+
recentInboundReachable: boolean;
|
|
54
|
+
event: string;
|
|
55
|
+
}) {
|
|
56
|
+
return buildPushOkArgs({
|
|
57
|
+
entry: args.entry,
|
|
58
|
+
connIds: args.connIds,
|
|
59
|
+
recentInboundReachable: args.recentInboundReachable,
|
|
60
|
+
event: args.event,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const DEFAULT_REGISTER_WARMUP_WINDOW_MS = 30_000;
|
|
2
|
+
|
|
3
|
+
export type RegisterTraceEntry = {
|
|
4
|
+
ts: number;
|
|
5
|
+
bridgeId: string;
|
|
6
|
+
gatewayPid: number;
|
|
7
|
+
registerCount: number;
|
|
8
|
+
apiGeneration: number;
|
|
9
|
+
apiRebound: boolean;
|
|
10
|
+
apiInstanceId: string | null;
|
|
11
|
+
registryFingerprint: string | null;
|
|
12
|
+
source: string | null;
|
|
13
|
+
pluginVersion: string | null;
|
|
14
|
+
stack: string;
|
|
15
|
+
stackBucket: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RegisterTraceSummary = {
|
|
19
|
+
startupWindowMs: number;
|
|
20
|
+
traceWindowSize: number;
|
|
21
|
+
sourceBuckets: Record<string, number>;
|
|
22
|
+
dominantBucket: string | null;
|
|
23
|
+
warmupRegisterCount: number;
|
|
24
|
+
postWarmupRegisterCount: number;
|
|
25
|
+
unexpectedRegisterAfterWarmup: boolean;
|
|
26
|
+
lastUnexpectedRegisterAt: number | null;
|
|
27
|
+
likelyRuntimeRegistryDrift: boolean;
|
|
28
|
+
likelyStartupFanoutOnly: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function classifyRegisterTrace(stack: string) {
|
|
32
|
+
if (
|
|
33
|
+
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
34
|
+
stack.includes('resolveRuntimeWebTools') ||
|
|
35
|
+
stack.includes('resolvePluginWebSearchProviders')
|
|
36
|
+
) {
|
|
37
|
+
return 'runtime/webtools';
|
|
38
|
+
}
|
|
39
|
+
if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
|
|
40
|
+
return 'gateway/startup';
|
|
41
|
+
}
|
|
42
|
+
if (stack.includes('resolvePluginImplicitProviders')) {
|
|
43
|
+
return 'provider/discovery/implicit';
|
|
44
|
+
}
|
|
45
|
+
if (stack.includes('resolvePluginDiscoveryProviders')) {
|
|
46
|
+
return 'provider/discovery/discovery';
|
|
47
|
+
}
|
|
48
|
+
if (stack.includes('resolvePluginProviders')) {
|
|
49
|
+
return 'provider/discovery/providers';
|
|
50
|
+
}
|
|
51
|
+
return 'other';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
55
|
+
let winner: string | null = null;
|
|
56
|
+
let winnerCount = -1;
|
|
57
|
+
for (const [bucket, count] of Object.entries(sourceBuckets)) {
|
|
58
|
+
if (count > winnerCount) {
|
|
59
|
+
winner = bucket;
|
|
60
|
+
winnerCount = count;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return winner;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildRegisterTraceSummary(args: {
|
|
67
|
+
traceRecent: RegisterTraceEntry[];
|
|
68
|
+
firstRegisterAt: number | null;
|
|
69
|
+
warmupWindowMs?: number;
|
|
70
|
+
}): RegisterTraceSummary {
|
|
71
|
+
const warmupWindowMs = Math.max(
|
|
72
|
+
0,
|
|
73
|
+
Number(args.warmupWindowMs ?? DEFAULT_REGISTER_WARMUP_WINDOW_MS) || 0,
|
|
74
|
+
);
|
|
75
|
+
const buckets: Record<string, number> = {};
|
|
76
|
+
let warmupCount = 0;
|
|
77
|
+
let postWarmupCount = 0;
|
|
78
|
+
let unexpectedRegisterAfterWarmup = false;
|
|
79
|
+
let lastUnexpectedRegisterAt: number | null = null;
|
|
80
|
+
const baseline = args.firstRegisterAt;
|
|
81
|
+
|
|
82
|
+
for (const trace of args.traceRecent) {
|
|
83
|
+
buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
|
|
84
|
+
const isWarmup = baseline != null && trace.ts - baseline <= warmupWindowMs;
|
|
85
|
+
if (isWarmup) {
|
|
86
|
+
warmupCount += 1;
|
|
87
|
+
} else {
|
|
88
|
+
postWarmupCount += 1;
|
|
89
|
+
unexpectedRegisterAfterWarmup = true;
|
|
90
|
+
lastUnexpectedRegisterAt = trace.ts;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dominantBucket = dominantRegisterBucket(buckets);
|
|
95
|
+
const likelyRuntimeRegistryDrift = postWarmupCount > 0;
|
|
96
|
+
const likelyStartupFanoutOnly = warmupCount > 0 && postWarmupCount === 0;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
startupWindowMs: warmupWindowMs,
|
|
100
|
+
traceWindowSize: args.traceRecent.length,
|
|
101
|
+
sourceBuckets: buckets,
|
|
102
|
+
dominantBucket,
|
|
103
|
+
warmupRegisterCount: warmupCount,
|
|
104
|
+
postWarmupRegisterCount: postWarmupCount,
|
|
105
|
+
unexpectedRegisterAfterWarmup,
|
|
106
|
+
lastUnexpectedRegisterAt,
|
|
107
|
+
likelyRuntimeRegistryDrift,
|
|
108
|
+
likelyStartupFanoutOnly,
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/core/status.ts
CHANGED
|
@@ -142,6 +142,58 @@ export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
export function buildAccountStatusSnapshot(input: {
|
|
146
|
+
account: { accountId: string; name?: string; enabled?: boolean };
|
|
147
|
+
runtime: any;
|
|
148
|
+
healthSummary: string;
|
|
149
|
+
displayName: string;
|
|
150
|
+
}) {
|
|
151
|
+
const rt = input.runtime || {};
|
|
152
|
+
const meta = rt?.meta || {};
|
|
153
|
+
|
|
154
|
+
const pending = Number(rt?.pending ?? meta.pending ?? 0);
|
|
155
|
+
const deadLetter = Number(rt?.deadLetter ?? meta.deadLetter ?? 0);
|
|
156
|
+
const lastSessionKey = rt?.lastSessionKey ?? meta.lastSessionKey ?? null;
|
|
157
|
+
const lastSessionScope = rt?.lastSessionScope ?? meta.lastSessionScope ?? null;
|
|
158
|
+
const lastSessionAt = rt?.lastSessionAt ?? meta.lastSessionAt ?? null;
|
|
159
|
+
const lastSessionAgo = rt?.lastSessionAgo ?? meta.lastSessionAgo ?? '-';
|
|
160
|
+
const lastActivityAt = rt?.lastActivityAt ?? meta.lastActivityAt ?? null;
|
|
161
|
+
const lastActivityAgo = rt?.lastActivityAgo ?? meta.lastActivityAgo ?? '-';
|
|
162
|
+
const lastInboundAt = rt?.lastInboundAt ?? meta.lastInboundAt ?? null;
|
|
163
|
+
const lastInboundAgo = rt?.lastInboundAgo ?? meta.lastInboundAgo ?? '-';
|
|
164
|
+
const lastOutboundAt = rt?.lastOutboundAt ?? meta.lastOutboundAt ?? null;
|
|
165
|
+
const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
|
|
166
|
+
const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
|
|
167
|
+
const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
accountId: input.account.accountId,
|
|
171
|
+
name: input.displayName,
|
|
172
|
+
enabled: input.account.enabled !== false,
|
|
173
|
+
configured: true,
|
|
174
|
+
linked: Boolean(rt?.connected),
|
|
175
|
+
running: rt?.running ?? false,
|
|
176
|
+
connected: rt?.connected ?? false,
|
|
177
|
+
lastEventAt: rt?.lastEventAt ?? null,
|
|
178
|
+
lastError: rt?.lastError ?? null,
|
|
179
|
+
mode: normalizedMode,
|
|
180
|
+
pending,
|
|
181
|
+
deadLetter,
|
|
182
|
+
healthSummary: input.healthSummary,
|
|
183
|
+
lastSessionKey,
|
|
184
|
+
lastSessionScope,
|
|
185
|
+
lastSessionAt,
|
|
186
|
+
lastSessionAgo,
|
|
187
|
+
lastActivityAt,
|
|
188
|
+
lastActivityAgo,
|
|
189
|
+
lastInboundAt,
|
|
190
|
+
lastInboundAgo,
|
|
191
|
+
lastOutboundAt,
|
|
192
|
+
lastOutboundAgo,
|
|
193
|
+
diagnostics,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
145
197
|
export function buildChannelSummaryFromRuntime(input: RuntimeStatusInput) {
|
|
146
198
|
const headline = buildStatusHeadlineFromRuntime(input);
|
|
147
199
|
return {
|