@ttt-productions/chat-react 0.13.3 → 0.13.4
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/realtime/channel-client.d.ts +21 -0
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +109 -4
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/diagnostics.d.ts +7 -0
- package/dist/realtime/diagnostics.d.ts.map +1 -1
- package/dist/realtime/diagnostics.js +7 -0
- package/dist/realtime/diagnostics.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +29 -8
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/shared.d.ts +21 -0
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +22 -0
- package/dist/realtime/shared.js.map +1 -1
- package/package.json +4 -4
|
@@ -73,6 +73,13 @@ export declare class ChannelClient {
|
|
|
73
73
|
private reauthAttempted;
|
|
74
74
|
private presenceSubscribed;
|
|
75
75
|
private heartbeatTimer;
|
|
76
|
+
/**
|
|
77
|
+
* When the CURRENT socket last proved it was alive: set on open, refreshed by
|
|
78
|
+
* every successfully parsed inbound frame (`heartbeat-ack` included — that IS
|
|
79
|
+
* the DO's liveness answer). Null whenever no socket is live, so the watchdog
|
|
80
|
+
* can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.
|
|
81
|
+
*/
|
|
82
|
+
private lastInboundAt;
|
|
76
83
|
private typingTimers;
|
|
77
84
|
private lastTypingSentAt;
|
|
78
85
|
private reconnectTimer;
|
|
@@ -292,6 +299,20 @@ export declare class ChannelClient {
|
|
|
292
299
|
private requestHistory;
|
|
293
300
|
private startHeartbeat;
|
|
294
301
|
private stopHeartbeat;
|
|
302
|
+
/** True when the live socket has produced no parseable frame for too long. */
|
|
303
|
+
private isInboundSilent;
|
|
304
|
+
/**
|
|
305
|
+
* The channel DO auto-acks every heartbeat, so silence past
|
|
306
|
+
* {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no
|
|
307
|
+
* `onclose` ever fired (the half-open-connection case that otherwise strands
|
|
308
|
+
* the UI on a socket that will never deliver another message).
|
|
309
|
+
*
|
|
310
|
+
* Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close
|
|
311
|
+
* lands on `onClose`'s ordinary transient path — which already owns the
|
|
312
|
+
* reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing
|
|
313
|
+
* about reconnect policy is duplicated here.
|
|
314
|
+
*/
|
|
315
|
+
private closeDeadSocket;
|
|
295
316
|
/** Permanently close this client (auth-user switch / unmount). Idempotent. */
|
|
296
317
|
close(): void;
|
|
297
318
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAahE,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AASjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAKL,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
|
18
18
|
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, ChatSendRejectedPayloadSchema, } from './wire.js';
|
|
19
19
|
import { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, } from './map.js';
|
|
20
20
|
import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
|
|
21
|
-
import { defaultTimers, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
|
|
21
|
+
import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
|
|
22
22
|
const INITIAL = {
|
|
23
23
|
status: 'idle',
|
|
24
24
|
messages: [],
|
|
@@ -39,6 +39,37 @@ const INITIAL = {
|
|
|
39
39
|
const MEMBERSHIP_PENDING_CODE = 'membership-pending';
|
|
40
40
|
/** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */
|
|
41
41
|
const MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;
|
|
42
|
+
/**
|
|
43
|
+
* Dead-socket watchdog: how long inbound silence may last before this client
|
|
44
|
+
* treats the socket as dead and closes it locally.
|
|
45
|
+
*
|
|
46
|
+
* CHANNEL-ONLY, and this is why it is sound here: the channel DO answers every
|
|
47
|
+
* 20 s client `heartbeat` through the hibernation AUTO-RESPONSE API, so a healthy
|
|
48
|
+
* channel socket is NEVER silent for a full heartbeat window. Inbound silence past
|
|
49
|
+
* this threshold is therefore provable death (a half-open TCP connection whose
|
|
50
|
+
* `onclose` never fires), not idleness. The INBOX socket has no such auto-response
|
|
51
|
+
* and a healthy idle inbox is legitimately silent for hours — applying this rule
|
|
52
|
+
* there would endlessly reconnect healthy sockets, so it is deliberately absent
|
|
53
|
+
* from `inbox-client.ts`.
|
|
54
|
+
*
|
|
55
|
+
* PRIVATE CLIENT POLICY, not a wire constant: nothing on the worker side agrees to
|
|
56
|
+
* it, so it does not belong in `@ttt-productions/chat-schemas` or in this package's
|
|
57
|
+
* exported surface. The check rides the existing 20 s heartbeat tick (no second
|
|
58
|
+
* timer), so detection lands on the NEXT tick after the deadline rather than at
|
|
59
|
+
* exactly 50 s — intended: 2.5 missed windows is unambiguous, and it costs no
|
|
60
|
+
* extra timer.
|
|
61
|
+
*/
|
|
62
|
+
const INBOUND_SILENCE_LIMIT_MS = 50_000;
|
|
63
|
+
/**
|
|
64
|
+
* Close code used for the watchdog's LOCAL close. A browser `WebSocket.close()`
|
|
65
|
+
* accepts only 1000 or 3000–4999, and the private 4xxx range is the chat wire's
|
|
66
|
+
* (`CHAT_CLOSE_CODES`) — a client-invented code there could collide with a future
|
|
67
|
+
* server code. 1000 keeps the close frame valid and, being neither 4401 nor 4403,
|
|
68
|
+
* routes through `onClose`'s ordinary transient path, which owns reconnect,
|
|
69
|
+
* backoff, and resume. The reason string + the liveness diagnostic are what
|
|
70
|
+
* identify it.
|
|
71
|
+
*/
|
|
72
|
+
const WATCHDOG_CLOSE_CODE = 1000;
|
|
42
73
|
export class ChannelClient {
|
|
43
74
|
config;
|
|
44
75
|
timers;
|
|
@@ -55,6 +86,13 @@ export class ChannelClient {
|
|
|
55
86
|
reauthAttempted = false;
|
|
56
87
|
presenceSubscribed = false;
|
|
57
88
|
heartbeatTimer = null;
|
|
89
|
+
/**
|
|
90
|
+
* When the CURRENT socket last proved it was alive: set on open, refreshed by
|
|
91
|
+
* every successfully parsed inbound frame (`heartbeat-ack` included — that IS
|
|
92
|
+
* the DO's liveness answer). Null whenever no socket is live, so the watchdog
|
|
93
|
+
* can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.
|
|
94
|
+
*/
|
|
95
|
+
lastInboundAt = null;
|
|
58
96
|
typingTimers = new Map();
|
|
59
97
|
lastTypingSentAt = Number.NEGATIVE_INFINITY;
|
|
60
98
|
reconnectTimer = null;
|
|
@@ -124,6 +162,23 @@ export class ChannelClient {
|
|
|
124
162
|
// legitimate reconnect.
|
|
125
163
|
if (this.state.status !== 'idle' && this.state.status !== 'closed')
|
|
126
164
|
return;
|
|
165
|
+
// REVIVE the controller. `controller.close()` is PERMANENT: while it is in
|
|
166
|
+
// state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`
|
|
167
|
+
// returns null. So without this reset a close() → connect() client opens a
|
|
168
|
+
// socket and works normally until its FIRST transient close, at which point
|
|
169
|
+
// `scheduleReconnect()` gets null and parks it terminally in `closed` with
|
|
170
|
+
// reconnect disabled and no surfaced error code. The idempotency guard above
|
|
171
|
+
// is what keeps this from disturbing a live lifecycle: only an `idle` or
|
|
172
|
+
// fully torn-down (`closed`) client ever reaches here.
|
|
173
|
+
this.controller.reset();
|
|
174
|
+
// A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN
|
|
175
|
+
// its lifecycle; an explicit connect() is a fresh attempt the authority may
|
|
176
|
+
// legitimately re-deny, so the previous lifecycle's code must not persist into
|
|
177
|
+
// this one (the hook maps `access-denied` to `allowed: false`). Loaded
|
|
178
|
+
// messages and the initial-load flag are deliberately KEPT — a lifecycle
|
|
179
|
+
// restart must not churn the UI back to an empty opening state.
|
|
180
|
+
if (isTerminalErrorCode(this.state.lastErrorCode))
|
|
181
|
+
this.setState({ lastErrorCode: null });
|
|
127
182
|
this.closedByUs = false;
|
|
128
183
|
this.reconnectCause = 'initial';
|
|
129
184
|
this.controller.start();
|
|
@@ -159,6 +214,9 @@ export class ChannelClient {
|
|
|
159
214
|
}
|
|
160
215
|
if (this.closedByUs)
|
|
161
216
|
return;
|
|
217
|
+
// Socket REPLACEMENT: the previous socket's liveness record dies with it, so
|
|
218
|
+
// the watchdog never judges a new socket against an old deadline.
|
|
219
|
+
this.lastInboundAt = null;
|
|
162
220
|
const url = `${this.config.endpoint.replace(/\/$/, '')}/channel`;
|
|
163
221
|
this.socket = this.config.socketFactory({
|
|
164
222
|
url,
|
|
@@ -174,6 +232,9 @@ export class ChannelClient {
|
|
|
174
232
|
onOpen() {
|
|
175
233
|
this.controller.onOpen();
|
|
176
234
|
this.reauthAttempted = false;
|
|
235
|
+
// Start the liveness clock at open — the first heartbeat window is measured
|
|
236
|
+
// from here, not from a stale previous socket.
|
|
237
|
+
this.lastInboundAt = this.timers.now();
|
|
177
238
|
this.setState({ status: 'open' });
|
|
178
239
|
this.diag?.(DIAG.SOCKET_OPEN, {
|
|
179
240
|
attempt: this.connectAttempt,
|
|
@@ -512,6 +573,7 @@ export class ChannelClient {
|
|
|
512
573
|
onClose(code, _reason) {
|
|
513
574
|
this.stopHeartbeat();
|
|
514
575
|
this.socket = null;
|
|
576
|
+
this.lastInboundAt = null;
|
|
515
577
|
this.lastCloseCode = code;
|
|
516
578
|
if (this.closedByUs) {
|
|
517
579
|
this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed', resumeSeq: this.resumeSeq });
|
|
@@ -536,7 +598,7 @@ export class ChannelClient {
|
|
|
536
598
|
this.controller.close();
|
|
537
599
|
this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked', resumeSeq: this.resumeSeq });
|
|
538
600
|
this.failAllPendingSends();
|
|
539
|
-
this.setState({ status: 'closed', lastErrorCode:
|
|
601
|
+
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
|
|
540
602
|
return;
|
|
541
603
|
}
|
|
542
604
|
this.reconnectCause = 'transient-close';
|
|
@@ -579,15 +641,21 @@ export class ChannelClient {
|
|
|
579
641
|
this.closedByUs = true;
|
|
580
642
|
this.controller.close();
|
|
581
643
|
this.failAllPendingSends();
|
|
582
|
-
this.setState({ status: 'closed', lastErrorCode:
|
|
644
|
+
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });
|
|
583
645
|
}
|
|
584
646
|
// ---- inbound frames ----
|
|
585
647
|
onMessage(data) {
|
|
586
648
|
const frame = parseFrame(data);
|
|
587
649
|
if (!frame || !frame.type) {
|
|
650
|
+
// Unparseable: bytes arrived, but nothing proves they came from a DO
|
|
651
|
+
// speaking this protocol, so this must NOT refresh the liveness deadline.
|
|
588
652
|
this.diag?.(DIAG.FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });
|
|
589
653
|
return;
|
|
590
654
|
}
|
|
655
|
+
// A well-formed envelope IS the liveness proof — including the `heartbeat-ack`
|
|
656
|
+
// the DO auto-responds with, and a forward-compat type this build does not
|
|
657
|
+
// handle yet (a newer DO's frames must never be read as a dead socket).
|
|
658
|
+
this.lastInboundAt = this.timers.now();
|
|
591
659
|
const payload = frame.payload ?? {};
|
|
592
660
|
switch (frame.type) {
|
|
593
661
|
case 'message':
|
|
@@ -1064,12 +1132,17 @@ export class ChannelClient {
|
|
|
1064
1132
|
const sent = this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });
|
|
1065
1133
|
this.diag?.(DIAG.HISTORY_REQUEST, { beforeSeq, limit: HISTORY_PAGE_MAX, sent });
|
|
1066
1134
|
}
|
|
1067
|
-
// ---- heartbeat (20s) ----
|
|
1135
|
+
// ---- heartbeat (20s) + dead-socket watchdog ----
|
|
1068
1136
|
startHeartbeat() {
|
|
1069
1137
|
this.stopHeartbeat();
|
|
1070
1138
|
const tick = () => {
|
|
1071
1139
|
if (!this.socket || !this.socket.isOpen)
|
|
1072
1140
|
return;
|
|
1141
|
+
// Liveness is checked BEFORE the write: a socket already proven dead must
|
|
1142
|
+
// not be handed another frame. Closing here does not reschedule — onClose
|
|
1143
|
+
// stops the heartbeat and owns what happens next.
|
|
1144
|
+
if (this.isInboundSilent())
|
|
1145
|
+
return this.closeDeadSocket();
|
|
1073
1146
|
this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});
|
|
1074
1147
|
this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);
|
|
1075
1148
|
};
|
|
@@ -1081,12 +1154,44 @@ export class ChannelClient {
|
|
|
1081
1154
|
this.heartbeatTimer = null;
|
|
1082
1155
|
}
|
|
1083
1156
|
}
|
|
1157
|
+
/** True when the live socket has produced no parseable frame for too long. */
|
|
1158
|
+
isInboundSilent() {
|
|
1159
|
+
if (this.lastInboundAt == null)
|
|
1160
|
+
return false;
|
|
1161
|
+
return this.timers.now() - this.lastInboundAt >= INBOUND_SILENCE_LIMIT_MS;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* The channel DO auto-acks every heartbeat, so silence past
|
|
1165
|
+
* {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no
|
|
1166
|
+
* `onclose` ever fired (the half-open-connection case that otherwise strands
|
|
1167
|
+
* the UI on a socket that will never deliver another message).
|
|
1168
|
+
*
|
|
1169
|
+
* Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close
|
|
1170
|
+
* lands on `onClose`'s ordinary transient path — which already owns the
|
|
1171
|
+
* reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing
|
|
1172
|
+
* about reconnect policy is duplicated here.
|
|
1173
|
+
*/
|
|
1174
|
+
closeDeadSocket() {
|
|
1175
|
+
const socket = this.socket;
|
|
1176
|
+
this.diag?.(DIAG.SOCKET_LIVENESS_TIMEOUT, {
|
|
1177
|
+
silentMs: this.lastInboundAt == null ? null : this.timers.now() - this.lastInboundAt,
|
|
1178
|
+
thresholdMs: INBOUND_SILENCE_LIMIT_MS,
|
|
1179
|
+
attempt: this.connectAttempt,
|
|
1180
|
+
resumeSeq: this.resumeSeq,
|
|
1181
|
+
pendingSends: this.pendingSends.size,
|
|
1182
|
+
});
|
|
1183
|
+
this.lastInboundAt = null;
|
|
1184
|
+
socket?.close(WATCHDOG_CLOSE_CODE, 'client liveness timeout');
|
|
1185
|
+
}
|
|
1084
1186
|
// ---- teardown ----
|
|
1085
1187
|
/** Permanently close this client (auth-user switch / unmount). Idempotent. */
|
|
1086
1188
|
close() {
|
|
1087
1189
|
this.closedByUs = true;
|
|
1088
1190
|
this.controller.close();
|
|
1089
1191
|
this.stopHeartbeat();
|
|
1192
|
+
// The heartbeat timer IS the watchdog's timer — stopping it above ends both.
|
|
1193
|
+
// Clearing the record too means a later connect() starts from a clean deadline.
|
|
1194
|
+
this.lastInboundAt = null;
|
|
1090
1195
|
for (const t of this.typingTimers.values())
|
|
1091
1196
|
this.timers.clearTimeout(t);
|
|
1092
1197
|
this.typingTimers.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-client.js","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,kFAAkF;AAClF,8EAA8E;AAC9E,EAAE;AACF,iCAAiC;AACjC,iFAAiF;AACjF,4DAA4D;AAC5D,mFAAmF;AACnF,kEAAkE;AAClE,gDAAgD;AAChD,+DAA+D;AAC/D,wFAAwF;AACxF,2EAA2E;AAC3E,oFAAoF;AACpF,EAAE;AACF,oFAAoF;AAEpF,OAAO,EAAE,yBAAyB,EAA4B,MAAM,gCAAgC,CAAC;AAErG,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,eAAe,EACf,6BAA6B,GAK9B,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,cAAc,GAEf,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,6BAA6B,IAAI,IAAI,GAGtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAoFrB,MAAM,OAAO,GAAuB;IAClC,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,EAAE;IACZ,MAAM,EAAE,EAAE;IACV,QAAQ,EAAE,EAAE;IACZ,eAAe,EAAE,KAAK;IACtB,QAAQ,EAAE,IAAI;IACd,aAAa,EAAE,IAAI;IACnB,oBAAoB,EAAE,KAAK;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AACrD,uEAAuE;AACvE,MAAM,mCAAmC,GAAG,IAAI,CAAC;AAEjD,MAAM,OAAO,aAAa;IAyDK;IAxDZ,MAAM,CAAkB;IACxB,UAAU,CAAsB;IACzC,MAAM,GAA0B,IAAI,CAAC;IACrC,KAAK,GAAuB,OAAO,CAAC;IAC3B,SAAS,GAAG,IAAI,GAAG,EAAmC,CAAC;IAExE,wFAAwF;IAChF,SAAS,GAAG,CAAC,CAAC;IACtB;2CACuC;IAC/B,WAAW,GAAiD,IAAI,CAAC;IACzE,iGAAiG;IACzF,eAAe,GAAG,KAAK,CAAC;IACxB,kBAAkB,GAAG,KAAK,CAAC;IAC3B,cAAc,GAAqD,IAAI,CAAC;IACxE,YAAY,GAAG,IAAI,GAAG,EAAqD,CAAC;IAC5E,gBAAgB,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC5C,cAAc,GAAqD,IAAI,CAAC;IAChF,iFAAiF;IACzE,oBAAoB,GAAqD,IAAI,CAAC;IAC9E,UAAU,GAAG,KAAK,CAAC;IAC3B;;;;;;qCAMiC;IACzB,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;IACxD;;;sFAGkF;IAC1E,YAAY,GAAG,IAAI,GAAG,EAAyB,CAAC;IACxD;;;;;;;wDAOoD;IAC5C,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAC;IACtD;;;;OAIG;IACc,IAAI,CAA+B;IACpD,4EAA4E;IACpE,cAAc,GAAG,CAAC,CAAC;IAC3B,uEAAuE;IAC/D,cAAc,GAAmE,SAAS,CAAC;IACnG,gFAAgF;IACxE,aAAa,GAAkB,IAAI,CAAC;IAE5C,YAA6B,MAA2B;QAA3B,WAAM,GAAN,MAAM,CAAqB;QACtD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,aAAa,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,yBAAyB,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC3D,CAAC;IAED,6BAA6B;IAE7B,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,SAAS,CAAC,EAAmC;QAC3C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvB,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC;IAEO,QAAQ,CAAC,KAAkC;QACjD,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC;QACzC,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS;YAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED,iCAAiC;IAEjC,KAAK,CAAC,OAAO;QACX,2EAA2E;QAC3E,4EAA4E;QAC5E,+EAA+E;QAC/E,wEAAwE;QACxE,yEAAyE;QACzE,wBAAwB;QACxB,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO;QAC3E,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAChC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QACxC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,UAAU;QACtB,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE;YAChC,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,KAAK,EAAE,IAAI,CAAC,cAAc;YAC1B,SAAS,EAAE,IAAI,CAAC,aAAa;YAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;SACrC,CAAC,CAAC;QACH,IAAI,UAAkB,CAAC;QACvB,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,mEAAmE;YACnE,0EAA0E;YAC1E,6EAA6E;YAC7E,8EAA8E;YAC9E,8EAA8E;YAC9E,+BAA+B;YAC/B,MAAM,QAAQ,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAC;YAC9C,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC3E,IAAI,QAAQ;gBAAE,OAAO,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACjD,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;YACpC,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAClC,CAAC;QACD,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC;QACjE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC;YACtC,GAAG;YACH,UAAU;YACV,QAAQ,EAAE;gBACR,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;gBAC3B,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBACzC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;gBACrD,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE;aAC9B;SACF,CAAC,CAAC;IACL,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;YAC5B,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,KAAK,EAAE,IAAI,CAAC,cAAc;YAC1B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;YACpC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;SAC5C,CAAC,CAAC;QACH,gFAAgF;QAChF,qFAAqF;QACrF,qEAAqE;QACrE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;QAC7F,+DAA+D;QAC/D,IAAI,IAAI,CAAC,kBAAkB;YAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QACjF,kFAAkF;QAClF,mFAAmF;QACnF,IAAI,IAAI,CAAC,WAAW;YAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACrF,kFAAkF;QAClF,6EAA6E;QAC7E,8EAA8E;QAC9E,6DAA6D;QAC7D,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACrC,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACK,kBAAkB,CAAC,MAA0C;QACnE,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC9B,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YACtD,6EAA6E;YAC7E,gEAAgE;YAChE,IAAI,OAAO,CAAC,MAAM;gBAAE,SAAS;YAC7B,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;gBAC5B,6EAA6E;gBAC7E,0EAA0E;gBAC1E,6DAA6D;gBAC7D,IAAI,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,2BAA2B,EAAE,CAAC;oBACxD,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;oBAC/E,SAAS;gBACX,CAAC;gBACD,2EAA2E;gBAC3E,2EAA2E;gBAC3E,yEAAyE;gBACzE,yBAAyB;gBACzB,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC;iBAAM,CAAC;gBACN,4EAA4E;gBAC5E,oEAAoE;gBACpE,MAAM,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAuB,CAAC;gBAC/D,MAAM,eAAe,GAAG,OAAO,CAAC,QAAQ,IAAI,yBAAyB,CAAC;gBACtE,IAAI,MAAM,IAAI,eAAe,EAAE,CAAC;oBAC9B,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;oBAC9C,SAAS;gBACX,CAAC;YACH,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;gBAC7C,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,IAAI,EAAE,OAAO,CAAC,IAAI;aACnB,CAAC,CAAC;YACH,IAAI,IAAI;gBAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC;YAChC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;gBAC3B,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,MAAM;gBACN,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM;gBAC3B,IAAI;aACL,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;;;;;;;;;OAWG;IACK,eAAe,CACrB,eAAuB,EACvB,IAA4B,EAC5B,SAAmB;QAEnB,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM;YAAE,OAAO;QACvC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QACtB,iFAAiF;QACjF,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;YAC5B,eAAe;YACf,IAAI,EAAE,IAAI,IAAI,aAAa;YAC3B,SAAS,EAAE,SAAS,IAAI,IAAI;YAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM;SAC1C,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACzC,CAAC,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,eAAe;YAC/D,CAAC,CAAC;gBACE,GAAG,CAAC;gBACJ,IAAI,EAAE;oBACJ,GAAG,CAAC,CAAC,IAAI;oBACT,UAAU,EAAE,IAAI;oBAChB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC1C,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC3D;aACF;YACH,CAAC,CAAC,CAAC,CACN,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,IAAI,aAAa,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,gEAAgE;IACxD,mBAAmB,CAAC,OAAoB;QAC9C,IAAI,OAAO,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAC7C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,gBAAgB,CAAC,eAAuB;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,IAAI,OAAO;YAAE,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC;IAED,oFAAoF;IAC5E,8BAA8B,CAAC,OAAe;QACpD,IAAI,IAAI,CAAC,oBAAoB,IAAI,IAAI;YAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAC3F,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACtD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;YACjC,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YAC5B,IAAI,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,CAAC;QAChD,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,iBAAiB,CAAC,OAAgC;QACxD,MAAM,MAAM,GAAG,6BAA6B,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,6EAA6E;YAC7E,8EAA8E;YAC9E,6EAA6E;YAC7E,oDAAoD;YACpD,MAAM,KAAK,GAAI,OAAyC,CAAC,eAAe,CAAC;YACzE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;gBACzE,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC9C,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;oBACjC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,CAAC;oBAChG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;oBAC5B,OAAO;gBACT,CAAC;YACH,CAAC;YACD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;YAC3F,OAAO;QACT,CAAC;QACD,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC;QACvE,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,6EAA6E;QAC7E,8EAA8E;QAC9E,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;YAClG,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,0EAA0E;YAC1E,0EAA0E;YAC1E,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;YAClG,IAAI,CAAC,eAAe,CAAC,eAAe,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACnD,OAAO;QACT,CAAC;QAED,6EAA6E;QAC7E,6EAA6E;QAC7E,4BAA4B;QAC5B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;QAC/B,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;QACjC,8EAA8E;QAC9E,wDAAwD;QACxD,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,IAAI,2BAA2B,EAAE,CAAC;YACtE,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;YACzG,IAAI,CAAC,eAAe,CAAC,eAAe,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YAClD,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC;QACjH,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED;;;;;OAKG;IACK,iBAAiB,CAAC,eAAuB,EAAE,OAAe;QAChE,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YAC/C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;YAC1B,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;YACvD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM;gBAAE,OAAO;YACvC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,IAAI,2BAA2B,EAAE,CAAC;gBACtE,IAAI,CAAC,eAAe,CAAC,eAAe,EAAE,OAAO,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;gBACvE,OAAO;YACT,CAAC;YACD,+EAA+E;YAC/E,kEAAkE;YAClE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;gBAC7C,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,IAAI,EAAE,OAAO,CAAC,IAAI;aACnB,CAAC,CAAC;YACH,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;gBAC3B,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,MAAM,EAAE,WAAW;gBACnB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM;gBACzC,IAAI;aACL,CAAC,CAAC;YACH,4EAA4E;YAC5E,0EAA0E;YAC1E,wEAAwE;YACxE,6EAA6E;YAC7E,4EAA4E;YAC5E,MAAM,WAAW,GACf,2BAA2B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,uBAAuB,CAAC;YAC/F,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;gBAC/C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;gBAC1B,IAAI,IAAI,CAAC,UAAU;oBAAE,OAAO;gBAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBAC1D,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM;oBAAE,OAAO;gBAC7C,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,MAAM,IAAI,2BAA2B,EAAE,CAAC;oBACzE,IAAI,CAAC,eAAe,CAAC,eAAe,EAAE,UAAU,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;gBAC5E,CAAC;YACH,CAAC,EAAE,WAAW,CAAC,CAAC;QAClB,CAAC,EAAE,OAAO,CAAC,CAAC;IACd,CAAC;IAED,4FAA4F;IACpF,eAAe,CAAC,YAAgC;QACtD,MAAM,GAAG,GACP,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,GAAG,CAAC;YAClD,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,mCAAmC,CAAC;QAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,uBAAuB,CAAC,EAAE,uBAAuB,CAAC,CAAC;IACnF,CAAC;IAED;;;;OAIG;IACK,mBAAmB;QACzB,KAAK,MAAM,eAAe,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC5D,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,eAAuB;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,+EAA+E;QAC/E,kFAAkF;QAClF,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,eAAe,GAAG,KAAK,CAAC;QAChC,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;QACtC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;YAC7C,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC,CAAC;QACH,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;YAC3B,eAAe;YACf,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,eAAe,EAAE,KAAK;YACtB,KAAK,EAAE,CAAC;YACR,IAAI;SACL,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACzC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,eAAe,IAAI,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;gBAC/F,OAAO,CAAC,CAAC;YACX,CAAC;YACD,+DAA+D;YAC/D,MAAM,EACJ,UAAU,EAAE,WAAW,EACvB,eAAe,EAAE,gBAAgB,EACjC,aAAa,EAAE,cAAc,EAC7B,GAAG,QAAQ,EACZ,GAAG,CAAC,CAAC,IAAI,CAAC;YACX,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,QAAQ,EAAE,EAAE,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,OAAO;QACb,uEAAuE;IACzE,CAAC;IAEO,OAAO,CAAC,IAAY,EAAE,OAAe;QAC3C,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YACzG,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,iFAAiF;QACjF,IAAI,IAAI,KAAK,gBAAgB,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YACpE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;YAC5B,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;YACrC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC5G,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;YAC1C,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QACD,8EAA8E;QAC9E,+EAA+E;QAC/E,6EAA6E;QAC7E,oCAAoC;QACpC,IAAI,IAAI,KAAK,gBAAgB,CAAC,OAAO,EAAE,CAAC;YACtC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC3G,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC;YAC9D,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,iBAAiB,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC7G,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAC3B,CAAC;IAEO,iBAAiB;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QACxC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE;YACpC,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,IAAI,CAAC,cAAc;YAC1B,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QACH,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAClB,6EAA6E;YAC7E,sEAAsE;YACtE,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YAChD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YAC5B,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;QACzB,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAED;;;;;;;;OAQG;IACK,oBAAoB;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,2BAA2B;IAEnB,SAAS,CAAC,IAAY;QAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;YACjF,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;QACpC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,SAAS;gBACZ,OAAO,IAAI,CAAC,YAAY,CAAE,OAAuC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACrF,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAuE,CAAC,CAAC;YAChG,KAAK,cAAc;gBACjB,OAAO,IAAI,CAAC,gBAAgB,CAAE,OAA0C,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;YAC3F,KAAK,UAAU;gBACb,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAG,OAAgC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;YACrF,KAAK,QAAQ;gBACX,OAAO,IAAI,CAAC,WAAW,CAAE,OAA2B,CAAC,GAAG,CAAC,CAAC;YAC5D,KAAK,UAAU;gBACb,OAAO,IAAI,CAAC,oBAAoB,CAAC,OAAyC,CAAC,CAAC;YAC9E,KAAK,eAAe;gBAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;YACzC,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,IAAI,GAAG,MAAM,CAAE,OAA8B,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC;gBACrE,+EAA+E;gBAC/E,6DAA6D;gBAC7D,gFAAgF;gBAChF,0EAA0E;gBAC1E,4EAA4E;gBAC5E,4EAA4E;gBAC5E,4EAA4E;gBAC5E,IAAI,IAAI,KAAK,uBAAuB,EAAE,CAAC;oBACrC,MAAM,GAAG,GAAI,OAAsC,CAAC,YAAY,CAAC;oBACjE,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;oBAC9E,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;wBAC5B,IAAI,EAAE,uBAAuB;wBAC7B,UAAU,EAAE,KAAK;wBACjB,OAAO,EAAE,KAAK;wBACd,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;qBACrC,CAAC,CAAC;oBACH,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC;wBACjD,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;4BACpB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;4BAC/B,OAAO,CAAC,iBAAiB,GAAG,uBAAuB,CAAC;wBACtD,CAAC;oBACH,CAAC;oBACD,OAAO,IAAI,CAAC,8BAA8B,CAAC,KAAK,CAAC,CAAC;gBACpD,CAAC;gBACD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,mBAAmB,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;gBACtF,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;YACD,KAAK,UAAU;gBACb,OAAO,IAAI,CAAC,kBAAkB,CAAC,OAAiF,CAAC,CAAC;YACpH,KAAK,eAAe;gBAClB,yEAAyE;gBACzE,0EAA0E;gBAC1E,4EAA4E;gBAC5E,6EAA6E;gBAC7E,0EAA0E;gBAC1E,6EAA6E;gBAC7E,8EAA8E;gBAC9E,0EAA0E;gBAC1E,OAAO;YACT;gBACE,oDAAoD;gBACpD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;gBACnG,OAAO;QACX,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACK,kBAAkB,CAAC,OAA+E;QACxG,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;QAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAC1B,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;QAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,eAAe,KAAK,QAAQ,EAAE,CAAC;YACnF,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACnF,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,oFAAoF;QACpF,IAAI,IAAI,IAAI,IAAI,CAAC,eAAe,IAAI,eAAe,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE;gBAC9B,IAAI,EAAE,UAAU;gBAChB,MAAM,EAAE,gBAAgB;gBACxB,GAAG;gBACH,eAAe;gBACf,YAAY,EAAE,IAAI,CAAC,eAAe;aACnC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;QAClD,8EAA8E;QAC9E,0EAA0E;QAC1E,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;QACjG,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE;YACjC,GAAG;YACH,IAAI;YACJ,eAAe;YACf,kFAAkF;YAClF,QAAQ;SACT,CAAC,CAAC;QACH,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACpF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACxF,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,gBAAgB,CAAC,GAAW,EAAE,UAAoC;QACxE,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QACjD,IAAI,CAAC,UAAU;YAAE,OAAO,SAAS,CAAC;QAClC,IAAI,CAAC,SAAS;YAAE,OAAO,UAAU,CAAC;QAClC,OAAO,SAAS,CAAC,eAAe,IAAI,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;IAC1F,CAAC;IAED,kFAAkF;IAC1E,SAAS,CAAC,GAAmB;QACnC,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC3D,8EAA8E;QAC9E,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,sBAAsB,CAAC,MAAM,EAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7F,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACK,oBAAoB,CAAC,IAAyB;QACpD,8DAA8D;QAC9D,IAAI,eAAe,CAAC,IAAa,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC,CAAC;YAC3F,OAAO;QACT,CAAC;QACD,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,QAAQ,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC,CAAC;YAC1F,OAAO;QACT,CAAC;QAED,6EAA6E;QAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC;QACrC,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAE1D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,4EAA4E;YAC5E,+EAA+E;YAC/E,8DAA8D;YAC9D,KAAK,MAAM,GAAG,IAAI,KAAK;gBAAE,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACzD,IAAI,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS;gBAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC;YAC/E,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE;gBAC9B,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,MAAM,EAAE,KAAK;gBACb,UAAU,EAAE,KAAK,CAAC,MAAM;gBACxB,YAAY;gBACZ,WAAW,EAAE,IAAI,CAAC,SAAS;gBAC3B,OAAO,EAAE,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;aAChE,CAAC,CAAC;YACH,+EAA+E;YAC/E,4EAA4E;YAC5E,iEAAiE;YACjE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,oBAAoB;gBAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QAED,gFAAgF;QAChF,iFAAiF;QACjF,8EAA8E;QAC9E,gFAAgF;QAChF,8EAA8E;QAC9E,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACnB,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC9C,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE;YAC9B,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,MAAM,EAAE,IAAI;YACZ,UAAU,EAAE,KAAK,CAAC,MAAM;YACxB,YAAY;YACZ,WAAW,EAAE,CAAC;YACd,OAAO,EAAE,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;SAChE,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE;YACpC,2EAA2E;YAC3E,YAAY,EAAE,UAAU,GAAG,cAAc,CAAC,MAAM;YAChD,cAAc,EAAE,cAAc,CAAC,MAAM;SACtC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;OASG;IACK,YAAY,CAAC,GAAmB,EAAE,MAAwB;QAChE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACxC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QACD,+EAA+E;QAC/E,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACzD,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC/B,IAAI,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,CAAC;QACvD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QAEjC,MAAM,OAAO,GAAG,GAAG,CAAC,cAAc,IAAI,IAAI,IAAI,KAAK,CAAC,oBAAoB,CAAC;QACzE,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,OAAO;YAAE,OAAO;QAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE;YAC5B,IAAI,EAAE,SAAS;YACf,MAAM;YACN,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,kBAAkB,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;YAChD,cAAc,EAAE,GAAG,CAAC,cAAc,IAAI,IAAI;YAC1C,EAAE,EAAE,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ;YACpG,YAAY;YACZ,WAAW,EAAE,IAAI,CAAC,SAAS;SAC5B,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,aAAa,CAAC,GAAmB;QAIvC,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;QAC5E,OAAO;YACL,MAAM,EAAE,QAAQ,IAAI,IAAI;YACxB,oBAAoB,EAAE,OAAO,CAC3B,GAAG,CAAC,eAAe;gBACjB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,GAAG,CAAC,eAAe,CAAC,CACzG;SACF,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,MAAqB,EAAE,GAAmB;QAC7D,6EAA6E;QAC7E,8EAA8E;QAC9E,+EAA+E;QAC/E,IAAI,GAAG,CAAC,eAAe;YAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CACrC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS;YAChC,6DAA6D;YAC7D,CAAC,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,GAAG,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,CAC3E,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrB,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;IAEO,QAAQ,CAAC,OAAqE;QACpF,uEAAuE;QACvE,IAAI,OAAO,CAAC,eAAe,IAAI,IAAI,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ;YAAE,OAAO;QAC/E,6EAA6E;QAC7E,+EAA+E;QAC/E,qEAAqE;QACrE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;QACxB,MAAM,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC;QACrC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE;YACzB,eAAe,EAAE,IAAI;YACrB,GAAG;YACH,YAAY,EAAE,IAAI,CAAC,SAAS;YAC5B,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS;YACxD,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;SACxC,CAAC,CAAC;QACH,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACzC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI;gBAAE,OAAO,CAAC,CAAC;YACtE,0EAA0E;YAC1E,8EAA8E;YAC9E,4CAA4C;YAC5C,MAAM,EACJ,UAAU,EAAE,WAAW,EACvB,eAAe,EAAE,gBAAgB,EACjC,aAAa,EAAE,cAAc,EAC7B,GAAG,QAAQ,EACZ,GAAG,CAAC,CAAC,IAAI,CAAC;YACX,OAAO,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC;QACjG,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrB,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;QAC/C,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;IAEO,gBAAgB,CAAC,IAAsB;QAC7C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAClD,4EAA4E;QAC5E,+EAA+E;QAC/E,qFAAqF;QACrF,+EAA+E;QAC/E,qEAAqE;QACrE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;QACxC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;gBACtB,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;gBACrC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;QAC/C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpC,IAAI,CAAC,CAAC,IAAI,EAAE,UAAU,IAAI,OAAO,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,QAAQ,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;gBACnH,SAAS,CAAC,wCAAwC;YACpD,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAC5B,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,MAAM;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnD,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,IAAI,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS;gBAAE,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC;QACzE,IAAI,CAAC,QAAQ,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,eAAe,EAAE,KAAK;YACtB,QAAQ,EAAE,IAAI,CAAC,MAAM,IAAI,gBAAgB;YACzC,kFAAkF;YAClF,iFAAiF;YACjF,uEAAuE;YACvE,oBAAoB,EAAE,IAAI;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QACtF,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;YAC3B,KAAK,EAAE,IAAI,CAAC,MAAM;YAClB,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;YAClD,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;YAClD,YAAY;YACZ,WAAW,EAAE,IAAI,CAAC,SAAS;YAC3B,oBAAoB,EAAE,aAAa,CAAC,IAAI;YACxC,QAAQ,EAAE,IAAI,CAAC,MAAM,IAAI,gBAAgB;YACzC,KAAK,EAAE,MAAM,CAAC,MAAM;SACrB,CAAC,CAAC;IACL,CAAC;IAEO,WAAW,CAAC,GAAW;QAC7B,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,CAAC,aAAa;YAAE,OAAO;QACtD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7F,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,IAAI;YAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,YAAY,CAAC,GAAG,CACnB,GAAG,EACH,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YAC1B,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,CAC3B,CAAC;IACJ,CAAC;IAED,6BAA6B;IAErB,SAAS,CAAC,IAAY,EAAE,OAAgC;QAC9D,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,IAA+C;QAClD,4EAA4E;QAC5E,gFAAgF;QAChF,kFAAkF;QAClF,uDAAuD;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE;YAC7C,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE;gBAC5B,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,IAAI;gBACf,QAAQ,EAAE,CAAC;gBACX,MAAM,EAAE,iBAAiB;aAC1B,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC9B,mEAAmE;QACnE,kEAAkE;QAClE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE;YAC1C,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,GAAG;YACX,QAAQ,EAAE,CAAC;YACX,MAAM,EAAE,KAAK;YACb,eAAe,EAAE,KAAK;YACtB,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,iBAAiB,CAAC;YAC7B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC9B,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,aAAa;YACnC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,GAAG;SACf,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE;YAChC,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI;SACrC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,OAAe,EAAE,OAAgB;QACvC,IAAI,CAAC,WAAW,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,yEAAyE;IACzE,MAAM;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC9B,IAAI,GAAG,GAAG,IAAI,CAAC,gBAAgB,GAAG,kBAAkB;YAAE,OAAO;QAC7D,IAAI,CAAC,gBAAgB,GAAG,GAAG,CAAC;QAC5B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAC/B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,mBAAmB;QACjB,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;QAChC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,2FAA2F;IAC3F,UAAU;QACR,IAAI,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE,OAAO;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC;QAChF,MAAM,SAAS,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,IAAI,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAM,CAAC,IAAI,CAAC,GAAc,CAAC,CAAC,CAAC,IAAI,CAAC;QACtG,IAAI,CAAC,QAAQ,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC;IAEO,cAAc,CAAC,SAAwB;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAC1F,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,4BAA4B;IAEpB,cAAc;QACpB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;gBAAE,OAAO;YAChD,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;YAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QACnE,CAAC,CAAC;QACF,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACnE,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAC9C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,qBAAqB;IAErB,8EAA8E;IAC9E,KAAK;QACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACxE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,sEAAsE;QACtE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACpF,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAC9C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,IAAI,CAAC,oBAAoB,IAAI,IAAI,EAAE,CAAC;YACtC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;YACpD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACnC,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;YAC3C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;IAChE,CAAC;CACF;AAED,SAAS,SAAS,CAAC,CAAgB,EAAE,CAAgB;IACnD,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAI,CAAC,GAAc,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAC/F,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAI,CAAC,GAAc,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAC/F,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IAC9B,OAAO,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;AACnC,CAAC","sourcesContent":["// The Channel realtime client: ONE WebSocket to a single channel DO, driving the\n// Contract C wire protocol. Framework-neutral (a plain observable store the React\n// hook subscribes to) so it is unit-testable with a mock socket and no React.\n//\n// Responsibilities (Contract C):\n// - connect via subprotocol auth (grant offered by the injected grantProvider)\n// - receive versioned `message` frames; server owns `seq`\n// - optimistic send keyed by clientMessageId, reconciled on the server `ack` seq\n// - explicit read acks (delivery != read; focus signals \"read\")\n// - typing (>= 2s coalesced per the V1 limit)\n// - presence-subscribe/-unsubscribe + a 20s client heartbeat\n// - reconnect (realtime-core controller) with a resume cursor -> snapshot then deltas\n// - history pagination over the socket (page <= 50, epoch-aware via seq)\n// - error codes incl. 4401 AUTH_EXPIRED -> re-mint a grant ONCE via grantProvider\n//\n// Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n ChatSendRejectedPayloadSchema,\n type WireMessageRow,\n type WireChannelSnapshot,\n type RevisionKind,\n type ChatSendRejectionCode,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport {\n wireRowToMessage,\n optimisticMessage,\n seqToMessageId,\n applyModerationOverlay,\n overlayFromRow,\n type ModerationOverlay,\n} from './map.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n HEARTBEAT_MS,\n TYPING_COALESCE_MS,\n HISTORY_PAGE_MAX,\n MAX_PENDING_SEND_ATTEMPTS,\n PENDING_SEND_MAX_AGE_MS,\n SERVER_RETRYABLE_MAX_AGE_MS,\n SEND_RETRY_MIN_DELAY_MS,\n SEND_RETRY_MAX_DELAY_MS,\n} from './shared.js';\n\n/** An un-acked send tracked for reconnect resend (clientMessageId idempotency). */\ninterface PendingSend {\n clientMessageId: string;\n text: string;\n /** When the send was first attempted (for the age/budget caps). Reset by retrySend(). */\n sentAt: number;\n /** How many times it has been (re)sent over a socket. Reset by retrySend(). */\n attempts: number;\n /** True once the attempt/age/budget cap flipped this send to the visible failed\n * state. A failed entry is RETAINED (not deleted) so retrySend() can re-queue it\n * with the SAME clientMessageId; it is skipped by the automatic reconnect resend. */\n failed: boolean;\n /**\n * Set true once the Worker has declared THIS send retryable — either a correlated\n * `send-rejected` (retryable) frame or the legacy uncorrelated\n * `error{code:'membership-pending'}`. A server-retryable send is governed by the\n * {@link SERVER_RETRYABLE_MAX_AGE_MS} wall-clock budget and is NEVER failed by the\n * generic {@link MAX_PENDING_SEND_ATTEMPTS} cap (that cap stays for blind reconnect\n * resends of sends the server has said nothing about).\n */\n serverRetryable: boolean;\n /** The most recent server rejection code, recorded so a budget-exhausted failure\n * can surface the reason (and keep a retry affordance for the transient class). */\n lastRejectionCode?: ChatSendRejectionCode;\n /** The single per-send resend timer for a correlated retryable rejection (never\n * stacks — a newer rejection clears it first; cleared on ack/echo/manual\n * retry/terminal/close/dispose). */\n retryTimer: ReturnType<TransportTimers['setTimeout']> | null;\n}\n\nexport interface ChannelClientState {\n status: RealtimeStatus;\n /** Ordered ascending by seq; optimistic messages (no seq yet) sort to the tail. */\n messages: ChatMessageV1[];\n /** uids currently typing (cleared after ~TYPING_COALESCE_MS of silence). */\n typing: string[];\n /** uids the DO reports online (presence). Empty until presence-subscribe. */\n presence: string[];\n /** True while an older-history page request is in flight. */\n isFetchingOlder: boolean;\n /** False once a history page returns fewer than the requested limit (reached the head). */\n hasOlder: boolean;\n /** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */\n lastErrorCode: string | null;\n /**\n * True once the FIRST authoritative chat data has been applied — a non-resync\n * resume snapshot, OR the first history page (INCLUDING an empty one). Drives the\n * hook's `isInitialLoading` so the shell shows an honest \"Opening chat…\" state\n * while the first data is loading, and a real empty-chat surface only after an\n * authoritative empty result. NEVER inferred from `messages.length === 0` (an\n * empty first history page IS a successful load). Once true it stays true — a\n * later disconnect/reconnect keeps loaded messages mounted, not a re-loader.\n * A grant retry / socket open before any snapshot leaves it false.\n */\n hasLoadedInitialData: boolean;\n}\n\nexport interface ChannelClientConfig {\n /** WebSocket endpoint base, e.g. `wss://chat.ttt.productions`. The client appends `/channel`. */\n endpoint: string;\n /** The logical thread id the UI keys on (used as ChatMessageV1.threadId). */\n threadId: string;\n /** The authenticated uid (sender identity for optimistic echoes + auth-switch guard). */\n currentUserId: string;\n /** Mints/refreshes a channel grant. Called on connect and once on a 4401 re-grant. */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n /** Reconnect backoff tuning (forwarded to realtime-core). */\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics (default OFF — absent/`false` changes\n * nothing and costs one nullish check per decision point). `true` emits one\n * `console.debug` line per decision with a stable `chat_client_*` event name\n * and a compact, content-free JSON payload; a function routes those entries\n * into the app's own logger instead. Never logs message text, captions,\n * filenames, URLs, tokens, or previews — ids, seqs, states, counts, and codes\n * only. See `./diagnostics.ts`.\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: ChannelClientState = {\n status: 'idle',\n messages: [],\n typing: [],\n presence: [],\n isFetchingOlder: false,\n hasOlder: true,\n lastErrorCode: null,\n hasLoadedInitialData: false,\n};\n\n/**\n * Retryable error-frame code the channel DO sends when a send arrives BEFORE the\n * member row has synced through the async backend→worker pipeline (a just-created\n * invite/channel). The socket stays open; the client re-sends after `retryAfterMs`\n * through the normal resendPendingSends machinery (same caps). The string is part\n * of the wire contract with the worker runtime's send handler.\n */\nconst MEMBERSHIP_PENDING_CODE = 'membership-pending';\n/** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */\nconst MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;\n\nexport class ChannelClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: ChannelClientState = INITIAL;\n private readonly listeners = new Set<(s: ChannelClientState) => void>();\n\n /** Resume cursor: highest seq we've durably applied. Sent on resume after reconnect. */\n private resumeSeq = 0;\n /** Last read-ack the UI requested; re-sent on reconnect so a drop during a closed\n * socket window is recovered (M2). */\n private lastReadAck: { readSeq: number; focused: boolean } | null = null;\n /** True after a `4401` re-grant has been attempted for the CURRENT connect cycle (once-only). */\n private reauthAttempted = false;\n private presenceSubscribed = false;\n private heartbeatTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private typingTimers = new Map<string, ReturnType<TransportTimers['setTimeout']>>();\n private lastTypingSentAt = Number.NEGATIVE_INFINITY;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n /** Single scheduled re-send for the membership-pending window (never stacks). */\n private membershipRetryTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /**\n * Per-seq moderation overlay (the max-revision wins). A `revision` frame writes\n * here keyed by `messageSeq` so it applies whether it arrives BEFORE its base\n * row (re-applied when the row later renders) or AFTER (re-renders the present\n * row in place). Merged with the row's own inline `moderationKind` so a row that\n * arrives already-moderated from the DO AND one covered by a live frame both\n * render the moderated state. */\n private overlays = new Map<number, ModerationOverlay>();\n /**\n * The ORIGINAL (un-overlaid) mapped message per seq. A `restore` revision must\n * revert a previously-redacted message to its original text, so re-renders start\n * from this cache rather than the (possibly already-blanked) message in state. */\n private originalRows = new Map<number, ChatMessageV1>();\n /**\n * Un-acked sends keyed by clientMessageId. A send is added here when its frame is\n * written and removed when its `ack` (or the canonical `message` echo carrying the\n * same clientMessageId) arrives. On every reconnect resume they are RE-SENT with\n * the SAME clientMessageId — the DO dedups (re-acks without re-broadcast), so a\n * send that lost its socket before the ack is delivered exactly once. A pending\n * send that exceeds the attempt/age cap is flipped to a visible failed state\n * (retry affordance) instead of staying a ghost. */\n private pendingSends = new Map<string, PendingSend>();\n /**\n * The structured diagnostics emitter, or null when diagnostics are off (the\n * default). Every call site uses `this.diag?.(...)` so the off path constructs\n * no payload at all.\n */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n // ---- observable store ----\n\n getState(): ChannelClientState {\n return this.state;\n }\n\n subscribe(fn: (s: ChannelClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<ChannelClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n // ---- connection lifecycle ----\n\n async connect(): Promise<void> {\n // Idempotency guard (C-B7): connect() only starts a lifecycle from a fresh\n // (idle) or fully torn-down (closed) state. A second connect() while one is\n // already connecting / open / reconnecting is a no-op, so a stray double-owner\n // can't open a second WebSocket. The reconnect path uses openSocket() /\n // scheduleReconnect() directly (never connect()), so this never blocks a\n // legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n resumeSeq: this.resumeSeq,\n pendingSends: this.pendingSends.size,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app's grant provider translated an\n // authoritative policy \"no\" into the package-owned ChatAccessDeniedError)\n // must NOT reconnect — the next mint would just re-deny, stranding the shell\n // on the opening loader forever. Every other mint failure (network, transient\n // 'unavailable'/'internal', post-login 'unauthenticated') stays retryable and\n // backs off exactly as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/channel`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => this.onError(),\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n resumeSeq: this.resumeSeq,\n pendingSends: this.pendingSends.size,\n presenceSubscribed: this.presenceSubscribed,\n });\n // Resume: ask the DO for the authoritative snapshot, then live deltas flow. The\n // field name MUST be `afterSeq` — the Channel DO reads `payload.afterSeq`; any other\n // name reads as cursorless and forces a full resync every reconnect.\n this.sendFrame(CLIENT_FRAME.RESUME, { afterSeq: this.resumeSeq });\n this.diag?.(DIAG.RESUME_REQUEST, { afterSeq: this.resumeSeq, attempt: this.connectAttempt });\n // If the UI had presence open, re-subscribe after a reconnect.\n if (this.presenceSubscribed) this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n // Re-send the last read cursor so a read-ack dropped during the disconnect window\n // (sendFrame returns false on a closed socket) is not lost until the next message.\n if (this.lastReadAck) this.sendFrame(CLIENT_FRAME.READ_ACK, { ...this.lastReadAck });\n // Re-send every un-acked pending send with its ORIGINAL clientMessageId so a send\n // that lost its socket before the ack is not silently dropped. The DO dedups\n // (re-acks, no re-broadcast, no flood-token) — this is the client half of the\n // Contract C send-idempotency the server side was built for.\n this.resendPendingSends('reconnect');\n this.startHeartbeat();\n }\n\n /**\n * On reconnect, re-send each un-acked pending send with the SAME clientMessageId\n * (the DO dedups). A send that has exceeded the attempt or age cap is flipped to a\n * visible failed state instead of being resent forever — the ghost becomes a\n * retryable failure the UI can surface.\n */\n private resendPendingSends(reason: 'reconnect' | 'membership-pending'): void {\n if (this.pendingSends.size === 0) return;\n const now = this.timers.now();\n for (const pending of [...this.pendingSends.values()]) {\n // Already flipped to the visible failed state — only an explicit retrySend()\n // re-queues it; the automatic reconnect resend leaves it alone.\n if (pending.failed) continue;\n if (pending.serverRetryable) {\n // The Worker declared this send retryable — give-up is the wall-clock budget\n // ONLY (the 5-attempt cap must not fail a server-declared transient after\n // ~10 s while the backend projection window is up to 60+ s).\n if (now - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(pending.clientMessageId, pending.lastRejectionCode, true);\n continue;\n }\n // This reconnect resend supersedes any per-send timer scheduled before the\n // socket dropped — clear it so the send doesn't go out twice (the DO would\n // dedup, but the duplicate frame is pure waste). A fresh rejection frame\n // re-schedules as usual.\n this.clearSendRetryTimer(pending);\n } else {\n // A blind reconnect resend of a send the server has said nothing about: the\n // generic attempt/age caps still protect against a permanent ghost.\n const tooOld = now - pending.sentAt >= PENDING_SEND_MAX_AGE_MS;\n const tooManyAttempts = pending.attempts >= MAX_PENDING_SEND_ATTEMPTS;\n if (tooOld || tooManyAttempts) {\n this.failPendingSend(pending.clientMessageId);\n continue;\n }\n }\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n });\n if (sent) pending.attempts += 1;\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId: pending.clientMessageId,\n reason,\n attempts: pending.attempts,\n serverRetryable: pending.serverRetryable,\n ageMs: now - pending.sentAt,\n sent,\n });\n }\n }\n\n /**\n * Flip an un-acked optimistic row to a visible failed state. The pending entry is\n * RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME\n * clientMessageId; the row is marked `meta.sendFailed` (plus the correlated\n * `sendFailureCode`/`sendRetryable` when a `send-rejected` frame supplied them) so\n * the default renderer shows the right reason + retry affordance rather than an\n * indistinguishable \"sent\" bubble. A late ack/echo still reconciles + clears it.\n *\n * `code`/`retryable` are present for a correlated rejection (terminal) or a\n * budget-exhausted server-retryable send; absent for a blind reconnect give-up\n * (which keeps the neutral `send-failed` code).\n */\n private failPendingSend(\n clientMessageId: string,\n code?: ChatSendRejectionCode,\n retryable?: boolean,\n ): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending || pending.failed) return;\n pending.failed = true;\n // A terminal/exhausted send never retries on a timer — cancel any scheduled one.\n this.clearSendRetryTimer(pending);\n this.diag?.(DIAG.SEND_FAILED, {\n clientMessageId,\n code: code ?? 'send-failed',\n retryable: retryable ?? null,\n attempts: pending.attempts,\n serverRetryable: pending.serverRetryable,\n ageMs: this.timers.now() - pending.sentAt,\n });\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId\n ? {\n ...m,\n meta: {\n ...m.meta,\n sendFailed: true,\n ...(code ? { sendFailureCode: code } : {}),\n ...(retryable != null ? { sendRetryable: retryable } : {}),\n },\n }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: code ?? 'send-failed' });\n }\n\n /** Clear a pending send's per-send retry timer (idempotent). */\n private clearSendRetryTimer(pending: PendingSend): void {\n if (pending.retryTimer != null) {\n this.timers.clearTimeout(pending.retryTimer);\n pending.retryTimer = null;\n }\n }\n\n /**\n * Stop tracking a send (acked / reconciled by an echo / history row): cancel its\n * per-send retry timer first so a late reconcile never leaves a stray timer that\n * would resend after the send already landed, then drop it from the map.\n */\n private clearPendingSend(clientMessageId: string): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (pending) this.clearSendRetryTimer(pending);\n this.pendingSends.delete(clientMessageId);\n }\n\n /** One timer for the membership-pending re-send window; a newer frame resets it. */\n private scheduleMembershipPendingRetry(delayMs: number): void {\n if (this.membershipRetryTimer != null) this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = this.timers.setTimeout(() => {\n this.membershipRetryTimer = null;\n if (this.closedByUs) return;\n this.resendPendingSends('membership-pending');\n }, delayMs);\n }\n\n /**\n * Handle a CORRELATED `send-rejected` frame (the target protocol): parse the\n * untrusted payload with the shared schema, correlate to EXACTLY ONE pending send\n * by clientMessageId, and either fail just that bubble (terminal) or schedule a\n * single per-send resend (retryable).\n *\n * A frame that fails validation is NOT simply dropped when it still names one of\n * OUR pending sends: unlike an unknown frame type, an id-correlated rejection is\n * the server explicitly telling us this send did not land — swallowing it would\n * strand the bubble on \"Sending…\" forever (no further frame, no reconnect, no\n * budget check ever runs). A future/unknown rejection code (version skew during a\n * rollout) therefore fails that send CLOSED with the neutral copy + Retry. A\n * malformed frame with no recognizable pending id is dropped (forward-compat\n * noise), never surfaced as an error.\n */\n private applySendRejected(payload: Record<string, unknown>): void {\n const parsed = ChatSendRejectedPayloadSchema.safeParse(payload);\n if (!parsed.success) {\n // Defensive fail-closed: if the unparseable rejection still carries a string\n // id matching one of our un-failed pending sends, that send was rejected by a\n // server speaking a newer/buggy dialect — fail it visibly (neutral code-less\n // \"Couldn't send\" + Retry) instead of stranding it.\n const rawId = (payload as { clientMessageId?: unknown }).clientMessageId;\n if (typeof rawId === 'string' && rawId.length > 0 && rawId.length <= 200) {\n const orphaned = this.pendingSends.get(rawId);\n if (orphaned && !orphaned.failed) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId: rawId, outcome: 'unparseable-fail-closed' });\n this.failPendingSend(rawId);\n return;\n }\n }\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId: null, outcome: 'unparseable-dropped' });\n return;\n }\n const { clientMessageId, code, retryable, retryAfterMs } = parsed.data;\n const pending = this.pendingSends.get(clientMessageId);\n // No matching pending send — already acked/reconciled, or never ours. A late\n // ack/echo (which may have already landed) always wins; nothing to correlate.\n if (!pending || pending.failed) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable, outcome: 'no-pending-send' });\n return;\n }\n\n if (!retryable) {\n // Terminal: an unchanged resend cannot succeed (blocked-word / archived /\n // deleted). Fail ONLY this bubble, recording the reason + terminal class.\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: false, outcome: 'terminal' });\n this.failPendingSend(clientMessageId, code, false);\n return;\n }\n\n // Retryable: server-declared, so switch this send onto the wall-clock budget\n // (the 5-attempt cap no longer applies) and record the reason for a possible\n // budget-exhausted failure.\n pending.serverRetryable = true;\n pending.lastRejectionCode = code;\n // If the budget already elapsed (a long stream of retryable rejections), fail\n // now rather than scheduling yet another doomed resend.\n if (this.timers.now() - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'budget-exhausted' });\n this.failPendingSend(clientMessageId, code, true);\n return;\n }\n const delayMs = this.clampRetryDelay(retryAfterMs);\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'retry-scheduled', delayMs });\n this.scheduleSendRetry(clientMessageId, delayMs);\n }\n\n /**\n * Schedule EXACTLY ONE per-send resend of a correlated retryable rejection (never\n * stacks — a newer rejection clears the prior timer first). When it fires it\n * re-sends the SAME clientMessageId (the DO dedups) unless the wall-clock budget\n * has elapsed, in which case the send fails with its recorded reason.\n */\n private scheduleSendRetry(clientMessageId: string, delayMs: number): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return;\n this.clearSendRetryTimer(pending);\n pending.retryTimer = this.timers.setTimeout(() => {\n pending.retryTimer = null;\n if (this.closedByUs) return;\n const current = this.pendingSends.get(clientMessageId);\n if (!current || current.failed) return;\n if (this.timers.now() - current.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(clientMessageId, current.lastRejectionCode, true);\n return;\n }\n // A server-retryable resend does NOT count toward the attempt cap (that cap is\n // for blind reconnect resends only) — leave `attempts` untouched.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: current.clientMessageId,\n text: current.text,\n });\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId: current.clientMessageId,\n reason: 'scheduled',\n attempts: current.attempts,\n serverRetryable: current.serverRetryable,\n ageMs: this.timers.now() - current.sentAt,\n sent,\n });\n // Budget backstop: the contract says every send gets an ack or a rejection,\n // and either one replaces/clears this timer. If the server goes silent (a\n // contract violation), NOTHING else would ever re-check the budget on a\n // stable socket — so arm a deadline check that fails the send at budget end.\n // This makes SERVER_RETRYABLE_MAX_AGE_MS a hard ceiling, not a best effort.\n const remainingMs =\n SERVER_RETRYABLE_MAX_AGE_MS - (this.timers.now() - current.sentAt) + SEND_RETRY_MIN_DELAY_MS;\n current.retryTimer = this.timers.setTimeout(() => {\n current.retryTimer = null;\n if (this.closedByUs) return;\n const atDeadline = this.pendingSends.get(clientMessageId);\n if (!atDeadline || atDeadline.failed) return;\n if (this.timers.now() - atDeadline.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(clientMessageId, atDeadline.lastRejectionCode, true);\n }\n }, remainingMs);\n }, delayMs);\n }\n\n /** Clamp a server `retryAfterMs` hint into a sane resend delay (defaulting when absent). */\n private clampRetryDelay(retryAfterMs: number | undefined): number {\n const raw =\n typeof retryAfterMs === 'number' && retryAfterMs > 0\n ? retryAfterMs\n : MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;\n return Math.min(Math.max(raw, SEND_RETRY_MIN_DELAY_MS), SEND_RETRY_MAX_DELAY_MS);\n }\n\n /**\n * Flip EVERY un-acked pending send to the visible failed state. Called on the\n * terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send\n * in flight when the socket dies for good shows \"Sending…\" until unmount.\n */\n private failAllPendingSends(): void {\n for (const clientMessageId of [...this.pendingSends.keys()]) {\n this.failPendingSend(clientMessageId);\n }\n }\n\n /**\n * Retry a FAILED (or still-pending) send, RE-USING its original clientMessageId\n * so the DO's send-idempotency dedups against any copy that actually landed.\n * Resets the attempt/age budget, clears the row's `sendFailed` marker (back to\n * the normal pending/optimistic look), and re-sends immediately when the socket\n * is open — otherwise the entry rides the next reconnect's automatic resend.\n * Returns false when the clientMessageId is not a tracked pending send (already\n * acked/reconciled, or never sent from this client).\n */\n retrySend(clientMessageId: string): boolean {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return false;\n // A manual retry supersedes any scheduled server-retryable resend and starts a\n // fresh budget from now; the server re-declares retryability if it rejects again.\n this.clearSendRetryTimer(pending);\n pending.failed = false;\n pending.serverRetryable = false;\n pending.lastRejectionCode = undefined;\n pending.sentAt = this.timers.now();\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n });\n pending.attempts = sent ? 1 : 0;\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId,\n reason: 'manual',\n attempts: pending.attempts,\n serverRetryable: false,\n ageMs: 0,\n sent,\n });\n const next = this.state.messages.map((m) => {\n if (!(m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed)) {\n return m;\n }\n // Back to the normal pending look — drop every failure marker.\n const {\n sendFailed: _sendFailed,\n sendFailureCode: _sendFailureCode,\n sendRetryable: _sendRetryable,\n ...restMeta\n } = m.meta;\n return { ...m, meta: { ...restMeta } };\n });\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n private onError(): void {\n // The close handler does the backoff; onerror is advisory in browsers.\n }\n\n private onClose(code: number, _reason: string): void {\n this.stopHeartbeat();\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed', resumeSeq: this.resumeSeq });\n this.setState({ status: 'closed' });\n return;\n }\n // 4401 AUTH_EXPIRED: re-mint a grant ONCE and reconnect immediately, no backoff.\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant', resumeSeq: this.resumeSeq });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n // 4403 REVOKED: access pulled. Stop — a reconnect would just re-deny at mint.\n // Terminal: flip every un-acked pending send to the visible failed state first\n // (failPendingSend sets lastErrorCode 'send-failed'; the 'revoked' set below\n // wins as the final surfaced code).\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked', resumeSeq: this.resumeSeq });\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect', resumeSeq: this.resumeSeq });\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n resumeSeq: this.resumeSeq,\n });\n if (delay == null) {\n // Reconnect give-up: terminal, same as REVOKED — pending sends must not stay\n // an eternal \"Sending…\"; flip them to the visible failed/retry state.\n this.failAllPendingSends();\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n /**\n * Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).\n * A reconnect would just re-deny at the next mint, so stop for good — the same\n * terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect\n * controller, and flip every un-acked pending send to the visible failed state.\n * `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does\n * not win as the surfaced error; the React hook maps 'access-denied' to\n * `allowed: false` so the shell renders its existing no-access surface.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'access-denied' });\n }\n\n // ---- inbound frames ----\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.type) {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message, 'live');\n case 'ack':\n return this.applyAck(payload as { clientMessageId?: string; seq?: number; readSeq?: number });\n case 'history-page':\n return this.applyHistoryPage((payload as { messages: WireMessageRow[] }).messages ?? []);\n case 'presence':\n return this.setState({ presence: (payload as { online: string[] }).online ?? [] });\n case 'typing':\n return this.applyTyping((payload as { uid: string }).uid);\n case 'snapshot':\n return this.applyChannelSnapshot(payload as unknown as WireChannelSnapshot);\n case 'send-rejected':\n return this.applySendRejected(payload);\n case 'error': {\n const code = String((payload as { code?: unknown }).code ?? 'error');\n // ROLLOUT COMPATIBILITY ONLY (not the target protocol): a Worker that predates\n // the correlated `send-rejected` frame emits an UNCORRELATED\n // error{code:'membership-pending'} with no clientMessageId, so we cannot target\n // one send. Keep the send(s) pending (no visible error — the bubble stays\n // \"Sending…\"), mark every in-flight send server-retryable so the wall-clock\n // budget (NOT the 5-attempt cap) governs give-up, and re-send all after the\n // server-suggested delay. The correlated path below is the target protocol.\n if (code === MEMBERSHIP_PENDING_CODE) {\n const raw = (payload as { retryAfterMs?: unknown }).retryAfterMs;\n const delay = this.clampRetryDelay(typeof raw === 'number' ? raw : undefined);\n this.diag?.(DIAG.ERROR_FRAME, {\n code: MEMBERSHIP_PENDING_CODE,\n correlated: false,\n delayMs: delay,\n pendingSends: this.pendingSends.size,\n });\n for (const pending of this.pendingSends.values()) {\n if (!pending.failed) {\n pending.serverRetryable = true;\n pending.lastRejectionCode = MEMBERSHIP_PENDING_CODE;\n }\n }\n return this.scheduleMembershipPendingRetry(delay);\n }\n this.diag?.(DIAG.ERROR_FRAME, { code: safeDiagnosticLabel(code), correlated: false });\n return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n case 'heartbeat-ack':\n // The DO runtime's hibernation AUTO-RESPONSE to our 20s `heartbeat`. Per\n // Contract C liveness is tracked SERVER-side (the auto-response timestamp\n // the dueWork sweep reads), so the client heartbeat is fire-and-forget: the\n // ack needs no state change and the next tick is already scheduled. Explicit\n // no-op — and deliberately NO diagnostic, not even frame_applied: at ~20s\n // this is the one frame that recurs forever, so logging it would violate the\n // bounded \"one line per DECISION, never per heartbeat\" contract and drown the\n // frame_dropped signal that distinguishes \"never arrived\" from \"dropped\".\n return;\n default:\n // unknown/optional types tolerated (forward compat)\n this.diag?.(DIAG.FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n }\n\n /**\n * Apply a live moderation `revision` frame: record the overlay keyed by seq\n * (max-`messageRevision` wins — a stale/older frame is ignored) and re-render the\n * matching message in place if it is already loaded. If the base row hasn't\n * arrived yet the overlay is retained and re-applied when the row renders, so a\n * frame that races ahead of its message never lets the original content show.\n */\n private applyRevisionFrame(payload: { messageSeq?: number; kind?: RevisionKind; messageRevision?: number }): void {\n const seq = payload.messageSeq;\n const kind = payload.kind;\n const messageRevision = payload.messageRevision;\n if (typeof seq !== 'number' || kind == null || typeof messageRevision !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'revision', reason: 'malformed-payload' });\n return;\n }\n const prev = this.overlays.get(seq);\n // Max-revision merge: a higher messageRevision supersedes; an older one is dropped.\n if (prev && prev.messageRevision >= messageRevision) {\n this.diag?.(DIAG.FRAME_DROPPED, {\n kind: 'revision',\n reason: 'stale-revision',\n seq,\n messageRevision,\n heldRevision: prev.messageRevision,\n });\n return;\n }\n this.overlays.set(seq, { kind, messageRevision });\n // Re-render the matching message in place if it is already present. Re-derive\n // from the ORIGINAL row so a `restore` reverts a previously-blanked text.\n const messageId = seqToMessageId(seq);\n const original = this.originalRows.get(seq);\n const rendered = Boolean(original) && this.state.messages.some((m) => m.messageId === messageId);\n this.diag?.(DIAG.REVISION_APPLIED, {\n seq,\n kind,\n messageRevision,\n // false = overlay retained for a row not loaded yet (re-applied when it renders).\n rendered,\n });\n if (rendered && original) {\n const overlaid = applyModerationOverlay(original, this.effectiveOverlay(seq, null));\n const next = this.state.messages.map((m) => (m.messageId === messageId ? overlaid : m));\n this.setState({ messages: next });\n }\n }\n\n /**\n * The effective overlay for a seq = the max-`messageRevision` between the row's\n * own inline overlay (from the DO history/delta merge) and any received `revision`\n * frame stored in `this.overlays`. Either source may be ahead of the other.\n */\n private effectiveOverlay(seq: number, rowOverlay: ModerationOverlay | null): ModerationOverlay | null {\n const fromFrame = this.overlays.get(seq) ?? null;\n if (!rowOverlay) return fromFrame;\n if (!fromFrame) return rowOverlay;\n return fromFrame.messageRevision >= rowOverlay.messageRevision ? fromFrame : rowOverlay;\n }\n\n /** Map a DO row to a UI message with its effective moderation overlay applied. */\n private renderRow(row: WireMessageRow): ChatMessageV1 {\n const mapped = wireRowToMessage(row, this.config.threadId);\n // Cache the ORIGINAL so a later `restore` frame can revert a blanked message.\n this.originalRows.set(row.seq, mapped);\n return applyModerationOverlay(mapped, this.effectiveOverlay(row.seq, overlayFromRow(row)));\n }\n\n /**\n * Apply the Contract C resume snapshot. The DO always returns the authoritative\n * tail + read cursor and, per the pinned contract, EITHER a contiguous delta\n * (`resync:false`) OR a resync directive (`resync:true`).\n *\n * - `resync === false`: apply the contiguous `delta` (the ≤500-message backlog\n * after our cursor, oldest-first) through the same render/merge path as a live\n * `message`, then advance `resumeSeq` to `lastMessageSeq`. This heals ANY gap\n * within the backlog — including moderation `revision`s the DO baked into the\n * delta rows — with no history fetch, so a >50-message reconnect gap is no\n * longer a permanent hole.\n * - `resync === true`: the gap is beyond the backlog (or we sent no cursor). Drop\n * the local tail (messages + overlays + original-row cache) and re-page history\n * from the top via a cursorless `requestHistory(null)`.\n *\n * A pre-contract DO (no `resync`/`delta`) is treated as `resync:true` ⇒ the safe\n * re-page path, so a stale worker never strands the client. An empty delta with\n * `resync:false` (already caught up) is a no-op beyond advancing the cursor.\n */\n private applyChannelSnapshot(snap: WireChannelSnapshot): void {\n // Guard: an inbox snapshot must never reach a channel client.\n if (isInboxSnapshot(snap as never)) {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'snapshot', reason: 'inbox-snapshot-on-channel' });\n return;\n }\n if (typeof snap.lastMessageSeq !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'snapshot', reason: 'missing-last-message-seq' });\n return;\n }\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\n const cursorBefore = this.resumeSeq;\n const delta = Array.isArray(snap.delta) ? snap.delta : [];\n\n if (!resync) {\n // Apply the contiguous delta oldest-first, exactly like live messages, then\n // advance the cursor to the authoritative tail so the NEXT resume tells the DO\n // we're caught up only after we actually applied the backlog.\n for (const row of delta) this.applyMessage(row, 'delta');\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\n this.diag?.(DIAG.RESUME_RESULT, {\n lastMessageSeq: snap.lastMessageSeq,\n resync: false,\n deltaCount: delta.length,\n cursorBefore,\n cursorAfter: this.resumeSeq,\n readSeq: typeof snap.readSeq === 'number' ? snap.readSeq : null,\n });\n // A non-resync snapshot IS the first authoritative data (even an empty delta =\n // caught up) — end initial loading. A resync snapshot instead defers to the\n // re-paged first history page below, so it must NOT end it here.\n if (!this.state.hasLoadedInitialData) this.setState({ hasLoadedInitialData: true });\n return;\n }\n\n // Resync: drop the local tail and re-page from the top per the pinned contract.\n // Clear the derived caches too so a stale overlay/original-row can't resurrect a\n // message the re-page will reload fresh (with its baked-in moderation state).\n // KEEP un-acked optimistic rows — they aren't server history yet; their pending\n // sends were already re-sent in onOpen and reconcile when the DO echoes them.\n this.overlays.clear();\n this.originalRows.clear();\n this.resumeSeq = 0;\n const priorCount = this.state.messages.length;\n const keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.diag?.(DIAG.RESUME_RESULT, {\n lastMessageSeq: snap.lastMessageSeq,\n resync: true,\n deltaCount: delta.length,\n cursorBefore,\n cursorAfter: 0,\n readSeq: typeof snap.readSeq === 'number' ? snap.readSeq : null,\n });\n this.diag?.(DIAG.RESYNC_DROPPED_TAIL, {\n // The client REMOVED rows from the UI here — the counterpart of an upsert.\n removedCount: priorCount - keptOptimistic.length,\n keptOptimistic: keptOptimistic.length,\n });\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n /**\n * Apply one server row (a live `message` frame or a resume-delta row) and record\n * the decision the client made about it.\n *\n * Diagnostics bounding: a LIVE frame always emits one `chat_client_frame_applied`\n * line; a DELTA row emits one only when it is materially interesting (carries a\n * moderation kind, or reconciles one of our optimistic sends) — a 500-row backlog\n * is otherwise summarized by `chat_client_resume_result` rather than spamming 500\n * lines.\n */\n private applyMessage(row: WireMessageRow, source: 'live' | 'delta'): void {\n if (!row || typeof row.seq !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'message', source, reason: 'missing-seq' });\n return;\n }\n // Read the pre-merge view ONLY when diagnostics are on (these are O(n) scans).\n const prior = this.diag ? this.priorRowFacts(row) : null;\n const cursorBefore = this.resumeSeq;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\n if (!this.diag || !prior) return;\n\n const notable = row.moderationKind != null || prior.reconciledOptimistic;\n if (source === 'delta' && !notable) return;\n this.diag(DIAG.FRAME_APPLIED, {\n kind: 'message',\n source,\n seq: row.seq,\n hasClientMessageId: Boolean(row.clientMessageId),\n moderationKind: row.moderationKind ?? null,\n op: prior.reconciledOptimistic ? 'optimistic-reconcile' : prior.hadRow ? 'replace-by-seq' : 'insert',\n cursorBefore,\n cursorAfter: this.resumeSeq,\n });\n }\n\n /**\n * The pre-merge view of a row's seq: whether we already hold that seq, and whether\n * this row reconciles a live optimistic send.\n * Diagnostics-only — never called when diagnostics are off.\n */\n private priorRowFacts(row: WireMessageRow): {\n hadRow: boolean;\n reconciledOptimistic: boolean;\n } {\n const messageId = seqToMessageId(row.seq);\n const existing = this.state.messages.find((m) => m.messageId === messageId);\n return {\n hadRow: existing != null,\n reconciledOptimistic: Boolean(\n row.clientMessageId &&\n this.state.messages.some((m) => m.meta?.optimistic && m.meta?.clientMessageId === row.clientMessageId),\n ),\n };\n }\n\n /**\n * Merge a server message: reconcile an optimistic echo by clientMessageId (swap\n * the `optimistic:` row for the canonical seq row), or insert/replace by seq.\n * Keeps the list sorted ascending by seq with optimistic rows at the tail.\n */\n private mergeMessage(mapped: ChatMessageV1, row: WireMessageRow): void {\n // If this canonical row carries a clientMessageId we still have pending (the\n // sender's own broadcast, or a resume delta that included our send), the send\n // landed — stop tracking it for reconnect resend (and cancel any retry timer).\n if (row.clientMessageId) this.clearPendingSend(row.clientMessageId);\n const next = this.state.messages.filter(\n (m) =>\n m.messageId !== mapped.messageId &&\n // drop the optimistic placeholder this server row reconciles\n (m.meta?.clientMessageId !== row.clientMessageId || !m.meta?.optimistic),\n );\n next.push(mapped);\n next.sort(sortBySeq);\n this.setState({ messages: next });\n }\n\n private applyAck(payload: { clientMessageId?: string; seq?: number; readSeq?: number }): void {\n // A read-ack echo carries readSeq only — nothing to reconcile locally.\n if (payload.clientMessageId == null || typeof payload.seq !== 'number') return;\n // Reconcile the optimistic echo: stamp the server seq onto it. The canonical\n // `message` broadcast (which the sender also receives) replaces it fully; this\n // ack guarantees the seq even if the broadcast is dropped/reordered.\n const seq = payload.seq;\n const cmid = payload.clientMessageId;\n this.diag?.(DIAG.SEND_ACK, {\n clientMessageId: cmid,\n seq,\n cursorBefore: this.resumeSeq,\n cursorAfter: seq > this.resumeSeq ? seq : this.resumeSeq,\n wasPending: this.pendingSends.has(cmid),\n });\n // The send is acked — stop tracking it for reconnect resend (and cancel any\n // per-send retry timer so a scheduled resend never fires after it landed).\n this.clearPendingSend(cmid);\n const next = this.state.messages.map((m) => {\n if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid) return m;\n // Strip any failed markers — a late ack (e.g. a retry raced the cap, or a\n // correlated rejection was superseded) means the send actually landed, so the\n // row must render as delivered, not failed.\n const {\n sendFailed: _sendFailed,\n sendFailureCode: _sendFailureCode,\n sendRetryable: _sendRetryable,\n ...restMeta\n } = m.meta;\n return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };\n });\n next.sort(sortBySeq);\n if (seq > this.resumeSeq) this.resumeSeq = seq;\n this.setState({ messages: next });\n }\n\n private applyHistoryPage(rows: WireMessageRow[]): void {\n const cursorBefore = this.resumeSeq;\n const mapped = rows.map((r) => this.renderRow(r));\n // Mirror mergeMessage's optimistic reconciliation: a history row carrying a\n // clientMessageId we still hold as an optimistic placeholder IS that send (the\n // DO stored it; the ack was lost). Drop the placeholder — keyed `optimistic:{cmid}`,\n // it would otherwise survive alongside its seq-keyed server row as a duplicate\n // bubble after a resync + re-page — and stop tracking it for resend.\n const incomingCmids = new Set<string>();\n for (const r of rows) {\n if (r.clientMessageId) {\n incomingCmids.add(r.clientMessageId);\n this.clearPendingSend(r.clientMessageId);\n }\n }\n const bySeq = new Map<string, ChatMessageV1>();\n for (const m of this.state.messages) {\n if (m.meta?.optimistic && typeof m.meta?.clientMessageId === 'string' && incomingCmids.has(m.meta.clientMessageId)) {\n continue; // reconciled by the incoming server row\n }\n bySeq.set(m.messageId, m);\n }\n for (const m of mapped) bySeq.set(m.messageId, m);\n const merged = [...bySeq.values()].sort(sortBySeq);\n for (const r of rows) if (r.seq > this.resumeSeq) this.resumeSeq = r.seq;\n this.setState({\n messages: merged,\n isFetchingOlder: false,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n // The first history page — INCLUDING an empty one — is a successful authoritative\n // load, so it ends initial loading. (Older-page fetches redundantly re-set true,\n // which is a harmless no-op since the flag never flips back to false.)\n hasLoadedInitialData: true,\n });\n if (!this.diag) return;\n const seqs = rows.map((r) => r.seq).filter((s): s is number => typeof s === 'number');\n this.diag(DIAG.HISTORY_PAGE, {\n count: rows.length,\n minSeq: seqs.length > 0 ? Math.min(...seqs) : null,\n maxSeq: seqs.length > 0 ? Math.max(...seqs) : null,\n cursorBefore,\n cursorAfter: this.resumeSeq,\n reconciledOptimistic: incomingCmids.size,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n total: merged.length,\n });\n }\n\n private applyTyping(uid: string): void {\n if (!uid || uid === this.config.currentUserId) return;\n if (!this.state.typing.includes(uid)) this.setState({ typing: [...this.state.typing, uid] });\n const prev = this.typingTimers.get(uid);\n if (prev) this.timers.clearTimeout(prev);\n this.typingTimers.set(\n uid,\n this.timers.setTimeout(() => {\n this.typingTimers.delete(uid);\n this.setState({ typing: this.state.typing.filter((u) => u !== uid) });\n }, TYPING_COALESCE_MS * 2),\n );\n }\n\n // ---- outbound actions ----\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /**\n * Optimistic send: render the echo immediately keyed by clientMessageId, then\n * emit the `send` frame. The server `ack`/`message` reconciles on the seq.\n */\n send(args: { clientMessageId: string; text: string }): boolean {\n // C-B8: write the frame FIRST and render the optimistic echo ONLY if it was\n // actually sent over an open socket. A closed socket returns false — no phantom\n // \"sent\" bubble, and the caller keeps the composer text — so a disconnect between\n // render and click can't silently swallow the message.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n });\n if (!sent) {\n this.diag?.(DIAG.SEND_FAILED, {\n clientMessageId: args.clientMessageId,\n code: 'send-failed',\n retryable: null,\n attempts: 0,\n reason: 'socket-not-open',\n });\n this.setState({ lastErrorCode: 'send-failed' });\n return false;\n }\n const now = this.timers.now();\n // Track the un-acked send so a reconnect re-sends it with the same\n // clientMessageId (the DO dedups) — until the ack/echo clears it.\n this.pendingSends.set(args.clientMessageId, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n sentAt: now,\n attempts: 1,\n failed: false,\n serverRetryable: false,\n retryTimer: null,\n });\n const echo = optimisticMessage({\n clientMessageId: args.clientMessageId,\n threadId: this.config.threadId,\n senderId: this.config.currentUserId,\n text: args.text,\n createdAt: now,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\n this.diag?.(DIAG.SEND_OPTIMISTIC, {\n clientMessageId: args.clientMessageId,\n pendingSends: this.pendingSends.size,\n });\n return true;\n }\n\n /**\n * Explicit read ack (delivery != read). `focused` => the channel is open + at latest.\n * Remembers the cursor (re-sent on reconnect) and returns whether the frame was\n * actually sent over an open socket, so the caller only advances its local\n * already-acked marker on a real send (M2).\n */\n readAck(readSeq: number, focused: boolean): boolean {\n this.lastReadAck = { readSeq, focused };\n return this.sendFrame(CLIENT_FRAME.READ_ACK, { readSeq, focused });\n }\n\n /** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */\n typing(): void {\n const now = this.timers.now();\n if (now - this.lastTypingSentAt < TYPING_COALESCE_MS) return;\n this.lastTypingSentAt = now;\n this.sendFrame(CLIENT_FRAME.TYPING, {});\n }\n\n presenceSubscribe(): void {\n this.presenceSubscribed = true;\n this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n }\n\n presenceUnsubscribe(): void {\n this.presenceSubscribed = false;\n this.sendFrame(CLIENT_FRAME.PRESENCE_UNSUBSCRIBE, {});\n this.setState({ presence: [] });\n }\n\n /** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */\n fetchOlder(): void {\n if (this.state.isFetchingOlder || !this.state.hasOlder) return;\n const oldest = this.state.messages.find((m) => typeof m.meta?.seq === 'number');\n const beforeSeq = oldest && typeof oldest.meta?.seq === 'number' ? (oldest.meta.seq as number) : null;\n this.setState({ isFetchingOlder: true });\n this.requestHistory(beforeSeq);\n }\n\n private requestHistory(beforeSeq: number | null): void {\n const sent = this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\n this.diag?.(DIAG.HISTORY_REQUEST, { beforeSeq, limit: HISTORY_PAGE_MAX, sent });\n }\n\n // ---- heartbeat (20s) ----\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const tick = () => {\n if (!this.socket || !this.socket.isOpen) return;\n this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n };\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer != null) {\n this.timers.clearTimeout(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n // ---- teardown ----\n\n /** Permanently close this client (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n this.stopHeartbeat();\n for (const t of this.typingTimers.values()) this.timers.clearTimeout(t);\n this.typingTimers.clear();\n this.overlays.clear();\n this.originalRows.clear();\n // Cancel every per-send retry timer before dropping the tracking map.\n for (const pending of this.pendingSends.values()) this.clearSendRetryTimer(pending);\n this.pendingSends.clear();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.membershipRetryTimer != null) {\n this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed', typing: [], presence: [] });\n }\n}\n\nfunction sortBySeq(a: ChatMessageV1, b: ChatMessageV1): number {\n const sa = typeof a.meta?.seq === 'number' ? (a.meta.seq as number) : Number.POSITIVE_INFINITY;\n const sb = typeof b.meta?.seq === 'number' ? (b.meta.seq as number) : Number.POSITIVE_INFINITY;\n if (sa !== sb) return sa - sb;\n return a.createdAt - b.createdAt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"channel-client.js","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Channel realtime client: ONE WebSocket to a single channel DO, driving the\n// Contract C wire protocol. Framework-neutral (a plain observable store the React\n// hook subscribes to) so it is unit-testable with a mock socket and no React.\n//\n// Responsibilities (Contract C):\n// - connect via subprotocol auth (grant offered by the injected grantProvider)\n// - receive versioned `message` frames; server owns `seq`\n// - optimistic send keyed by clientMessageId, reconciled on the server `ack` seq\n// - explicit read acks (delivery != read; focus signals \"read\")\n// - typing (>= 2s coalesced per the V1 limit)\n// - presence-subscribe/-unsubscribe + a 20s client heartbeat\n// - reconnect (realtime-core controller) with a resume cursor -> snapshot then deltas\n// - history pagination over the socket (page <= 50, epoch-aware via seq)\n// - error codes incl. 4401 AUTH_EXPIRED -> re-mint a grant ONCE via grantProvider\n//\n// Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n ChatSendRejectedPayloadSchema,\n type WireMessageRow,\n type WireChannelSnapshot,\n type RevisionKind,\n type ChatSendRejectionCode,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport {\n wireRowToMessage,\n optimisticMessage,\n seqToMessageId,\n applyModerationOverlay,\n overlayFromRow,\n type ModerationOverlay,\n} from './map.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n isTerminalErrorCode,\n TERMINAL_ERROR_CODE,\n HEARTBEAT_MS,\n TYPING_COALESCE_MS,\n HISTORY_PAGE_MAX,\n MAX_PENDING_SEND_ATTEMPTS,\n PENDING_SEND_MAX_AGE_MS,\n SERVER_RETRYABLE_MAX_AGE_MS,\n SEND_RETRY_MIN_DELAY_MS,\n SEND_RETRY_MAX_DELAY_MS,\n} from './shared.js';\n\n/** An un-acked send tracked for reconnect resend (clientMessageId idempotency). */\ninterface PendingSend {\n clientMessageId: string;\n text: string;\n /** When the send was first attempted (for the age/budget caps). Reset by retrySend(). */\n sentAt: number;\n /** How many times it has been (re)sent over a socket. Reset by retrySend(). */\n attempts: number;\n /** True once the attempt/age/budget cap flipped this send to the visible failed\n * state. A failed entry is RETAINED (not deleted) so retrySend() can re-queue it\n * with the SAME clientMessageId; it is skipped by the automatic reconnect resend. */\n failed: boolean;\n /**\n * Set true once the Worker has declared THIS send retryable — either a correlated\n * `send-rejected` (retryable) frame or the legacy uncorrelated\n * `error{code:'membership-pending'}`. A server-retryable send is governed by the\n * {@link SERVER_RETRYABLE_MAX_AGE_MS} wall-clock budget and is NEVER failed by the\n * generic {@link MAX_PENDING_SEND_ATTEMPTS} cap (that cap stays for blind reconnect\n * resends of sends the server has said nothing about).\n */\n serverRetryable: boolean;\n /** The most recent server rejection code, recorded so a budget-exhausted failure\n * can surface the reason (and keep a retry affordance for the transient class). */\n lastRejectionCode?: ChatSendRejectionCode;\n /** The single per-send resend timer for a correlated retryable rejection (never\n * stacks — a newer rejection clears it first; cleared on ack/echo/manual\n * retry/terminal/close/dispose). */\n retryTimer: ReturnType<TransportTimers['setTimeout']> | null;\n}\n\nexport interface ChannelClientState {\n status: RealtimeStatus;\n /** Ordered ascending by seq; optimistic messages (no seq yet) sort to the tail. */\n messages: ChatMessageV1[];\n /** uids currently typing (cleared after ~TYPING_COALESCE_MS of silence). */\n typing: string[];\n /** uids the DO reports online (presence). Empty until presence-subscribe. */\n presence: string[];\n /** True while an older-history page request is in flight. */\n isFetchingOlder: boolean;\n /** False once a history page returns fewer than the requested limit (reached the head). */\n hasOlder: boolean;\n /** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */\n lastErrorCode: string | null;\n /**\n * True once the FIRST authoritative chat data has been applied — a non-resync\n * resume snapshot, OR the first history page (INCLUDING an empty one). Drives the\n * hook's `isInitialLoading` so the shell shows an honest \"Opening chat…\" state\n * while the first data is loading, and a real empty-chat surface only after an\n * authoritative empty result. NEVER inferred from `messages.length === 0` (an\n * empty first history page IS a successful load). Once true it stays true — a\n * later disconnect/reconnect keeps loaded messages mounted, not a re-loader.\n * A grant retry / socket open before any snapshot leaves it false.\n */\n hasLoadedInitialData: boolean;\n}\n\nexport interface ChannelClientConfig {\n /** WebSocket endpoint base, e.g. `wss://chat.ttt.productions`. The client appends `/channel`. */\n endpoint: string;\n /** The logical thread id the UI keys on (used as ChatMessageV1.threadId). */\n threadId: string;\n /** The authenticated uid (sender identity for optimistic echoes + auth-switch guard). */\n currentUserId: string;\n /** Mints/refreshes a channel grant. Called on connect and once on a 4401 re-grant. */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n /** Reconnect backoff tuning (forwarded to realtime-core). */\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics (default OFF — absent/`false` changes\n * nothing and costs one nullish check per decision point). `true` emits one\n * `console.debug` line per decision with a stable `chat_client_*` event name\n * and a compact, content-free JSON payload; a function routes those entries\n * into the app's own logger instead. Never logs message text, captions,\n * filenames, URLs, tokens, or previews — ids, seqs, states, counts, and codes\n * only. See `./diagnostics.ts`.\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: ChannelClientState = {\n status: 'idle',\n messages: [],\n typing: [],\n presence: [],\n isFetchingOlder: false,\n hasOlder: true,\n lastErrorCode: null,\n hasLoadedInitialData: false,\n};\n\n/**\n * Retryable error-frame code the channel DO sends when a send arrives BEFORE the\n * member row has synced through the async backend→worker pipeline (a just-created\n * invite/channel). The socket stays open; the client re-sends after `retryAfterMs`\n * through the normal resendPendingSends machinery (same caps). The string is part\n * of the wire contract with the worker runtime's send handler.\n */\nconst MEMBERSHIP_PENDING_CODE = 'membership-pending';\n/** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */\nconst MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;\n\n/**\n * Dead-socket watchdog: how long inbound silence may last before this client\n * treats the socket as dead and closes it locally.\n *\n * CHANNEL-ONLY, and this is why it is sound here: the channel DO answers every\n * 20 s client `heartbeat` through the hibernation AUTO-RESPONSE API, so a healthy\n * channel socket is NEVER silent for a full heartbeat window. Inbound silence past\n * this threshold is therefore provable death (a half-open TCP connection whose\n * `onclose` never fires), not idleness. The INBOX socket has no such auto-response\n * and a healthy idle inbox is legitimately silent for hours — applying this rule\n * there would endlessly reconnect healthy sockets, so it is deliberately absent\n * from `inbox-client.ts`.\n *\n * PRIVATE CLIENT POLICY, not a wire constant: nothing on the worker side agrees to\n * it, so it does not belong in `@ttt-productions/chat-schemas` or in this package's\n * exported surface. The check rides the existing 20 s heartbeat tick (no second\n * timer), so detection lands on the NEXT tick after the deadline rather than at\n * exactly 50 s — intended: 2.5 missed windows is unambiguous, and it costs no\n * extra timer.\n */\nconst INBOUND_SILENCE_LIMIT_MS = 50_000;\n\n/**\n * Close code used for the watchdog's LOCAL close. A browser `WebSocket.close()`\n * accepts only 1000 or 3000–4999, and the private 4xxx range is the chat wire's\n * (`CHAT_CLOSE_CODES`) — a client-invented code there could collide with a future\n * server code. 1000 keeps the close frame valid and, being neither 4401 nor 4403,\n * routes through `onClose`'s ordinary transient path, which owns reconnect,\n * backoff, and resume. The reason string + the liveness diagnostic are what\n * identify it.\n */\nconst WATCHDOG_CLOSE_CODE = 1000;\n\nexport class ChannelClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: ChannelClientState = INITIAL;\n private readonly listeners = new Set<(s: ChannelClientState) => void>();\n\n /** Resume cursor: highest seq we've durably applied. Sent on resume after reconnect. */\n private resumeSeq = 0;\n /** Last read-ack the UI requested; re-sent on reconnect so a drop during a closed\n * socket window is recovered (M2). */\n private lastReadAck: { readSeq: number; focused: boolean } | null = null;\n /** True after a `4401` re-grant has been attempted for the CURRENT connect cycle (once-only). */\n private reauthAttempted = false;\n private presenceSubscribed = false;\n private heartbeatTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n /**\n * When the CURRENT socket last proved it was alive: set on open, refreshed by\n * every successfully parsed inbound frame (`heartbeat-ack` included — that IS\n * the DO's liveness answer). Null whenever no socket is live, so the watchdog\n * can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.\n */\n private lastInboundAt: number | null = null;\n private typingTimers = new Map<string, ReturnType<TransportTimers['setTimeout']>>();\n private lastTypingSentAt = Number.NEGATIVE_INFINITY;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n /** Single scheduled re-send for the membership-pending window (never stacks). */\n private membershipRetryTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /**\n * Per-seq moderation overlay (the max-revision wins). A `revision` frame writes\n * here keyed by `messageSeq` so it applies whether it arrives BEFORE its base\n * row (re-applied when the row later renders) or AFTER (re-renders the present\n * row in place). Merged with the row's own inline `moderationKind` so a row that\n * arrives already-moderated from the DO AND one covered by a live frame both\n * render the moderated state. */\n private overlays = new Map<number, ModerationOverlay>();\n /**\n * The ORIGINAL (un-overlaid) mapped message per seq. A `restore` revision must\n * revert a previously-redacted message to its original text, so re-renders start\n * from this cache rather than the (possibly already-blanked) message in state. */\n private originalRows = new Map<number, ChatMessageV1>();\n /**\n * Un-acked sends keyed by clientMessageId. A send is added here when its frame is\n * written and removed when its `ack` (or the canonical `message` echo carrying the\n * same clientMessageId) arrives. On every reconnect resume they are RE-SENT with\n * the SAME clientMessageId — the DO dedups (re-acks without re-broadcast), so a\n * send that lost its socket before the ack is delivered exactly once. A pending\n * send that exceeds the attempt/age cap is flipped to a visible failed state\n * (retry affordance) instead of staying a ghost. */\n private pendingSends = new Map<string, PendingSend>();\n /**\n * The structured diagnostics emitter, or null when diagnostics are off (the\n * default). Every call site uses `this.diag?.(...)` so the off path constructs\n * no payload at all.\n */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n // ---- observable store ----\n\n getState(): ChannelClientState {\n return this.state;\n }\n\n subscribe(fn: (s: ChannelClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<ChannelClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n // ---- connection lifecycle ----\n\n async connect(): Promise<void> {\n // Idempotency guard (C-B7): connect() only starts a lifecycle from a fresh\n // (idle) or fully torn-down (closed) state. A second connect() while one is\n // already connecting / open / reconnecting is a no-op, so a stray double-owner\n // can't open a second WebSocket. The reconnect path uses openSocket() /\n // scheduleReconnect() directly (never connect()), so this never blocks a\n // legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n // REVIVE the controller. `controller.close()` is PERMANENT: while it is in\n // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`\n // returns null. So without this reset a close() → connect() client opens a\n // socket and works normally until its FIRST transient close, at which point\n // `scheduleReconnect()` gets null and parks it terminally in `closed` with\n // reconnect disabled and no surfaced error code. The idempotency guard above\n // is what keeps this from disturbing a live lifecycle: only an `idle` or\n // fully torn-down (`closed`) client ever reaches here.\n this.controller.reset();\n // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN\n // its lifecycle; an explicit connect() is a fresh attempt the authority may\n // legitimately re-deny, so the previous lifecycle's code must not persist into\n // this one (the hook maps `access-denied` to `allowed: false`). Loaded\n // messages and the initial-load flag are deliberately KEPT — a lifecycle\n // restart must not churn the UI back to an empty opening state.\n if (isTerminalErrorCode(this.state.lastErrorCode)) this.setState({ lastErrorCode: null });\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n resumeSeq: this.resumeSeq,\n pendingSends: this.pendingSends.size,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app's grant provider translated an\n // authoritative policy \"no\" into the package-owned ChatAccessDeniedError)\n // must NOT reconnect — the next mint would just re-deny, stranding the shell\n // on the opening loader forever. Every other mint failure (network, transient\n // 'unavailable'/'internal', post-login 'unauthenticated') stays retryable and\n // backs off exactly as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n // Socket REPLACEMENT: the previous socket's liveness record dies with it, so\n // the watchdog never judges a new socket against an old deadline.\n this.lastInboundAt = null;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/channel`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => this.onError(),\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n // Start the liveness clock at open — the first heartbeat window is measured\n // from here, not from a stale previous socket.\n this.lastInboundAt = this.timers.now();\n this.setState({ status: 'open' });\n this.diag?.(DIAG.SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n resumeSeq: this.resumeSeq,\n pendingSends: this.pendingSends.size,\n presenceSubscribed: this.presenceSubscribed,\n });\n // Resume: ask the DO for the authoritative snapshot, then live deltas flow. The\n // field name MUST be `afterSeq` — the Channel DO reads `payload.afterSeq`; any other\n // name reads as cursorless and forces a full resync every reconnect.\n this.sendFrame(CLIENT_FRAME.RESUME, { afterSeq: this.resumeSeq });\n this.diag?.(DIAG.RESUME_REQUEST, { afterSeq: this.resumeSeq, attempt: this.connectAttempt });\n // If the UI had presence open, re-subscribe after a reconnect.\n if (this.presenceSubscribed) this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n // Re-send the last read cursor so a read-ack dropped during the disconnect window\n // (sendFrame returns false on a closed socket) is not lost until the next message.\n if (this.lastReadAck) this.sendFrame(CLIENT_FRAME.READ_ACK, { ...this.lastReadAck });\n // Re-send every un-acked pending send with its ORIGINAL clientMessageId so a send\n // that lost its socket before the ack is not silently dropped. The DO dedups\n // (re-acks, no re-broadcast, no flood-token) — this is the client half of the\n // Contract C send-idempotency the server side was built for.\n this.resendPendingSends('reconnect');\n this.startHeartbeat();\n }\n\n /**\n * On reconnect, re-send each un-acked pending send with the SAME clientMessageId\n * (the DO dedups). A send that has exceeded the attempt or age cap is flipped to a\n * visible failed state instead of being resent forever — the ghost becomes a\n * retryable failure the UI can surface.\n */\n private resendPendingSends(reason: 'reconnect' | 'membership-pending'): void {\n if (this.pendingSends.size === 0) return;\n const now = this.timers.now();\n for (const pending of [...this.pendingSends.values()]) {\n // Already flipped to the visible failed state — only an explicit retrySend()\n // re-queues it; the automatic reconnect resend leaves it alone.\n if (pending.failed) continue;\n if (pending.serverRetryable) {\n // The Worker declared this send retryable — give-up is the wall-clock budget\n // ONLY (the 5-attempt cap must not fail a server-declared transient after\n // ~10 s while the backend projection window is up to 60+ s).\n if (now - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(pending.clientMessageId, pending.lastRejectionCode, true);\n continue;\n }\n // This reconnect resend supersedes any per-send timer scheduled before the\n // socket dropped — clear it so the send doesn't go out twice (the DO would\n // dedup, but the duplicate frame is pure waste). A fresh rejection frame\n // re-schedules as usual.\n this.clearSendRetryTimer(pending);\n } else {\n // A blind reconnect resend of a send the server has said nothing about: the\n // generic attempt/age caps still protect against a permanent ghost.\n const tooOld = now - pending.sentAt >= PENDING_SEND_MAX_AGE_MS;\n const tooManyAttempts = pending.attempts >= MAX_PENDING_SEND_ATTEMPTS;\n if (tooOld || tooManyAttempts) {\n this.failPendingSend(pending.clientMessageId);\n continue;\n }\n }\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n });\n if (sent) pending.attempts += 1;\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId: pending.clientMessageId,\n reason,\n attempts: pending.attempts,\n serverRetryable: pending.serverRetryable,\n ageMs: now - pending.sentAt,\n sent,\n });\n }\n }\n\n /**\n * Flip an un-acked optimistic row to a visible failed state. The pending entry is\n * RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME\n * clientMessageId; the row is marked `meta.sendFailed` (plus the correlated\n * `sendFailureCode`/`sendRetryable` when a `send-rejected` frame supplied them) so\n * the default renderer shows the right reason + retry affordance rather than an\n * indistinguishable \"sent\" bubble. A late ack/echo still reconciles + clears it.\n *\n * `code`/`retryable` are present for a correlated rejection (terminal) or a\n * budget-exhausted server-retryable send; absent for a blind reconnect give-up\n * (which keeps the neutral `send-failed` code).\n */\n private failPendingSend(\n clientMessageId: string,\n code?: ChatSendRejectionCode,\n retryable?: boolean,\n ): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending || pending.failed) return;\n pending.failed = true;\n // A terminal/exhausted send never retries on a timer — cancel any scheduled one.\n this.clearSendRetryTimer(pending);\n this.diag?.(DIAG.SEND_FAILED, {\n clientMessageId,\n code: code ?? 'send-failed',\n retryable: retryable ?? null,\n attempts: pending.attempts,\n serverRetryable: pending.serverRetryable,\n ageMs: this.timers.now() - pending.sentAt,\n });\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId\n ? {\n ...m,\n meta: {\n ...m.meta,\n sendFailed: true,\n ...(code ? { sendFailureCode: code } : {}),\n ...(retryable != null ? { sendRetryable: retryable } : {}),\n },\n }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: code ?? 'send-failed' });\n }\n\n /** Clear a pending send's per-send retry timer (idempotent). */\n private clearSendRetryTimer(pending: PendingSend): void {\n if (pending.retryTimer != null) {\n this.timers.clearTimeout(pending.retryTimer);\n pending.retryTimer = null;\n }\n }\n\n /**\n * Stop tracking a send (acked / reconciled by an echo / history row): cancel its\n * per-send retry timer first so a late reconcile never leaves a stray timer that\n * would resend after the send already landed, then drop it from the map.\n */\n private clearPendingSend(clientMessageId: string): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (pending) this.clearSendRetryTimer(pending);\n this.pendingSends.delete(clientMessageId);\n }\n\n /** One timer for the membership-pending re-send window; a newer frame resets it. */\n private scheduleMembershipPendingRetry(delayMs: number): void {\n if (this.membershipRetryTimer != null) this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = this.timers.setTimeout(() => {\n this.membershipRetryTimer = null;\n if (this.closedByUs) return;\n this.resendPendingSends('membership-pending');\n }, delayMs);\n }\n\n /**\n * Handle a CORRELATED `send-rejected` frame (the target protocol): parse the\n * untrusted payload with the shared schema, correlate to EXACTLY ONE pending send\n * by clientMessageId, and either fail just that bubble (terminal) or schedule a\n * single per-send resend (retryable).\n *\n * A frame that fails validation is NOT simply dropped when it still names one of\n * OUR pending sends: unlike an unknown frame type, an id-correlated rejection is\n * the server explicitly telling us this send did not land — swallowing it would\n * strand the bubble on \"Sending…\" forever (no further frame, no reconnect, no\n * budget check ever runs). A future/unknown rejection code (version skew during a\n * rollout) therefore fails that send CLOSED with the neutral copy + Retry. A\n * malformed frame with no recognizable pending id is dropped (forward-compat\n * noise), never surfaced as an error.\n */\n private applySendRejected(payload: Record<string, unknown>): void {\n const parsed = ChatSendRejectedPayloadSchema.safeParse(payload);\n if (!parsed.success) {\n // Defensive fail-closed: if the unparseable rejection still carries a string\n // id matching one of our un-failed pending sends, that send was rejected by a\n // server speaking a newer/buggy dialect — fail it visibly (neutral code-less\n // \"Couldn't send\" + Retry) instead of stranding it.\n const rawId = (payload as { clientMessageId?: unknown }).clientMessageId;\n if (typeof rawId === 'string' && rawId.length > 0 && rawId.length <= 200) {\n const orphaned = this.pendingSends.get(rawId);\n if (orphaned && !orphaned.failed) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId: rawId, outcome: 'unparseable-fail-closed' });\n this.failPendingSend(rawId);\n return;\n }\n }\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId: null, outcome: 'unparseable-dropped' });\n return;\n }\n const { clientMessageId, code, retryable, retryAfterMs } = parsed.data;\n const pending = this.pendingSends.get(clientMessageId);\n // No matching pending send — already acked/reconciled, or never ours. A late\n // ack/echo (which may have already landed) always wins; nothing to correlate.\n if (!pending || pending.failed) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable, outcome: 'no-pending-send' });\n return;\n }\n\n if (!retryable) {\n // Terminal: an unchanged resend cannot succeed (blocked-word / archived /\n // deleted). Fail ONLY this bubble, recording the reason + terminal class.\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: false, outcome: 'terminal' });\n this.failPendingSend(clientMessageId, code, false);\n return;\n }\n\n // Retryable: server-declared, so switch this send onto the wall-clock budget\n // (the 5-attempt cap no longer applies) and record the reason for a possible\n // budget-exhausted failure.\n pending.serverRetryable = true;\n pending.lastRejectionCode = code;\n // If the budget already elapsed (a long stream of retryable rejections), fail\n // now rather than scheduling yet another doomed resend.\n if (this.timers.now() - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'budget-exhausted' });\n this.failPendingSend(clientMessageId, code, true);\n return;\n }\n const delayMs = this.clampRetryDelay(retryAfterMs);\n this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'retry-scheduled', delayMs });\n this.scheduleSendRetry(clientMessageId, delayMs);\n }\n\n /**\n * Schedule EXACTLY ONE per-send resend of a correlated retryable rejection (never\n * stacks — a newer rejection clears the prior timer first). When it fires it\n * re-sends the SAME clientMessageId (the DO dedups) unless the wall-clock budget\n * has elapsed, in which case the send fails with its recorded reason.\n */\n private scheduleSendRetry(clientMessageId: string, delayMs: number): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return;\n this.clearSendRetryTimer(pending);\n pending.retryTimer = this.timers.setTimeout(() => {\n pending.retryTimer = null;\n if (this.closedByUs) return;\n const current = this.pendingSends.get(clientMessageId);\n if (!current || current.failed) return;\n if (this.timers.now() - current.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(clientMessageId, current.lastRejectionCode, true);\n return;\n }\n // A server-retryable resend does NOT count toward the attempt cap (that cap is\n // for blind reconnect resends only) — leave `attempts` untouched.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: current.clientMessageId,\n text: current.text,\n });\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId: current.clientMessageId,\n reason: 'scheduled',\n attempts: current.attempts,\n serverRetryable: current.serverRetryable,\n ageMs: this.timers.now() - current.sentAt,\n sent,\n });\n // Budget backstop: the contract says every send gets an ack or a rejection,\n // and either one replaces/clears this timer. If the server goes silent (a\n // contract violation), NOTHING else would ever re-check the budget on a\n // stable socket — so arm a deadline check that fails the send at budget end.\n // This makes SERVER_RETRYABLE_MAX_AGE_MS a hard ceiling, not a best effort.\n const remainingMs =\n SERVER_RETRYABLE_MAX_AGE_MS - (this.timers.now() - current.sentAt) + SEND_RETRY_MIN_DELAY_MS;\n current.retryTimer = this.timers.setTimeout(() => {\n current.retryTimer = null;\n if (this.closedByUs) return;\n const atDeadline = this.pendingSends.get(clientMessageId);\n if (!atDeadline || atDeadline.failed) return;\n if (this.timers.now() - atDeadline.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {\n this.failPendingSend(clientMessageId, atDeadline.lastRejectionCode, true);\n }\n }, remainingMs);\n }, delayMs);\n }\n\n /** Clamp a server `retryAfterMs` hint into a sane resend delay (defaulting when absent). */\n private clampRetryDelay(retryAfterMs: number | undefined): number {\n const raw =\n typeof retryAfterMs === 'number' && retryAfterMs > 0\n ? retryAfterMs\n : MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;\n return Math.min(Math.max(raw, SEND_RETRY_MIN_DELAY_MS), SEND_RETRY_MAX_DELAY_MS);\n }\n\n /**\n * Flip EVERY un-acked pending send to the visible failed state. Called on the\n * terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send\n * in flight when the socket dies for good shows \"Sending…\" until unmount.\n */\n private failAllPendingSends(): void {\n for (const clientMessageId of [...this.pendingSends.keys()]) {\n this.failPendingSend(clientMessageId);\n }\n }\n\n /**\n * Retry a FAILED (or still-pending) send, RE-USING its original clientMessageId\n * so the DO's send-idempotency dedups against any copy that actually landed.\n * Resets the attempt/age budget, clears the row's `sendFailed` marker (back to\n * the normal pending/optimistic look), and re-sends immediately when the socket\n * is open — otherwise the entry rides the next reconnect's automatic resend.\n * Returns false when the clientMessageId is not a tracked pending send (already\n * acked/reconciled, or never sent from this client).\n */\n retrySend(clientMessageId: string): boolean {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return false;\n // A manual retry supersedes any scheduled server-retryable resend and starts a\n // fresh budget from now; the server re-declares retryability if it rejects again.\n this.clearSendRetryTimer(pending);\n pending.failed = false;\n pending.serverRetryable = false;\n pending.lastRejectionCode = undefined;\n pending.sentAt = this.timers.now();\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n });\n pending.attempts = sent ? 1 : 0;\n this.diag?.(DIAG.SEND_RETRY, {\n clientMessageId,\n reason: 'manual',\n attempts: pending.attempts,\n serverRetryable: false,\n ageMs: 0,\n sent,\n });\n const next = this.state.messages.map((m) => {\n if (!(m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed)) {\n return m;\n }\n // Back to the normal pending look — drop every failure marker.\n const {\n sendFailed: _sendFailed,\n sendFailureCode: _sendFailureCode,\n sendRetryable: _sendRetryable,\n ...restMeta\n } = m.meta;\n return { ...m, meta: { ...restMeta } };\n });\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n private onError(): void {\n // The close handler does the backoff; onerror is advisory in browsers.\n }\n\n private onClose(code: number, _reason: string): void {\n this.stopHeartbeat();\n this.socket = null;\n this.lastInboundAt = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed', resumeSeq: this.resumeSeq });\n this.setState({ status: 'closed' });\n return;\n }\n // 4401 AUTH_EXPIRED: re-mint a grant ONCE and reconnect immediately, no backoff.\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant', resumeSeq: this.resumeSeq });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n // 4403 REVOKED: access pulled. Stop — a reconnect would just re-deny at mint.\n // Terminal: flip every un-acked pending send to the visible failed state first\n // (failPendingSend sets lastErrorCode 'send-failed'; the 'revoked' set below\n // wins as the final surfaced code).\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked', resumeSeq: this.resumeSeq });\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect', resumeSeq: this.resumeSeq });\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n resumeSeq: this.resumeSeq,\n });\n if (delay == null) {\n // Reconnect give-up: terminal, same as REVOKED — pending sends must not stay\n // an eternal \"Sending…\"; flip them to the visible failed/retry state.\n this.failAllPendingSends();\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n /**\n * Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).\n * A reconnect would just re-deny at the next mint, so stop for good — the same\n * terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect\n * controller, and flip every un-acked pending send to the visible failed state.\n * `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does\n * not win as the surfaced error; the React hook maps 'access-denied' to\n * `allowed: false` so the shell renders its existing no-access surface.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });\n }\n\n // ---- inbound frames ----\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.type) {\n // Unparseable: bytes arrived, but nothing proves they came from a DO\n // speaking this protocol, so this must NOT refresh the liveness deadline.\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // A well-formed envelope IS the liveness proof — including the `heartbeat-ack`\n // the DO auto-responds with, and a forward-compat type this build does not\n // handle yet (a newer DO's frames must never be read as a dead socket).\n this.lastInboundAt = this.timers.now();\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message, 'live');\n case 'ack':\n return this.applyAck(payload as { clientMessageId?: string; seq?: number; readSeq?: number });\n case 'history-page':\n return this.applyHistoryPage((payload as { messages: WireMessageRow[] }).messages ?? []);\n case 'presence':\n return this.setState({ presence: (payload as { online: string[] }).online ?? [] });\n case 'typing':\n return this.applyTyping((payload as { uid: string }).uid);\n case 'snapshot':\n return this.applyChannelSnapshot(payload as unknown as WireChannelSnapshot);\n case 'send-rejected':\n return this.applySendRejected(payload);\n case 'error': {\n const code = String((payload as { code?: unknown }).code ?? 'error');\n // ROLLOUT COMPATIBILITY ONLY (not the target protocol): a Worker that predates\n // the correlated `send-rejected` frame emits an UNCORRELATED\n // error{code:'membership-pending'} with no clientMessageId, so we cannot target\n // one send. Keep the send(s) pending (no visible error — the bubble stays\n // \"Sending…\"), mark every in-flight send server-retryable so the wall-clock\n // budget (NOT the 5-attempt cap) governs give-up, and re-send all after the\n // server-suggested delay. The correlated path below is the target protocol.\n if (code === MEMBERSHIP_PENDING_CODE) {\n const raw = (payload as { retryAfterMs?: unknown }).retryAfterMs;\n const delay = this.clampRetryDelay(typeof raw === 'number' ? raw : undefined);\n this.diag?.(DIAG.ERROR_FRAME, {\n code: MEMBERSHIP_PENDING_CODE,\n correlated: false,\n delayMs: delay,\n pendingSends: this.pendingSends.size,\n });\n for (const pending of this.pendingSends.values()) {\n if (!pending.failed) {\n pending.serverRetryable = true;\n pending.lastRejectionCode = MEMBERSHIP_PENDING_CODE;\n }\n }\n return this.scheduleMembershipPendingRetry(delay);\n }\n this.diag?.(DIAG.ERROR_FRAME, { code: safeDiagnosticLabel(code), correlated: false });\n return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n case 'heartbeat-ack':\n // The DO runtime's hibernation AUTO-RESPONSE to our 20s `heartbeat`. Per\n // Contract C liveness is tracked SERVER-side (the auto-response timestamp\n // the dueWork sweep reads), so the client heartbeat is fire-and-forget: the\n // ack needs no state change and the next tick is already scheduled. Explicit\n // no-op — and deliberately NO diagnostic, not even frame_applied: at ~20s\n // this is the one frame that recurs forever, so logging it would violate the\n // bounded \"one line per DECISION, never per heartbeat\" contract and drown the\n // frame_dropped signal that distinguishes \"never arrived\" from \"dropped\".\n return;\n default:\n // unknown/optional types tolerated (forward compat)\n this.diag?.(DIAG.FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n }\n\n /**\n * Apply a live moderation `revision` frame: record the overlay keyed by seq\n * (max-`messageRevision` wins — a stale/older frame is ignored) and re-render the\n * matching message in place if it is already loaded. If the base row hasn't\n * arrived yet the overlay is retained and re-applied when the row renders, so a\n * frame that races ahead of its message never lets the original content show.\n */\n private applyRevisionFrame(payload: { messageSeq?: number; kind?: RevisionKind; messageRevision?: number }): void {\n const seq = payload.messageSeq;\n const kind = payload.kind;\n const messageRevision = payload.messageRevision;\n if (typeof seq !== 'number' || kind == null || typeof messageRevision !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'revision', reason: 'malformed-payload' });\n return;\n }\n const prev = this.overlays.get(seq);\n // Max-revision merge: a higher messageRevision supersedes; an older one is dropped.\n if (prev && prev.messageRevision >= messageRevision) {\n this.diag?.(DIAG.FRAME_DROPPED, {\n kind: 'revision',\n reason: 'stale-revision',\n seq,\n messageRevision,\n heldRevision: prev.messageRevision,\n });\n return;\n }\n this.overlays.set(seq, { kind, messageRevision });\n // Re-render the matching message in place if it is already present. Re-derive\n // from the ORIGINAL row so a `restore` reverts a previously-blanked text.\n const messageId = seqToMessageId(seq);\n const original = this.originalRows.get(seq);\n const rendered = Boolean(original) && this.state.messages.some((m) => m.messageId === messageId);\n this.diag?.(DIAG.REVISION_APPLIED, {\n seq,\n kind,\n messageRevision,\n // false = overlay retained for a row not loaded yet (re-applied when it renders).\n rendered,\n });\n if (rendered && original) {\n const overlaid = applyModerationOverlay(original, this.effectiveOverlay(seq, null));\n const next = this.state.messages.map((m) => (m.messageId === messageId ? overlaid : m));\n this.setState({ messages: next });\n }\n }\n\n /**\n * The effective overlay for a seq = the max-`messageRevision` between the row's\n * own inline overlay (from the DO history/delta merge) and any received `revision`\n * frame stored in `this.overlays`. Either source may be ahead of the other.\n */\n private effectiveOverlay(seq: number, rowOverlay: ModerationOverlay | null): ModerationOverlay | null {\n const fromFrame = this.overlays.get(seq) ?? null;\n if (!rowOverlay) return fromFrame;\n if (!fromFrame) return rowOverlay;\n return fromFrame.messageRevision >= rowOverlay.messageRevision ? fromFrame : rowOverlay;\n }\n\n /** Map a DO row to a UI message with its effective moderation overlay applied. */\n private renderRow(row: WireMessageRow): ChatMessageV1 {\n const mapped = wireRowToMessage(row, this.config.threadId);\n // Cache the ORIGINAL so a later `restore` frame can revert a blanked message.\n this.originalRows.set(row.seq, mapped);\n return applyModerationOverlay(mapped, this.effectiveOverlay(row.seq, overlayFromRow(row)));\n }\n\n /**\n * Apply the Contract C resume snapshot. The DO always returns the authoritative\n * tail + read cursor and, per the pinned contract, EITHER a contiguous delta\n * (`resync:false`) OR a resync directive (`resync:true`).\n *\n * - `resync === false`: apply the contiguous `delta` (the ≤500-message backlog\n * after our cursor, oldest-first) through the same render/merge path as a live\n * `message`, then advance `resumeSeq` to `lastMessageSeq`. This heals ANY gap\n * within the backlog — including moderation `revision`s the DO baked into the\n * delta rows — with no history fetch, so a >50-message reconnect gap is no\n * longer a permanent hole.\n * - `resync === true`: the gap is beyond the backlog (or we sent no cursor). Drop\n * the local tail (messages + overlays + original-row cache) and re-page history\n * from the top via a cursorless `requestHistory(null)`.\n *\n * A pre-contract DO (no `resync`/`delta`) is treated as `resync:true` ⇒ the safe\n * re-page path, so a stale worker never strands the client. An empty delta with\n * `resync:false` (already caught up) is a no-op beyond advancing the cursor.\n */\n private applyChannelSnapshot(snap: WireChannelSnapshot): void {\n // Guard: an inbox snapshot must never reach a channel client.\n if (isInboxSnapshot(snap as never)) {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'snapshot', reason: 'inbox-snapshot-on-channel' });\n return;\n }\n if (typeof snap.lastMessageSeq !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'snapshot', reason: 'missing-last-message-seq' });\n return;\n }\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\n const cursorBefore = this.resumeSeq;\n const delta = Array.isArray(snap.delta) ? snap.delta : [];\n\n if (!resync) {\n // Apply the contiguous delta oldest-first, exactly like live messages, then\n // advance the cursor to the authoritative tail so the NEXT resume tells the DO\n // we're caught up only after we actually applied the backlog.\n for (const row of delta) this.applyMessage(row, 'delta');\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\n this.diag?.(DIAG.RESUME_RESULT, {\n lastMessageSeq: snap.lastMessageSeq,\n resync: false,\n deltaCount: delta.length,\n cursorBefore,\n cursorAfter: this.resumeSeq,\n readSeq: typeof snap.readSeq === 'number' ? snap.readSeq : null,\n });\n // A non-resync snapshot IS the first authoritative data (even an empty delta =\n // caught up) — end initial loading. A resync snapshot instead defers to the\n // re-paged first history page below, so it must NOT end it here.\n if (!this.state.hasLoadedInitialData) this.setState({ hasLoadedInitialData: true });\n return;\n }\n\n // Resync: drop the local tail and re-page from the top per the pinned contract.\n // Clear the derived caches too so a stale overlay/original-row can't resurrect a\n // message the re-page will reload fresh (with its baked-in moderation state).\n // KEEP un-acked optimistic rows — they aren't server history yet; their pending\n // sends were already re-sent in onOpen and reconcile when the DO echoes them.\n this.overlays.clear();\n this.originalRows.clear();\n this.resumeSeq = 0;\n const priorCount = this.state.messages.length;\n const keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.diag?.(DIAG.RESUME_RESULT, {\n lastMessageSeq: snap.lastMessageSeq,\n resync: true,\n deltaCount: delta.length,\n cursorBefore,\n cursorAfter: 0,\n readSeq: typeof snap.readSeq === 'number' ? snap.readSeq : null,\n });\n this.diag?.(DIAG.RESYNC_DROPPED_TAIL, {\n // The client REMOVED rows from the UI here — the counterpart of an upsert.\n removedCount: priorCount - keptOptimistic.length,\n keptOptimistic: keptOptimistic.length,\n });\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n /**\n * Apply one server row (a live `message` frame or a resume-delta row) and record\n * the decision the client made about it.\n *\n * Diagnostics bounding: a LIVE frame always emits one `chat_client_frame_applied`\n * line; a DELTA row emits one only when it is materially interesting (carries a\n * moderation kind, or reconciles one of our optimistic sends) — a 500-row backlog\n * is otherwise summarized by `chat_client_resume_result` rather than spamming 500\n * lines.\n */\n private applyMessage(row: WireMessageRow, source: 'live' | 'delta'): void {\n if (!row || typeof row.seq !== 'number') {\n this.diag?.(DIAG.FRAME_DROPPED, { kind: 'message', source, reason: 'missing-seq' });\n return;\n }\n // Read the pre-merge view ONLY when diagnostics are on (these are O(n) scans).\n const prior = this.diag ? this.priorRowFacts(row) : null;\n const cursorBefore = this.resumeSeq;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\n if (!this.diag || !prior) return;\n\n const notable = row.moderationKind != null || prior.reconciledOptimistic;\n if (source === 'delta' && !notable) return;\n this.diag(DIAG.FRAME_APPLIED, {\n kind: 'message',\n source,\n seq: row.seq,\n hasClientMessageId: Boolean(row.clientMessageId),\n moderationKind: row.moderationKind ?? null,\n op: prior.reconciledOptimistic ? 'optimistic-reconcile' : prior.hadRow ? 'replace-by-seq' : 'insert',\n cursorBefore,\n cursorAfter: this.resumeSeq,\n });\n }\n\n /**\n * The pre-merge view of a row's seq: whether we already hold that seq, and whether\n * this row reconciles a live optimistic send.\n * Diagnostics-only — never called when diagnostics are off.\n */\n private priorRowFacts(row: WireMessageRow): {\n hadRow: boolean;\n reconciledOptimistic: boolean;\n } {\n const messageId = seqToMessageId(row.seq);\n const existing = this.state.messages.find((m) => m.messageId === messageId);\n return {\n hadRow: existing != null,\n reconciledOptimistic: Boolean(\n row.clientMessageId &&\n this.state.messages.some((m) => m.meta?.optimistic && m.meta?.clientMessageId === row.clientMessageId),\n ),\n };\n }\n\n /**\n * Merge a server message: reconcile an optimistic echo by clientMessageId (swap\n * the `optimistic:` row for the canonical seq row), or insert/replace by seq.\n * Keeps the list sorted ascending by seq with optimistic rows at the tail.\n */\n private mergeMessage(mapped: ChatMessageV1, row: WireMessageRow): void {\n // If this canonical row carries a clientMessageId we still have pending (the\n // sender's own broadcast, or a resume delta that included our send), the send\n // landed — stop tracking it for reconnect resend (and cancel any retry timer).\n if (row.clientMessageId) this.clearPendingSend(row.clientMessageId);\n const next = this.state.messages.filter(\n (m) =>\n m.messageId !== mapped.messageId &&\n // drop the optimistic placeholder this server row reconciles\n (m.meta?.clientMessageId !== row.clientMessageId || !m.meta?.optimistic),\n );\n next.push(mapped);\n next.sort(sortBySeq);\n this.setState({ messages: next });\n }\n\n private applyAck(payload: { clientMessageId?: string; seq?: number; readSeq?: number }): void {\n // A read-ack echo carries readSeq only — nothing to reconcile locally.\n if (payload.clientMessageId == null || typeof payload.seq !== 'number') return;\n // Reconcile the optimistic echo: stamp the server seq onto it. The canonical\n // `message` broadcast (which the sender also receives) replaces it fully; this\n // ack guarantees the seq even if the broadcast is dropped/reordered.\n const seq = payload.seq;\n const cmid = payload.clientMessageId;\n this.diag?.(DIAG.SEND_ACK, {\n clientMessageId: cmid,\n seq,\n cursorBefore: this.resumeSeq,\n cursorAfter: seq > this.resumeSeq ? seq : this.resumeSeq,\n wasPending: this.pendingSends.has(cmid),\n });\n // The send is acked — stop tracking it for reconnect resend (and cancel any\n // per-send retry timer so a scheduled resend never fires after it landed).\n this.clearPendingSend(cmid);\n const next = this.state.messages.map((m) => {\n if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid) return m;\n // Strip any failed markers — a late ack (e.g. a retry raced the cap, or a\n // correlated rejection was superseded) means the send actually landed, so the\n // row must render as delivered, not failed.\n const {\n sendFailed: _sendFailed,\n sendFailureCode: _sendFailureCode,\n sendRetryable: _sendRetryable,\n ...restMeta\n } = m.meta;\n return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };\n });\n next.sort(sortBySeq);\n if (seq > this.resumeSeq) this.resumeSeq = seq;\n this.setState({ messages: next });\n }\n\n private applyHistoryPage(rows: WireMessageRow[]): void {\n const cursorBefore = this.resumeSeq;\n const mapped = rows.map((r) => this.renderRow(r));\n // Mirror mergeMessage's optimistic reconciliation: a history row carrying a\n // clientMessageId we still hold as an optimistic placeholder IS that send (the\n // DO stored it; the ack was lost). Drop the placeholder — keyed `optimistic:{cmid}`,\n // it would otherwise survive alongside its seq-keyed server row as a duplicate\n // bubble after a resync + re-page — and stop tracking it for resend.\n const incomingCmids = new Set<string>();\n for (const r of rows) {\n if (r.clientMessageId) {\n incomingCmids.add(r.clientMessageId);\n this.clearPendingSend(r.clientMessageId);\n }\n }\n const bySeq = new Map<string, ChatMessageV1>();\n for (const m of this.state.messages) {\n if (m.meta?.optimistic && typeof m.meta?.clientMessageId === 'string' && incomingCmids.has(m.meta.clientMessageId)) {\n continue; // reconciled by the incoming server row\n }\n bySeq.set(m.messageId, m);\n }\n for (const m of mapped) bySeq.set(m.messageId, m);\n const merged = [...bySeq.values()].sort(sortBySeq);\n for (const r of rows) if (r.seq > this.resumeSeq) this.resumeSeq = r.seq;\n this.setState({\n messages: merged,\n isFetchingOlder: false,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n // The first history page — INCLUDING an empty one — is a successful authoritative\n // load, so it ends initial loading. (Older-page fetches redundantly re-set true,\n // which is a harmless no-op since the flag never flips back to false.)\n hasLoadedInitialData: true,\n });\n if (!this.diag) return;\n const seqs = rows.map((r) => r.seq).filter((s): s is number => typeof s === 'number');\n this.diag(DIAG.HISTORY_PAGE, {\n count: rows.length,\n minSeq: seqs.length > 0 ? Math.min(...seqs) : null,\n maxSeq: seqs.length > 0 ? Math.max(...seqs) : null,\n cursorBefore,\n cursorAfter: this.resumeSeq,\n reconciledOptimistic: incomingCmids.size,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n total: merged.length,\n });\n }\n\n private applyTyping(uid: string): void {\n if (!uid || uid === this.config.currentUserId) return;\n if (!this.state.typing.includes(uid)) this.setState({ typing: [...this.state.typing, uid] });\n const prev = this.typingTimers.get(uid);\n if (prev) this.timers.clearTimeout(prev);\n this.typingTimers.set(\n uid,\n this.timers.setTimeout(() => {\n this.typingTimers.delete(uid);\n this.setState({ typing: this.state.typing.filter((u) => u !== uid) });\n }, TYPING_COALESCE_MS * 2),\n );\n }\n\n // ---- outbound actions ----\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /**\n * Optimistic send: render the echo immediately keyed by clientMessageId, then\n * emit the `send` frame. The server `ack`/`message` reconciles on the seq.\n */\n send(args: { clientMessageId: string; text: string }): boolean {\n // C-B8: write the frame FIRST and render the optimistic echo ONLY if it was\n // actually sent over an open socket. A closed socket returns false — no phantom\n // \"sent\" bubble, and the caller keeps the composer text — so a disconnect between\n // render and click can't silently swallow the message.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n });\n if (!sent) {\n this.diag?.(DIAG.SEND_FAILED, {\n clientMessageId: args.clientMessageId,\n code: 'send-failed',\n retryable: null,\n attempts: 0,\n reason: 'socket-not-open',\n });\n this.setState({ lastErrorCode: 'send-failed' });\n return false;\n }\n const now = this.timers.now();\n // Track the un-acked send so a reconnect re-sends it with the same\n // clientMessageId (the DO dedups) — until the ack/echo clears it.\n this.pendingSends.set(args.clientMessageId, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n sentAt: now,\n attempts: 1,\n failed: false,\n serverRetryable: false,\n retryTimer: null,\n });\n const echo = optimisticMessage({\n clientMessageId: args.clientMessageId,\n threadId: this.config.threadId,\n senderId: this.config.currentUserId,\n text: args.text,\n createdAt: now,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\n this.diag?.(DIAG.SEND_OPTIMISTIC, {\n clientMessageId: args.clientMessageId,\n pendingSends: this.pendingSends.size,\n });\n return true;\n }\n\n /**\n * Explicit read ack (delivery != read). `focused` => the channel is open + at latest.\n * Remembers the cursor (re-sent on reconnect) and returns whether the frame was\n * actually sent over an open socket, so the caller only advances its local\n * already-acked marker on a real send (M2).\n */\n readAck(readSeq: number, focused: boolean): boolean {\n this.lastReadAck = { readSeq, focused };\n return this.sendFrame(CLIENT_FRAME.READ_ACK, { readSeq, focused });\n }\n\n /** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */\n typing(): void {\n const now = this.timers.now();\n if (now - this.lastTypingSentAt < TYPING_COALESCE_MS) return;\n this.lastTypingSentAt = now;\n this.sendFrame(CLIENT_FRAME.TYPING, {});\n }\n\n presenceSubscribe(): void {\n this.presenceSubscribed = true;\n this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n }\n\n presenceUnsubscribe(): void {\n this.presenceSubscribed = false;\n this.sendFrame(CLIENT_FRAME.PRESENCE_UNSUBSCRIBE, {});\n this.setState({ presence: [] });\n }\n\n /** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */\n fetchOlder(): void {\n if (this.state.isFetchingOlder || !this.state.hasOlder) return;\n const oldest = this.state.messages.find((m) => typeof m.meta?.seq === 'number');\n const beforeSeq = oldest && typeof oldest.meta?.seq === 'number' ? (oldest.meta.seq as number) : null;\n this.setState({ isFetchingOlder: true });\n this.requestHistory(beforeSeq);\n }\n\n private requestHistory(beforeSeq: number | null): void {\n const sent = this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\n this.diag?.(DIAG.HISTORY_REQUEST, { beforeSeq, limit: HISTORY_PAGE_MAX, sent });\n }\n\n // ---- heartbeat (20s) + dead-socket watchdog ----\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const tick = () => {\n if (!this.socket || !this.socket.isOpen) return;\n // Liveness is checked BEFORE the write: a socket already proven dead must\n // not be handed another frame. Closing here does not reschedule — onClose\n // stops the heartbeat and owns what happens next.\n if (this.isInboundSilent()) return this.closeDeadSocket();\n this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n };\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer != null) {\n this.timers.clearTimeout(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /** True when the live socket has produced no parseable frame for too long. */\n private isInboundSilent(): boolean {\n if (this.lastInboundAt == null) return false;\n return this.timers.now() - this.lastInboundAt >= INBOUND_SILENCE_LIMIT_MS;\n }\n\n /**\n * The channel DO auto-acks every heartbeat, so silence past\n * {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no\n * `onclose` ever fired (the half-open-connection case that otherwise strands\n * the UI on a socket that will never deliver another message).\n *\n * Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close\n * lands on `onClose`'s ordinary transient path — which already owns the\n * reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing\n * about reconnect policy is duplicated here.\n */\n private closeDeadSocket(): void {\n const socket = this.socket;\n this.diag?.(DIAG.SOCKET_LIVENESS_TIMEOUT, {\n silentMs: this.lastInboundAt == null ? null : this.timers.now() - this.lastInboundAt,\n thresholdMs: INBOUND_SILENCE_LIMIT_MS,\n attempt: this.connectAttempt,\n resumeSeq: this.resumeSeq,\n pendingSends: this.pendingSends.size,\n });\n this.lastInboundAt = null;\n socket?.close(WATCHDOG_CLOSE_CODE, 'client liveness timeout');\n }\n\n // ---- teardown ----\n\n /** Permanently close this client (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n this.stopHeartbeat();\n // The heartbeat timer IS the watchdog's timer — stopping it above ends both.\n // Clearing the record too means a later connect() starts from a clean deadline.\n this.lastInboundAt = null;\n for (const t of this.typingTimers.values()) this.timers.clearTimeout(t);\n this.typingTimers.clear();\n this.overlays.clear();\n this.originalRows.clear();\n // Cancel every per-send retry timer before dropping the tracking map.\n for (const pending of this.pendingSends.values()) this.clearSendRetryTimer(pending);\n this.pendingSends.clear();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.membershipRetryTimer != null) {\n this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed', typing: [], presence: [] });\n }\n}\n\nfunction sortBySeq(a: ChatMessageV1, b: ChatMessageV1): number {\n const sa = typeof a.meta?.seq === 'number' ? (a.meta.seq as number) : Number.POSITIVE_INFINITY;\n const sb = typeof b.meta?.seq === 'number' ? (b.meta.seq as number) : Number.POSITIVE_INFINITY;\n if (sa !== sb) return sa - sb;\n return a.createdAt - b.createdAt;\n}\n"]}
|
|
@@ -27,6 +27,13 @@ export declare const CHAT_CLIENT_DIAGNOSTIC_EVENTS: {
|
|
|
27
27
|
readonly SOCKET_OPEN: "chat_client_socket_open";
|
|
28
28
|
/** The socket closed, with the code and what the client decided to do next. */
|
|
29
29
|
readonly SOCKET_CLOSE: "chat_client_socket_close";
|
|
30
|
+
/**
|
|
31
|
+
* CHANNEL ONLY. The dead-socket watchdog closed a socket that had gone silent
|
|
32
|
+
* past the client's inbound-silence threshold (the channel DO auto-acks every
|
|
33
|
+
* heartbeat, so silence there is provable death). One line per detection —
|
|
34
|
+
* never per heartbeat — followed by the ordinary socket_close/reconnect lines.
|
|
35
|
+
*/
|
|
36
|
+
readonly SOCKET_LIVENESS_TIMEOUT: "chat_client_socket_liveness_timeout";
|
|
30
37
|
/** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */
|
|
31
38
|
readonly RECONNECT_SCHEDULED: "chat_client_reconnect_scheduled";
|
|
32
39
|
/** The resume cursor sent to the DO. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE/F;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,OAAO,GAAG,yBAAyB,CAAC;AAE9E,sFAAsF;AACtF,MAAM,MAAM,4BAA4B,GAAG,yBAAyB,GAAG,IAAI,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;IACxC,gFAAgF;;IAEhF,8EAA8E;;IAE9E,0EAA0E;;IAE1E,+EAA+E;;IAE/E,8EAA8E;;IAE9E,wCAAwC;;IAExC,qEAAqE;;IAErE,8EAA8E;;IAE9E,wFAAwF;;IAExF,gDAAgD;;IAEhD,yFAAyF;;IAEzF,qDAAqD;;IAErD,mEAAmE;;IAEnE,6DAA6D;;IAE7D,qDAAqD;;IAErD,kFAAkF;;IAElF,gFAAgF;;IAEhF,gEAAgE;;IAEhE,iDAAiD;;IAKjD,uFAAuF;;IAEvF,oFAAoF;;IAEpF,uEAAuE;;IAEvE,qFAAqF;;IAErF,qFAAqF;;IAErF,oFAAoF;;IAEpF,qFAAqF;;IAErF,kFAAkF;;IAElF,uDAAuD;;IAEvD,yEAAyE;;CAEjE,CAAC;AAEX,MAAM,MAAM,6BAA6B,GACvC,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,OAAO,6BAA6B,CAAC,CAAC;AAKrF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAIxD;AAaD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,2BAA2B,GAAG,SAAS,GAC9C,4BAA4B,CAY9B"}
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAE/F;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,OAAO,GAAG,yBAAyB,CAAC;AAE9E,sFAAsF;AACtF,MAAM,MAAM,4BAA4B,GAAG,yBAAyB,GAAG,IAAI,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;IACxC,gFAAgF;;IAEhF,8EAA8E;;IAE9E,0EAA0E;;IAE1E,+EAA+E;;IAE/E;;;;;OAKG;;IAEH,8EAA8E;;IAE9E,wCAAwC;;IAExC,qEAAqE;;IAErE,8EAA8E;;IAE9E,wFAAwF;;IAExF,gDAAgD;;IAEhD,yFAAyF;;IAEzF,qDAAqD;;IAErD,mEAAmE;;IAEnE,6DAA6D;;IAE7D,qDAAqD;;IAErD,kFAAkF;;IAElF,gFAAgF;;IAEhF,gEAAgE;;IAEhE,iDAAiD;;IAKjD,uFAAuF;;IAEvF,oFAAoF;;IAEpF,uEAAuE;;IAEvE,qFAAqF;;IAErF,qFAAqF;;IAErF,oFAAoF;;IAEpF,qFAAqF;;IAErF,kFAAkF;;IAElF,uDAAuD;;IAEvD,yEAAyE;;CAEjE,CAAC;AAEX,MAAM,MAAM,6BAA6B,GACvC,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,OAAO,6BAA6B,CAAC,CAAC;AAKrF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAIxD;AAaD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,2BAA2B,GAAG,SAAS,GAC9C,4BAA4B,CAY9B"}
|
|
@@ -38,6 +38,13 @@ export const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {
|
|
|
38
38
|
SOCKET_OPEN: 'chat_client_socket_open',
|
|
39
39
|
/** The socket closed, with the code and what the client decided to do next. */
|
|
40
40
|
SOCKET_CLOSE: 'chat_client_socket_close',
|
|
41
|
+
/**
|
|
42
|
+
* CHANNEL ONLY. The dead-socket watchdog closed a socket that had gone silent
|
|
43
|
+
* past the client's inbound-silence threshold (the channel DO auto-acks every
|
|
44
|
+
* heartbeat, so silence there is provable death). One line per detection —
|
|
45
|
+
* never per heartbeat — followed by the ordinary socket_close/reconnect lines.
|
|
46
|
+
*/
|
|
47
|
+
SOCKET_LIVENESS_TIMEOUT: 'chat_client_socket_liveness_timeout',
|
|
41
48
|
/** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */
|
|
42
49
|
RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',
|
|
43
50
|
/** The resume cursor sent to the DO. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"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","sourcesContent":["// OPT-IN structured client diagnostics for the realtime chat transport.\n//\n// The E2E/browser harness can already capture the raw WebSocket frames; what it\n// cannot see is what the CLIENT DECIDED about them — which frame it applied,\n// which it dropped and why, what resume cursor it sent, and whether a send was\n// reconciled or failed. This module is that missing half: a bounded, structured,\n// opt-in decision log.\n//\n// The connection-time, resume, presence, and socket-timing instruments are the\n// baseline-campaign metric families — they stay. Only the attachment-serving\n// instruments were retired with the chat-attachment architecture.\n//\n// Rules this module exists to enforce:\n// - OFF BY DEFAULT and zero behavior change. When the option is absent the\n// factory returns `null`, every call site is `this.diag?.(...)`, and JS\n// optional-call short-circuiting means the payload object is never even\n// constructed. The cost when off is one nullish check.\n// - NEVER user content. Ids, seqs, states, counts, codes, booleans only —\n// never message text, captions, filenames, URLs, tokens, or previews. The\n// INBOX events go one step further and log SIZES only: a `channelRef` names\n// one specific conversation, so registry/unread lines carry counts and\n// deltas, never the refs themselves.\n// - STABLE event names (`chat_client_*`) suitable for log queries and later\n// alert metrics; one line per DECISION, never per render or per heartbeat.\n// - A diagnostics sink can never break the client: every emit is wrapped, so a\n// throwing sink is swallowed rather than turned into a transport failure.\n\n/**\n * A structured diagnostic sink. `event` is one of the stable\n * {@link CHAT_CLIENT_DIAGNOSTIC_EVENTS} names; `data` is a compact, content-free\n * payload (ids/seqs/states/counts/codes only). Supplying a sink lets the app\n * route entries into its own logger; `true` uses the console.debug default.\n */\nexport type ChatClientDiagnosticsSink = (event: string, data: Record<string, unknown>) => void;\n\n/**\n * The additive `diagnostics` config value: `false`/absent = off (the default and\n * the only production posture), `true` = the single-line `console.debug` sink,\n * or a custom {@link ChatClientDiagnosticsSink}.\n */\nexport type ChatClientDiagnosticsOption = boolean | ChatClientDiagnosticsSink;\n\n/** The bound emitter, or `null` when diagnostics are off (the zero-overhead path). */\nexport type ChatClientDiagnosticsEmitter = ChatClientDiagnosticsSink | null;\n\n/**\n * Stable event names. These are a QUERY CONTRACT — Cloud Logging filters, E2E\n * console greps, and any later alert metric key on them, so a name is added or\n * retired deliberately, never renamed in place.\n */\nexport const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {\n /** A socket open is being attempted (per attempt, with the reconnect cause). */\n CONNECT_ATTEMPT: 'chat_client_connect_attempt',\n /** The grant mint failed before a socket existed (terminal vs. retryable). */\n GRANT_FAILED: 'chat_client_grant_failed',\n /** The socket reached OPEN (the point the resume cursor is sent from). */\n SOCKET_OPEN: 'chat_client_socket_open',\n /** The socket closed, with the code and what the client decided to do next. */\n SOCKET_CLOSE: 'chat_client_socket_close',\n /** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',\n /** The resume cursor sent to the DO. */\n RESUME_REQUEST: 'chat_client_resume_request',\n /** The DO's snapshot answer + how the cursor moved because of it. */\n RESUME_RESULT: 'chat_client_resume_result',\n /** A `resync: true` snapshot dropped the local tail (removal, not upsert). */\n RESYNC_DROPPED_TAIL: 'chat_client_resync_dropped_tail',\n /** A frame was applied to UI state (insert / replace-by-seq / optimistic reconcile). */\n FRAME_APPLIED: 'chat_client_frame_applied',\n /** A frame was NOT applied, with the reason. */\n FRAME_DROPPED: 'chat_client_frame_dropped',\n /** A moderation revision overlay was recorded (and whether a loaded row re-rendered). */\n REVISION_APPLIED: 'chat_client_revision_applied',\n /** A history page was requested (cursor + limit). */\n HISTORY_REQUEST: 'chat_client_history_request',\n /** A history page was applied: size, seq range, cursor advance. */\n HISTORY_PAGE: 'chat_client_history_page',\n /** An optimistic send was rendered and its frame written. */\n SEND_OPTIMISTIC: 'chat_client_send_optimistic',\n /** A send was acked by the server (seq assigned). */\n SEND_ACK: 'chat_client_send_ack',\n /** A correlated `send-rejected` (or its legacy uncorrelated form) was handled. */\n SEND_REJECTED: 'chat_client_send_rejected',\n /** A send was re-emitted (reconnect resume / scheduled hint / manual retry). */\n SEND_RETRY: 'chat_client_send_retry',\n /** A send reached its visible failed state, with the reason. */\n SEND_FAILED: 'chat_client_send_failed',\n /** An uncorrelated server `error` frame code. */\n ERROR_FRAME: 'chat_client_error_frame',\n\n // ---- inbox socket (one per USER, separate from the per-thread channel socket) ----\n\n /** An inbox socket open is being attempted (per attempt, with the reconnect cause). */\n INBOX_CONNECT_ATTEMPT: 'chat_client_inbox_connect_attempt',\n /** The inbox grant mint failed before a socket existed (terminal vs. retryable). */\n INBOX_GRANT_FAILED: 'chat_client_inbox_grant_failed',\n /** The inbox socket reached OPEN (the point `resume` is sent from). */\n INBOX_SOCKET_OPEN: 'chat_client_inbox_socket_open',\n /** The inbox socket closed, with the code and what the client decided to do next. */\n INBOX_SOCKET_CLOSE: 'chat_client_inbox_socket_close',\n /** An inbox reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n INBOX_RECONNECT_SCHEDULED: 'chat_client_inbox_reconnect_scheduled',\n /** The cursorless inbox `resume` was sent (the DO answers with a full snapshot). */\n INBOX_RESUME_REQUEST: 'chat_client_inbox_resume_request',\n /** An authoritative inbox snapshot was applied — registry SIZES only, never refs. */\n INBOX_SNAPSHOT_APPLIED: 'chat_client_inbox_snapshot_applied',\n /** The unread projection actually CHANGED (dock dot and/or per-row dot count). */\n INBOX_UNREAD_UPDATED: 'chat_client_inbox_unread_updated',\n /** An inbox frame was NOT applied, with the reason. */\n INBOX_FRAME_DROPPED: 'chat_client_inbox_frame_dropped',\n /** A mark-read was written to the socket (no optimistic local clear). */\n INBOX_MARK_READ: 'chat_client_inbox_mark_read',\n} as const;\n\nexport type ChatClientDiagnosticEventName =\n (typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS)[keyof typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS];\n\n/** Max length of a server-supplied label (frame type, error code) we echo into a log line. */\nconst MAX_LABEL_LEN = 48;\n\n/**\n * Clamp a server-supplied label before it enters a log line. Frame types and\n * error codes are protocol tokens, not user content, but they arrive from the\n * wire — bound them so a hostile/buggy peer cannot emit an unbounded line.\n */\nexport function safeDiagnosticLabel(raw: unknown): string {\n if (typeof raw !== 'string') return 'unknown';\n const trimmed = raw.slice(0, MAX_LABEL_LEN);\n return trimmed.length > 0 ? trimmed : 'unknown';\n}\n\n/**\n * The default sink: ONE `console.debug` line per decision, shaped\n * `chat_client_<event> {\"compact\":\"json\"}` so a browser console capture or a log\n * query can filter on the stable prefix and parse the payload without a\n * multi-line join.\n */\nconst consoleDebugSink: ChatClientDiagnosticsSink = (event, data) => {\n if (typeof console === 'undefined' || typeof console.debug !== 'function') return;\n console.debug(`${event} ${JSON.stringify(data)}`);\n};\n\n/**\n * Build the emitter for a `diagnostics` config value. Returns `null` when\n * diagnostics are off so call sites can use `this.diag?.(...)` — with optional\n * call syntax the argument object is not constructed at all on the off path.\n */\nexport function createDiagnosticsEmitter(\n option: ChatClientDiagnosticsOption | undefined,\n): ChatClientDiagnosticsEmitter {\n if (option == null || option === false) return null;\n const sink = option === true ? consoleDebugSink : option;\n return (event, data) => {\n // Diagnostics observe; they must never alter transport behavior — a sink that\n // throws (or a payload that cannot be serialized) is swallowed here.\n try {\n sink(event, data);\n } catch {\n /* diagnostics are never allowed to fail the client */\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"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","sourcesContent":["// OPT-IN structured client diagnostics for the realtime chat transport.\n//\n// The E2E/browser harness can already capture the raw WebSocket frames; what it\n// cannot see is what the CLIENT DECIDED about them — which frame it applied,\n// which it dropped and why, what resume cursor it sent, and whether a send was\n// reconciled or failed. This module is that missing half: a bounded, structured,\n// opt-in decision log.\n//\n// The connection-time, resume, presence, and socket-timing instruments are the\n// baseline-campaign metric families — they stay. Only the attachment-serving\n// instruments were retired with the chat-attachment architecture.\n//\n// Rules this module exists to enforce:\n// - OFF BY DEFAULT and zero behavior change. When the option is absent the\n// factory returns `null`, every call site is `this.diag?.(...)`, and JS\n// optional-call short-circuiting means the payload object is never even\n// constructed. The cost when off is one nullish check.\n// - NEVER user content. Ids, seqs, states, counts, codes, booleans only —\n// never message text, captions, filenames, URLs, tokens, or previews. The\n// INBOX events go one step further and log SIZES only: a `channelRef` names\n// one specific conversation, so registry/unread lines carry counts and\n// deltas, never the refs themselves.\n// - STABLE event names (`chat_client_*`) suitable for log queries and later\n// alert metrics; one line per DECISION, never per render or per heartbeat.\n// - A diagnostics sink can never break the client: every emit is wrapped, so a\n// throwing sink is swallowed rather than turned into a transport failure.\n\n/**\n * A structured diagnostic sink. `event` is one of the stable\n * {@link CHAT_CLIENT_DIAGNOSTIC_EVENTS} names; `data` is a compact, content-free\n * payload (ids/seqs/states/counts/codes only). Supplying a sink lets the app\n * route entries into its own logger; `true` uses the console.debug default.\n */\nexport type ChatClientDiagnosticsSink = (event: string, data: Record<string, unknown>) => void;\n\n/**\n * The additive `diagnostics` config value: `false`/absent = off (the default and\n * the only production posture), `true` = the single-line `console.debug` sink,\n * or a custom {@link ChatClientDiagnosticsSink}.\n */\nexport type ChatClientDiagnosticsOption = boolean | ChatClientDiagnosticsSink;\n\n/** The bound emitter, or `null` when diagnostics are off (the zero-overhead path). */\nexport type ChatClientDiagnosticsEmitter = ChatClientDiagnosticsSink | null;\n\n/**\n * Stable event names. These are a QUERY CONTRACT — Cloud Logging filters, E2E\n * console greps, and any later alert metric key on them, so a name is added or\n * retired deliberately, never renamed in place.\n */\nexport const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {\n /** A socket open is being attempted (per attempt, with the reconnect cause). */\n CONNECT_ATTEMPT: 'chat_client_connect_attempt',\n /** The grant mint failed before a socket existed (terminal vs. retryable). */\n GRANT_FAILED: 'chat_client_grant_failed',\n /** The socket reached OPEN (the point the resume cursor is sent from). */\n SOCKET_OPEN: 'chat_client_socket_open',\n /** The socket closed, with the code and what the client decided to do next. */\n SOCKET_CLOSE: 'chat_client_socket_close',\n /**\n * CHANNEL ONLY. The dead-socket watchdog closed a socket that had gone silent\n * past the client's inbound-silence threshold (the channel DO auto-acks every\n * heartbeat, so silence there is provable death). One line per detection —\n * never per heartbeat — followed by the ordinary socket_close/reconnect lines.\n */\n SOCKET_LIVENESS_TIMEOUT: 'chat_client_socket_liveness_timeout',\n /** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',\n /** The resume cursor sent to the DO. */\n RESUME_REQUEST: 'chat_client_resume_request',\n /** The DO's snapshot answer + how the cursor moved because of it. */\n RESUME_RESULT: 'chat_client_resume_result',\n /** A `resync: true` snapshot dropped the local tail (removal, not upsert). */\n RESYNC_DROPPED_TAIL: 'chat_client_resync_dropped_tail',\n /** A frame was applied to UI state (insert / replace-by-seq / optimistic reconcile). */\n FRAME_APPLIED: 'chat_client_frame_applied',\n /** A frame was NOT applied, with the reason. */\n FRAME_DROPPED: 'chat_client_frame_dropped',\n /** A moderation revision overlay was recorded (and whether a loaded row re-rendered). */\n REVISION_APPLIED: 'chat_client_revision_applied',\n /** A history page was requested (cursor + limit). */\n HISTORY_REQUEST: 'chat_client_history_request',\n /** A history page was applied: size, seq range, cursor advance. */\n HISTORY_PAGE: 'chat_client_history_page',\n /** An optimistic send was rendered and its frame written. */\n SEND_OPTIMISTIC: 'chat_client_send_optimistic',\n /** A send was acked by the server (seq assigned). */\n SEND_ACK: 'chat_client_send_ack',\n /** A correlated `send-rejected` (or its legacy uncorrelated form) was handled. */\n SEND_REJECTED: 'chat_client_send_rejected',\n /** A send was re-emitted (reconnect resume / scheduled hint / manual retry). */\n SEND_RETRY: 'chat_client_send_retry',\n /** A send reached its visible failed state, with the reason. */\n SEND_FAILED: 'chat_client_send_failed',\n /** An uncorrelated server `error` frame code. */\n ERROR_FRAME: 'chat_client_error_frame',\n\n // ---- inbox socket (one per USER, separate from the per-thread channel socket) ----\n\n /** An inbox socket open is being attempted (per attempt, with the reconnect cause). */\n INBOX_CONNECT_ATTEMPT: 'chat_client_inbox_connect_attempt',\n /** The inbox grant mint failed before a socket existed (terminal vs. retryable). */\n INBOX_GRANT_FAILED: 'chat_client_inbox_grant_failed',\n /** The inbox socket reached OPEN (the point `resume` is sent from). */\n INBOX_SOCKET_OPEN: 'chat_client_inbox_socket_open',\n /** The inbox socket closed, with the code and what the client decided to do next. */\n INBOX_SOCKET_CLOSE: 'chat_client_inbox_socket_close',\n /** An inbox reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n INBOX_RECONNECT_SCHEDULED: 'chat_client_inbox_reconnect_scheduled',\n /** The cursorless inbox `resume` was sent (the DO answers with a full snapshot). */\n INBOX_RESUME_REQUEST: 'chat_client_inbox_resume_request',\n /** An authoritative inbox snapshot was applied — registry SIZES only, never refs. */\n INBOX_SNAPSHOT_APPLIED: 'chat_client_inbox_snapshot_applied',\n /** The unread projection actually CHANGED (dock dot and/or per-row dot count). */\n INBOX_UNREAD_UPDATED: 'chat_client_inbox_unread_updated',\n /** An inbox frame was NOT applied, with the reason. */\n INBOX_FRAME_DROPPED: 'chat_client_inbox_frame_dropped',\n /** A mark-read was written to the socket (no optimistic local clear). */\n INBOX_MARK_READ: 'chat_client_inbox_mark_read',\n} as const;\n\nexport type ChatClientDiagnosticEventName =\n (typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS)[keyof typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS];\n\n/** Max length of a server-supplied label (frame type, error code) we echo into a log line. */\nconst MAX_LABEL_LEN = 48;\n\n/**\n * Clamp a server-supplied label before it enters a log line. Frame types and\n * error codes are protocol tokens, not user content, but they arrive from the\n * wire — bound them so a hostile/buggy peer cannot emit an unbounded line.\n */\nexport function safeDiagnosticLabel(raw: unknown): string {\n if (typeof raw !== 'string') return 'unknown';\n const trimmed = raw.slice(0, MAX_LABEL_LEN);\n return trimmed.length > 0 ? trimmed : 'unknown';\n}\n\n/**\n * The default sink: ONE `console.debug` line per decision, shaped\n * `chat_client_<event> {\"compact\":\"json\"}` so a browser console capture or a log\n * query can filter on the stable prefix and parse the payload without a\n * multi-line join.\n */\nconst consoleDebugSink: ChatClientDiagnosticsSink = (event, data) => {\n if (typeof console === 'undefined' || typeof console.debug !== 'function') return;\n console.debug(`${event} ${JSON.stringify(data)}`);\n};\n\n/**\n * Build the emitter for a `diagnostics` config value. Returns `null` when\n * diagnostics are off so call sites can use `this.diag?.(...)` — with optional\n * call syntax the argument object is not constructed at all on the off path.\n */\nexport function createDiagnosticsEmitter(\n option: ChatClientDiagnosticsOption | undefined,\n): ChatClientDiagnosticsEmitter {\n if (option == null || option === false) return null;\n const sink = option === true ? consoleDebugSink : option;\n return (event, data) => {\n // Diagnostics observe; they must never alter transport behavior — a sink that\n // throws (or a payload that cannot be serialized) is swallowed here.\n try {\n sink(event, data);\n } catch {\n /* diagnostics are never allowed to fail the client */\n }\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOlF,OAAO,EAKL,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,uFAAuF;IACvF,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE,CAAC;IAC9G;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C;AAID,qBAAa,WAAW;IAkBV,OAAO,CAAC,QAAQ,CAAC,MAAM;IAjBnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,UAAU,CAAS;IAC3B,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,4EAA4E;IAC5E,OAAO,CAAC,cAAc,CAAK;IAC3B,uEAAuE;IACvE,OAAO,CAAC,cAAc,CAA6E;IACnG,gFAAgF;IAChF,OAAO,CAAC,aAAa,CAAuB;gBAEf,MAAM,EAAE,iBAAiB;IAMtD,QAAQ,IAAI,gBAAgB;IAI5B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD,OAAO,CAAC,QAAQ;IAKV,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YA2BhB,UAAU;IAqCxB,OAAO,CAAC,MAAM;IAiBd,OAAO,CAAC,OAAO;IA4Bf;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,SAAS;IAMjB,yEAAyE;IACzE,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI7C;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAQrC,kEAAkE;IAClE,KAAK,IAAI,IAAI;CAad"}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// 4401 re-grant once, 4403 stop, auth-switch teardown).
|
|
10
10
|
import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
11
11
|
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
12
|
-
import { defaultTimers, isChatAccessDeniedError } from './shared.js';
|
|
12
|
+
import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, } from './shared.js';
|
|
13
13
|
import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
|
|
14
14
|
const INITIAL = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };
|
|
15
15
|
export class InboxClient {
|
|
@@ -49,6 +49,27 @@ export class InboxClient {
|
|
|
49
49
|
fn(this.state);
|
|
50
50
|
}
|
|
51
51
|
async connect() {
|
|
52
|
+
// Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle
|
|
53
|
+
// from a fresh (idle) or fully torn-down (closed) state. A second connect()
|
|
54
|
+
// while one is already connecting / open / reconnecting is a no-op, so a
|
|
55
|
+
// double-mounted dock cannot open a SECOND inbox socket against the per-uid
|
|
56
|
+
// cap. The reconnect path uses openSocket() / scheduleReconnect() directly
|
|
57
|
+
// (never connect()), so this never blocks a legitimate reconnect.
|
|
58
|
+
if (this.state.status !== 'idle' && this.state.status !== 'closed')
|
|
59
|
+
return;
|
|
60
|
+
// REVIVE the controller. `controller.close()` is PERMANENT: while it is in
|
|
61
|
+
// state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`
|
|
62
|
+
// returns null — so without this reset a close() → connect() client opens a
|
|
63
|
+
// socket and works normally until its FIRST transient close, then parks
|
|
64
|
+
// terminally in `closed` with reconnect disabled and no surfaced error code.
|
|
65
|
+
this.controller.reset();
|
|
66
|
+
// A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN
|
|
67
|
+
// its lifecycle; an explicit connect() is a fresh attempt the authority may
|
|
68
|
+
// legitimately re-deny, so the previous lifecycle's code must not persist.
|
|
69
|
+
// The registry/unread projection is deliberately KEPT — the DO's next snapshot
|
|
70
|
+
// is authoritative, and blanking the dock on every restart is pure UI churn.
|
|
71
|
+
if (isTerminalErrorCode(this.state.lastErrorCode))
|
|
72
|
+
this.setState({ lastErrorCode: null });
|
|
52
73
|
this.closedByUs = false;
|
|
53
74
|
this.reconnectCause = 'initial';
|
|
54
75
|
this.controller.start();
|
|
@@ -130,7 +151,7 @@ export class InboxClient {
|
|
|
130
151
|
this.closedByUs = true;
|
|
131
152
|
this.controller.close();
|
|
132
153
|
this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });
|
|
133
|
-
this.setState({ status: 'closed', lastErrorCode:
|
|
154
|
+
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
|
|
134
155
|
return;
|
|
135
156
|
}
|
|
136
157
|
this.reconnectCause = 'transient-close';
|
|
@@ -148,7 +169,7 @@ export class InboxClient {
|
|
|
148
169
|
denyAccessTerminally() {
|
|
149
170
|
this.closedByUs = true;
|
|
150
171
|
this.controller.close();
|
|
151
|
-
this.setState({ status: 'closed', lastErrorCode:
|
|
172
|
+
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });
|
|
152
173
|
}
|
|
153
174
|
scheduleReconnect() {
|
|
154
175
|
const delay = this.controller.onClose();
|
|
@@ -289,11 +310,11 @@ export class InboxClient {
|
|
|
289
310
|
}
|
|
290
311
|
}
|
|
291
312
|
/**
|
|
292
|
-
* Derive the per-channel unread set from a snapshot. The
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* field
|
|
296
|
-
*
|
|
313
|
+
* Derive the per-channel unread set from a snapshot. The inbox DO's snapshot
|
|
314
|
+
* carries a per-entry `unread` boolean on every active registry entry alongside
|
|
315
|
+
* the global `hasUnread` roll-up that drives the dock dot. An entry without the
|
|
316
|
+
* field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot
|
|
317
|
+
* still lights off `hasUnread`.
|
|
297
318
|
*
|
|
298
319
|
* ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears
|
|
299
320
|
* unread on archive; the client also never counts them).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport { defaultTimers, isChatAccessDeniedError } from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n}\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => undefined,\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.setState({ status: 'closed', lastErrorCode: 'access-denied' });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n this.setState({ registry, hasUnread, unreadChannelRefs });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` on the inbox socket; the inbox DO advances the\n * member's read cursor to tail on the channel DO and then pushes a fresh\n * authoritative snapshot, which is what removes the row — there is NO optimistic\n * local clear, so a failed mark-read honestly leaves the dot in place.\n * Returns false when the socket is not open (the caller may surface a retry).\n */\n markRead(channelRef: string): boolean {\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n return sent;\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The current inbox DO snapshot\n * exposes a single `hasUnread` boolean (dock dot) + the active registry; it does\n * NOT yet enumerate which entries are unread. We surface a per-entry `unread`\n * field when the DO provides it (forward-compatible), else fall back to empty\n * per-row dots (the dock dot still lights). See the ASSUMPTIONS note in the docs.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n isTerminalErrorCode,\n TERMINAL_ERROR_CODE,\n} from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n}\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle\n // from a fresh (idle) or fully torn-down (closed) state. A second connect()\n // while one is already connecting / open / reconnecting is a no-op, so a\n // double-mounted dock cannot open a SECOND inbox socket against the per-uid\n // cap. The reconnect path uses openSocket() / scheduleReconnect() directly\n // (never connect()), so this never blocks a legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n // REVIVE the controller. `controller.close()` is PERMANENT: while it is in\n // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`\n // returns null — so without this reset a close() → connect() client opens a\n // socket and works normally until its FIRST transient close, then parks\n // terminally in `closed` with reconnect disabled and no surfaced error code.\n this.controller.reset();\n // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN\n // its lifecycle; an explicit connect() is a fresh attempt the authority may\n // legitimately re-deny, so the previous lifecycle's code must not persist.\n // The registry/unread projection is deliberately KEPT — the DO's next snapshot\n // is authoritative, and blanking the dock on every restart is pure UI churn.\n if (isTerminalErrorCode(this.state.lastErrorCode)) this.setState({ lastErrorCode: null });\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => undefined,\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n this.setState({ registry, hasUnread, unreadChannelRefs });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` on the inbox socket; the inbox DO advances the\n * member's read cursor to tail on the channel DO and then pushes a fresh\n * authoritative snapshot, which is what removes the row — there is NO optimistic\n * local clear, so a failed mark-read honestly leaves the dot in place.\n * Returns false when the socket is not open (the caller may surface a retry).\n */\n markRead(channelRef: string): boolean {\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n return sent;\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot\n * carries a per-entry `unread` boolean on every active registry entry alongside\n * the global `hasUnread` roll-up that drives the dock dot. An entry without the\n * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot\n * still lights off `hasUnread`.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
|
|
@@ -39,6 +39,27 @@ export interface TransportTimers {
|
|
|
39
39
|
export declare const defaultTimers: TransportTimers;
|
|
40
40
|
/** Connection status surfaced to the UI. */
|
|
41
41
|
export type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
|
|
42
|
+
/**
|
|
43
|
+
* The TERMINAL lifecycle verdicts both realtime clients surface on
|
|
44
|
+
* `lastErrorCode` — the two codes that mean "this lifecycle stopped on an
|
|
45
|
+
* authoritative NO", as opposed to a transient/protocol code (`send-failed`,
|
|
46
|
+
* `blocked-word`, …) that leaves the socket running.
|
|
47
|
+
*
|
|
48
|
+
* ONE declaration for both clients: each sets and clears the same two strings,
|
|
49
|
+
* and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.
|
|
50
|
+
* A terminal verdict is terminal WITHIN its lifecycle; a later explicit
|
|
51
|
+
* `connect()` is a fresh attempt the authority may legitimately re-deny, so the
|
|
52
|
+
* stale code is cleared there (never carried into the new lifecycle's state).
|
|
53
|
+
*/
|
|
54
|
+
export declare const TERMINAL_ERROR_CODE: {
|
|
55
|
+
/** A 4403 REVOKED close: access was pulled while the socket was open. */
|
|
56
|
+
readonly REVOKED: "revoked";
|
|
57
|
+
/** The grant provider threw `ChatAccessDeniedError` — a terminal policy "no". */
|
|
58
|
+
readonly ACCESS_DENIED: "access-denied";
|
|
59
|
+
};
|
|
60
|
+
export type TerminalErrorCode = (typeof TERMINAL_ERROR_CODE)[keyof typeof TERMINAL_ERROR_CODE];
|
|
61
|
+
/** True when `code` is one of the terminal lifecycle verdicts above. */
|
|
62
|
+
export declare function isTerminalErrorCode(code: string | null | undefined): code is TerminalErrorCode;
|
|
42
63
|
export { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';
|
|
43
64
|
/**
|
|
44
65
|
* Max reconnect resends of one un-acked send before its optimistic row is flipped
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,QAAQ;gBACvB,OAAO,SAAuB;CAI3C;AAED,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB,CAOlF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC1E,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,QAAQ;gBACvB,OAAO,SAAuB;CAI3C;AAED,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB,CAOlF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC1E,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAExF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB;IAC9B,yEAAyE;;IAEzE,iFAAiF;;CAEzE,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,OAAO,mBAAmB,CAAC,CAAC;AAE/F,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,iBAAiB,CAE9F;AAID,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,uBAAuB,QAAS,CAAC"}
|
package/dist/realtime/shared.js
CHANGED
|
@@ -43,6 +43,28 @@ export const defaultTimers = {
|
|
|
43
43
|
clearTimeout: (h) => clearTimeout(h),
|
|
44
44
|
now: () => Date.now(),
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The TERMINAL lifecycle verdicts both realtime clients surface on
|
|
48
|
+
* `lastErrorCode` — the two codes that mean "this lifecycle stopped on an
|
|
49
|
+
* authoritative NO", as opposed to a transient/protocol code (`send-failed`,
|
|
50
|
+
* `blocked-word`, …) that leaves the socket running.
|
|
51
|
+
*
|
|
52
|
+
* ONE declaration for both clients: each sets and clears the same two strings,
|
|
53
|
+
* and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.
|
|
54
|
+
* A terminal verdict is terminal WITHIN its lifecycle; a later explicit
|
|
55
|
+
* `connect()` is a fresh attempt the authority may legitimately re-deny, so the
|
|
56
|
+
* stale code is cleared there (never carried into the new lifecycle's state).
|
|
57
|
+
*/
|
|
58
|
+
export const TERMINAL_ERROR_CODE = {
|
|
59
|
+
/** A 4403 REVOKED close: access was pulled while the socket was open. */
|
|
60
|
+
REVOKED: 'revoked',
|
|
61
|
+
/** The grant provider threw `ChatAccessDeniedError` — a terminal policy "no". */
|
|
62
|
+
ACCESS_DENIED: 'access-denied',
|
|
63
|
+
};
|
|
64
|
+
/** True when `code` is one of the terminal lifecycle verdicts above. */
|
|
65
|
+
export function isTerminalErrorCode(code) {
|
|
66
|
+
return code === TERMINAL_ERROR_CODE.REVOKED || code === TERMINAL_ERROR_CODE.ACCESS_DENIED;
|
|
67
|
+
}
|
|
46
68
|
// The client-agreed limits live in the wire contract; re-export to keep this
|
|
47
69
|
// module's public surface stable.
|
|
48
70
|
export { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,yDAAyD;AACzD,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AAWjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IACzE,kBAAkB,GAAG,IAAI,CAAC;IACnC,YAAY,OAAO,GAAG,oBAAoB;QACxC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,OAAO,CACL,GAAG,YAAY,qBAAqB;QACpC,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,KAAK,IAAI;YACX,GAAwC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CACzE,CAAC;AACJ,CAAC;AASD,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAKF,6EAA6E;AAC7E,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC","sourcesContent":["// Shared injected contracts + tunable constants for the realtime transport.\n//\n// The client-agreed limits (`HEARTBEAT_MS`, `TYPING_COALESCE_MS`,\n// `HISTORY_PAGE_MAX`) come from the one wire contract in\n// `@ttt-productions/chat-schemas`; they are re-exported here so the transport's\n// existing import sites are unchanged. The worker runtime is the authority and\n// rejects anything out of bounds, but the client uses these to coalesce and\n// paginate politely. The remaining constants below are client-only retry policy.\n\n/**\n * Mints a short-lived chat grant token for the socket's scope. The app wires this\n * to its `mintChatGrant` callable (channel grant for a ChannelClient, inbox grant\n * for an InboxClient). Called on every connect AND once on a 4401 re-grant. The\n * app owns the Firestore permission check, the scope shape, caching to a React\n * Query staleTime < grant exp, and returning a FRESH token when re-invoked.\n */\nexport type GrantProvider = () => Promise<string>;\n\n/**\n * Package-owned, Firebase-FREE terminal access-denial signal. A grant provider\n * throws this (from `GrantProvider`) when the authoritative authorization answer\n * is a genuine, terminal \"no\" — as opposed to a transient/retryable mint failure.\n *\n * The transport treats it specially: instead of the normal reconnect backoff it\n * STOPS reconnecting, closes the lifecycle, fails any pending optimistic sends,\n * and surfaces the stable `access-denied` error code (which the React hook maps to\n * `allowed: false` so the shell renders its no-access surface instead of an eternal\n * loader). Every other thrown error stays transient and reconnects as before.\n *\n * This is deliberately generic: the consuming app translates its own backend\n * denial (e.g. a Firebase `functions/permission-denied`) into this class at the\n * grant-provider boundary. The package never imports or names Firebase. Consumers\n * that cannot subclass may instead throw any error carrying `isChatAccessDenied:\n * true` — `isChatAccessDeniedError` recognizes both (also cross-realm safe).\n */\nexport class ChatAccessDeniedError extends Error {\n /** Duck-typed marker so detection survives module-realm / re-throw boundaries. */\n readonly isChatAccessDenied = true;\n constructor(message = 'Chat access denied') {\n super(message);\n this.name = 'ChatAccessDeniedError';\n }\n}\n\n/** True when `err` is a terminal chat access denial (class instance or duck-typed marker). */\nexport function isChatAccessDeniedError(err: unknown): err is ChatAccessDeniedError {\n return (\n err instanceof ChatAccessDeniedError ||\n (typeof err === 'object' &&\n err !== null &&\n (err as { isChatAccessDenied?: unknown }).isChatAccessDenied === true)\n );\n}\n\n/** Injectable timers so the transport runs deterministically under fake timers in tests. */\nexport interface TransportTimers {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void;\n now: () => number;\n}\n\nexport const defaultTimers: TransportTimers = {\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n now: () => Date.now(),\n};\n\n/** Connection status surfaced to the UI. */\nexport type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n// The client-agreed limits live in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';\n\n/**\n * Max reconnect resends of one un-acked send before its optimistic row is flipped\n * to a visible failed state (retry affordance) instead of a permanent ghost. Each\n * reconnect that resumes with the pending send still un-acked counts one attempt.\n */\nexport const MAX_PENDING_SEND_ATTEMPTS = 5;\n\n/**\n * Max wall-clock age of an un-acked pending send before it is flipped to failed on\n * the next resume, even if the attempt cap hasn't been hit (a long offline window).\n */\nexport const PENDING_SEND_MAX_AGE_MS = 60_000;\n\n/**\n * Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has\n * rejected a send as retryable — a correlated `send-rejected` frame OR the legacy\n * uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the\n * server's own hint until this budget elapses, and is NEVER failed by the generic\n * {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect\n * resends). 90 s comfortably covers the backend projection/grant-bootstrap window\n * without stranding an optimistic bubble on \"Sending…\" the way the old ~10 s\n * five-attempt cap did.\n */\nexport const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;\n\n/**\n * Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a\n * resend, so a pathological (but schema-valid) hint can neither hammer the socket\n * nor park a send for minutes.\n */\nexport const SEND_RETRY_MIN_DELAY_MS = 250;\nexport const SEND_RETRY_MAX_DELAY_MS = 30_000;\n"]}
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,yDAAyD;AACzD,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AAWjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IACzE,kBAAkB,GAAG,IAAI,CAAC;IACnC,YAAY,OAAO,GAAG,oBAAoB;QACxC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,OAAO,CACL,GAAG,YAAY,qBAAqB;QACpC,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,KAAK,IAAI;YACX,GAAwC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CACzE,CAAC;AACJ,CAAC;AASD,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAKF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,yEAAyE;IACzE,OAAO,EAAE,SAAS;IAClB,iFAAiF;IACjF,aAAa,EAAE,eAAe;CACtB,CAAC;AAIX,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,OAAO,IAAI,KAAK,mBAAmB,CAAC,OAAO,IAAI,IAAI,KAAK,mBAAmB,CAAC,aAAa,CAAC;AAC5F,CAAC;AAED,6EAA6E;AAC7E,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC","sourcesContent":["// Shared injected contracts + tunable constants for the realtime transport.\n//\n// The client-agreed limits (`HEARTBEAT_MS`, `TYPING_COALESCE_MS`,\n// `HISTORY_PAGE_MAX`) come from the one wire contract in\n// `@ttt-productions/chat-schemas`; they are re-exported here so the transport's\n// existing import sites are unchanged. The worker runtime is the authority and\n// rejects anything out of bounds, but the client uses these to coalesce and\n// paginate politely. The remaining constants below are client-only retry policy.\n\n/**\n * Mints a short-lived chat grant token for the socket's scope. The app wires this\n * to its `mintChatGrant` callable (channel grant for a ChannelClient, inbox grant\n * for an InboxClient). Called on every connect AND once on a 4401 re-grant. The\n * app owns the Firestore permission check, the scope shape, caching to a React\n * Query staleTime < grant exp, and returning a FRESH token when re-invoked.\n */\nexport type GrantProvider = () => Promise<string>;\n\n/**\n * Package-owned, Firebase-FREE terminal access-denial signal. A grant provider\n * throws this (from `GrantProvider`) when the authoritative authorization answer\n * is a genuine, terminal \"no\" — as opposed to a transient/retryable mint failure.\n *\n * The transport treats it specially: instead of the normal reconnect backoff it\n * STOPS reconnecting, closes the lifecycle, fails any pending optimistic sends,\n * and surfaces the stable `access-denied` error code (which the React hook maps to\n * `allowed: false` so the shell renders its no-access surface instead of an eternal\n * loader). Every other thrown error stays transient and reconnects as before.\n *\n * This is deliberately generic: the consuming app translates its own backend\n * denial (e.g. a Firebase `functions/permission-denied`) into this class at the\n * grant-provider boundary. The package never imports or names Firebase. Consumers\n * that cannot subclass may instead throw any error carrying `isChatAccessDenied:\n * true` — `isChatAccessDeniedError` recognizes both (also cross-realm safe).\n */\nexport class ChatAccessDeniedError extends Error {\n /** Duck-typed marker so detection survives module-realm / re-throw boundaries. */\n readonly isChatAccessDenied = true;\n constructor(message = 'Chat access denied') {\n super(message);\n this.name = 'ChatAccessDeniedError';\n }\n}\n\n/** True when `err` is a terminal chat access denial (class instance or duck-typed marker). */\nexport function isChatAccessDeniedError(err: unknown): err is ChatAccessDeniedError {\n return (\n err instanceof ChatAccessDeniedError ||\n (typeof err === 'object' &&\n err !== null &&\n (err as { isChatAccessDenied?: unknown }).isChatAccessDenied === true)\n );\n}\n\n/** Injectable timers so the transport runs deterministically under fake timers in tests. */\nexport interface TransportTimers {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void;\n now: () => number;\n}\n\nexport const defaultTimers: TransportTimers = {\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n now: () => Date.now(),\n};\n\n/** Connection status surfaced to the UI. */\nexport type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/**\n * The TERMINAL lifecycle verdicts both realtime clients surface on\n * `lastErrorCode` — the two codes that mean \"this lifecycle stopped on an\n * authoritative NO\", as opposed to a transient/protocol code (`send-failed`,\n * `blocked-word`, …) that leaves the socket running.\n *\n * ONE declaration for both clients: each sets and clears the same two strings,\n * and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.\n * A terminal verdict is terminal WITHIN its lifecycle; a later explicit\n * `connect()` is a fresh attempt the authority may legitimately re-deny, so the\n * stale code is cleared there (never carried into the new lifecycle's state).\n */\nexport const TERMINAL_ERROR_CODE = {\n /** A 4403 REVOKED close: access was pulled while the socket was open. */\n REVOKED: 'revoked',\n /** The grant provider threw `ChatAccessDeniedError` — a terminal policy \"no\". */\n ACCESS_DENIED: 'access-denied',\n} as const;\n\nexport type TerminalErrorCode = (typeof TERMINAL_ERROR_CODE)[keyof typeof TERMINAL_ERROR_CODE];\n\n/** True when `code` is one of the terminal lifecycle verdicts above. */\nexport function isTerminalErrorCode(code: string | null | undefined): code is TerminalErrorCode {\n return code === TERMINAL_ERROR_CODE.REVOKED || code === TERMINAL_ERROR_CODE.ACCESS_DENIED;\n}\n\n// The client-agreed limits live in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';\n\n/**\n * Max reconnect resends of one un-acked send before its optimistic row is flipped\n * to a visible failed state (retry affordance) instead of a permanent ghost. Each\n * reconnect that resumes with the pending send still un-acked counts one attempt.\n */\nexport const MAX_PENDING_SEND_ATTEMPTS = 5;\n\n/**\n * Max wall-clock age of an un-acked pending send before it is flipped to failed on\n * the next resume, even if the attempt cap hasn't been hit (a long offline window).\n */\nexport const PENDING_SEND_MAX_AGE_MS = 60_000;\n\n/**\n * Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has\n * rejected a send as retryable — a correlated `send-rejected` frame OR the legacy\n * uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the\n * server's own hint until this budget elapses, and is NEVER failed by the generic\n * {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect\n * resends). 90 s comfortably covers the backend projection/grant-bootstrap window\n * without stranding an optimistic bubble on \"Sending…\" the way the old ~10 s\n * five-attempt cap did.\n */\nexport const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;\n\n/**\n * Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a\n * resend, so a pathological (but schema-valid) hint can neither hammer the socket\n * nor park a send for minutes.\n */\nexport const SEND_RETRY_MIN_DELAY_MS = 250;\nexport const SEND_RETRY_MAX_DELAY_MS = 30_000;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/chat-react",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "Chat React UI, hooks, and Firebase-client adapter for TTT Productions apps (realtime newest window + infinite older pagination)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"@ttt-productions/chat-core": "^0.11.1",
|
|
64
64
|
"@ttt-productions/chat-schemas": "^0.10.1",
|
|
65
65
|
"@ttt-productions/realtime-core": "^0.1.3",
|
|
66
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
67
|
-
"@ttt-productions/upload-ui": "^0.7.
|
|
66
|
+
"@ttt-productions/ui-core": "^0.11.12",
|
|
67
|
+
"@ttt-productions/upload-ui": "^0.7.8",
|
|
68
68
|
"@ttt-productions/mobile-core": "^0.9.1",
|
|
69
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
69
|
+
"@ttt-productions/firebase-helpers": "^0.12.5"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@ttt-productions/query-core": "^0.14.1",
|