@ttt-productions/chat-react 0.12.9 → 0.12.10
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 +41 -3
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +218 -30
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/shared.d.ts +18 -0
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +18 -0
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/wire.d.ts +6 -3
- package/dist/realtime/wire.d.ts.map +1 -1
- package/dist/realtime/wire.js +3 -1
- package/dist/realtime/wire.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +33 -1
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/package.json +3 -3
|
@@ -107,13 +107,51 @@ export declare class ChannelClient {
|
|
|
107
107
|
/**
|
|
108
108
|
* Flip an un-acked optimistic row to a visible failed state. The pending entry is
|
|
109
109
|
* RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME
|
|
110
|
-
* clientMessageId; the row is marked `meta.sendFailed`
|
|
111
|
-
*
|
|
112
|
-
*
|
|
110
|
+
* clientMessageId; the row is marked `meta.sendFailed` (plus the correlated
|
|
111
|
+
* `sendFailureCode`/`sendRetryable` when a `send-rejected` frame supplied them) so
|
|
112
|
+
* the default renderer shows the right reason + retry affordance rather than an
|
|
113
|
+
* indistinguishable "sent" bubble. A late ack/echo still reconciles + clears it.
|
|
114
|
+
*
|
|
115
|
+
* `code`/`retryable` are present for a correlated rejection (terminal) or a
|
|
116
|
+
* budget-exhausted server-retryable send; absent for a blind reconnect give-up
|
|
117
|
+
* (which keeps the neutral `send-failed` code).
|
|
113
118
|
*/
|
|
114
119
|
private failPendingSend;
|
|
120
|
+
/** Clear a pending send's per-send retry timer (idempotent). */
|
|
121
|
+
private clearSendRetryTimer;
|
|
122
|
+
/**
|
|
123
|
+
* Stop tracking a send (acked / reconciled by an echo / history row): cancel its
|
|
124
|
+
* per-send retry timer first so a late reconcile never leaves a stray timer that
|
|
125
|
+
* would resend after the send already landed, then drop it from the map.
|
|
126
|
+
*/
|
|
127
|
+
private clearPendingSend;
|
|
115
128
|
/** One timer for the membership-pending re-send window; a newer frame resets it. */
|
|
116
129
|
private scheduleMembershipPendingRetry;
|
|
130
|
+
/**
|
|
131
|
+
* Handle a CORRELATED `send-rejected` frame (the target protocol): parse the
|
|
132
|
+
* untrusted payload with the shared schema, correlate to EXACTLY ONE pending send
|
|
133
|
+
* by clientMessageId, and either fail just that bubble (terminal) or schedule a
|
|
134
|
+
* single per-send resend (retryable).
|
|
135
|
+
*
|
|
136
|
+
* A frame that fails validation is NOT simply dropped when it still names one of
|
|
137
|
+
* OUR pending sends: unlike an unknown frame type, an id-correlated rejection is
|
|
138
|
+
* the server explicitly telling us this send did not land — swallowing it would
|
|
139
|
+
* strand the bubble on "Sending…" forever (no further frame, no reconnect, no
|
|
140
|
+
* budget check ever runs). A future/unknown rejection code (version skew during a
|
|
141
|
+
* rollout) therefore fails that send CLOSED with the neutral copy + Retry. A
|
|
142
|
+
* malformed frame with no recognizable pending id is dropped (forward-compat
|
|
143
|
+
* noise), never surfaced as an error.
|
|
144
|
+
*/
|
|
145
|
+
private applySendRejected;
|
|
146
|
+
/**
|
|
147
|
+
* Schedule EXACTLY ONE per-send resend of a correlated retryable rejection (never
|
|
148
|
+
* stacks — a newer rejection clears the prior timer first). When it fires it
|
|
149
|
+
* re-sends the SAME clientMessageId (the DO dedups) unless the wall-clock budget
|
|
150
|
+
* has elapsed, in which case the send fails with its recorded reason.
|
|
151
|
+
*/
|
|
152
|
+
private scheduleSendRetry;
|
|
153
|
+
/** Clamp a server `retryAfterMs` hint into a sane resend delay (defaulting when absent). */
|
|
154
|
+
private clampRetryDelay;
|
|
117
155
|
/**
|
|
118
156
|
* Flip EVERY un-acked pending send to the visible failed state. Called on the
|
|
119
157
|
* terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.
|
|
17
17
|
import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
18
|
-
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
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
|
-
import { defaultTimers, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, } from './shared.js';
|
|
20
|
+
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
21
|
const INITIAL = {
|
|
22
22
|
status: 'idle',
|
|
23
23
|
messages: [],
|
|
@@ -182,11 +182,29 @@ export class ChannelClient {
|
|
|
182
182
|
// re-queues it; the automatic reconnect resend leaves it alone.
|
|
183
183
|
if (pending.failed)
|
|
184
184
|
continue;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
if (pending.serverRetryable) {
|
|
186
|
+
// The Worker declared this send retryable — give-up is the wall-clock budget
|
|
187
|
+
// ONLY (the 5-attempt cap must not fail a server-declared transient after
|
|
188
|
+
// ~10 s while the backend projection window is up to 60+ s).
|
|
189
|
+
if (now - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {
|
|
190
|
+
this.failPendingSend(pending.clientMessageId, pending.lastRejectionCode, true);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// This reconnect resend supersedes any per-send timer scheduled before the
|
|
194
|
+
// socket dropped — clear it so the send doesn't go out twice (the DO would
|
|
195
|
+
// dedup, but the duplicate frame is pure waste). A fresh rejection frame
|
|
196
|
+
// re-schedules as usual.
|
|
197
|
+
this.clearSendRetryTimer(pending);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// A blind reconnect resend of a send the server has said nothing about: the
|
|
201
|
+
// generic attempt/age caps still protect against a permanent ghost.
|
|
202
|
+
const tooOld = now - pending.sentAt >= PENDING_SEND_MAX_AGE_MS;
|
|
203
|
+
const tooManyAttempts = pending.attempts >= MAX_PENDING_SEND_ATTEMPTS;
|
|
204
|
+
if (tooOld || tooManyAttempts) {
|
|
205
|
+
this.failPendingSend(pending.clientMessageId);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
190
208
|
}
|
|
191
209
|
const sent = this.sendFrame(CLIENT_FRAME.SEND, {
|
|
192
210
|
clientMessageId: pending.clientMessageId,
|
|
@@ -200,19 +218,52 @@ export class ChannelClient {
|
|
|
200
218
|
/**
|
|
201
219
|
* Flip an un-acked optimistic row to a visible failed state. The pending entry is
|
|
202
220
|
* RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME
|
|
203
|
-
* clientMessageId; the row is marked `meta.sendFailed`
|
|
204
|
-
*
|
|
205
|
-
*
|
|
221
|
+
* clientMessageId; the row is marked `meta.sendFailed` (plus the correlated
|
|
222
|
+
* `sendFailureCode`/`sendRetryable` when a `send-rejected` frame supplied them) so
|
|
223
|
+
* the default renderer shows the right reason + retry affordance rather than an
|
|
224
|
+
* indistinguishable "sent" bubble. A late ack/echo still reconciles + clears it.
|
|
225
|
+
*
|
|
226
|
+
* `code`/`retryable` are present for a correlated rejection (terminal) or a
|
|
227
|
+
* budget-exhausted server-retryable send; absent for a blind reconnect give-up
|
|
228
|
+
* (which keeps the neutral `send-failed` code).
|
|
206
229
|
*/
|
|
207
|
-
failPendingSend(clientMessageId) {
|
|
230
|
+
failPendingSend(clientMessageId, code, retryable) {
|
|
208
231
|
const pending = this.pendingSends.get(clientMessageId);
|
|
209
232
|
if (!pending || pending.failed)
|
|
210
233
|
return;
|
|
211
234
|
pending.failed = true;
|
|
235
|
+
// A terminal/exhausted send never retries on a timer — cancel any scheduled one.
|
|
236
|
+
this.clearSendRetryTimer(pending);
|
|
212
237
|
const next = this.state.messages.map((m) => m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId
|
|
213
|
-
? {
|
|
238
|
+
? {
|
|
239
|
+
...m,
|
|
240
|
+
meta: {
|
|
241
|
+
...m.meta,
|
|
242
|
+
sendFailed: true,
|
|
243
|
+
...(code ? { sendFailureCode: code } : {}),
|
|
244
|
+
...(retryable != null ? { sendRetryable: retryable } : {}),
|
|
245
|
+
},
|
|
246
|
+
}
|
|
214
247
|
: m);
|
|
215
|
-
this.setState({ messages: next, lastErrorCode: 'send-failed' });
|
|
248
|
+
this.setState({ messages: next, lastErrorCode: code ?? 'send-failed' });
|
|
249
|
+
}
|
|
250
|
+
/** Clear a pending send's per-send retry timer (idempotent). */
|
|
251
|
+
clearSendRetryTimer(pending) {
|
|
252
|
+
if (pending.retryTimer != null) {
|
|
253
|
+
this.timers.clearTimeout(pending.retryTimer);
|
|
254
|
+
pending.retryTimer = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Stop tracking a send (acked / reconciled by an echo / history row): cancel its
|
|
259
|
+
* per-send retry timer first so a late reconcile never leaves a stray timer that
|
|
260
|
+
* would resend after the send already landed, then drop it from the map.
|
|
261
|
+
*/
|
|
262
|
+
clearPendingSend(clientMessageId) {
|
|
263
|
+
const pending = this.pendingSends.get(clientMessageId);
|
|
264
|
+
if (pending)
|
|
265
|
+
this.clearSendRetryTimer(pending);
|
|
266
|
+
this.pendingSends.delete(clientMessageId);
|
|
216
267
|
}
|
|
217
268
|
/** One timer for the membership-pending re-send window; a newer frame resets it. */
|
|
218
269
|
scheduleMembershipPendingRetry(delayMs) {
|
|
@@ -225,6 +276,116 @@ export class ChannelClient {
|
|
|
225
276
|
this.resendPendingSends();
|
|
226
277
|
}, delayMs);
|
|
227
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Handle a CORRELATED `send-rejected` frame (the target protocol): parse the
|
|
281
|
+
* untrusted payload with the shared schema, correlate to EXACTLY ONE pending send
|
|
282
|
+
* by clientMessageId, and either fail just that bubble (terminal) or schedule a
|
|
283
|
+
* single per-send resend (retryable).
|
|
284
|
+
*
|
|
285
|
+
* A frame that fails validation is NOT simply dropped when it still names one of
|
|
286
|
+
* OUR pending sends: unlike an unknown frame type, an id-correlated rejection is
|
|
287
|
+
* the server explicitly telling us this send did not land — swallowing it would
|
|
288
|
+
* strand the bubble on "Sending…" forever (no further frame, no reconnect, no
|
|
289
|
+
* budget check ever runs). A future/unknown rejection code (version skew during a
|
|
290
|
+
* rollout) therefore fails that send CLOSED with the neutral copy + Retry. A
|
|
291
|
+
* malformed frame with no recognizable pending id is dropped (forward-compat
|
|
292
|
+
* noise), never surfaced as an error.
|
|
293
|
+
*/
|
|
294
|
+
applySendRejected(payload) {
|
|
295
|
+
const parsed = ChatSendRejectedPayloadSchema.safeParse(payload);
|
|
296
|
+
if (!parsed.success) {
|
|
297
|
+
// Defensive fail-closed: if the unparseable rejection still carries a string
|
|
298
|
+
// id matching one of our un-failed pending sends, that send was rejected by a
|
|
299
|
+
// server speaking a newer/buggy dialect — fail it visibly (neutral code-less
|
|
300
|
+
// "Couldn't send" + Retry) instead of stranding it.
|
|
301
|
+
const rawId = payload.clientMessageId;
|
|
302
|
+
if (typeof rawId === 'string' && rawId.length > 0 && rawId.length <= 200) {
|
|
303
|
+
const orphaned = this.pendingSends.get(rawId);
|
|
304
|
+
if (orphaned && !orphaned.failed)
|
|
305
|
+
this.failPendingSend(rawId);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const { clientMessageId, code, retryable, retryAfterMs } = parsed.data;
|
|
310
|
+
const pending = this.pendingSends.get(clientMessageId);
|
|
311
|
+
// No matching pending send — already acked/reconciled, or never ours. A late
|
|
312
|
+
// ack/echo (which may have already landed) always wins; nothing to correlate.
|
|
313
|
+
if (!pending || pending.failed)
|
|
314
|
+
return;
|
|
315
|
+
if (!retryable) {
|
|
316
|
+
// Terminal: an unchanged resend cannot succeed (blocked-word / archived /
|
|
317
|
+
// deleted). Fail ONLY this bubble, recording the reason + terminal class.
|
|
318
|
+
this.failPendingSend(clientMessageId, code, false);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Retryable: server-declared, so switch this send onto the wall-clock budget
|
|
322
|
+
// (the 5-attempt cap no longer applies) and record the reason for a possible
|
|
323
|
+
// budget-exhausted failure.
|
|
324
|
+
pending.serverRetryable = true;
|
|
325
|
+
pending.lastRejectionCode = code;
|
|
326
|
+
// If the budget already elapsed (a long stream of retryable rejections), fail
|
|
327
|
+
// now rather than scheduling yet another doomed resend.
|
|
328
|
+
if (this.timers.now() - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {
|
|
329
|
+
this.failPendingSend(clientMessageId, code, true);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.scheduleSendRetry(clientMessageId, this.clampRetryDelay(retryAfterMs));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Schedule EXACTLY ONE per-send resend of a correlated retryable rejection (never
|
|
336
|
+
* stacks — a newer rejection clears the prior timer first). When it fires it
|
|
337
|
+
* re-sends the SAME clientMessageId (the DO dedups) unless the wall-clock budget
|
|
338
|
+
* has elapsed, in which case the send fails with its recorded reason.
|
|
339
|
+
*/
|
|
340
|
+
scheduleSendRetry(clientMessageId, delayMs) {
|
|
341
|
+
const pending = this.pendingSends.get(clientMessageId);
|
|
342
|
+
if (!pending)
|
|
343
|
+
return;
|
|
344
|
+
this.clearSendRetryTimer(pending);
|
|
345
|
+
pending.retryTimer = this.timers.setTimeout(() => {
|
|
346
|
+
pending.retryTimer = null;
|
|
347
|
+
if (this.closedByUs)
|
|
348
|
+
return;
|
|
349
|
+
const current = this.pendingSends.get(clientMessageId);
|
|
350
|
+
if (!current || current.failed)
|
|
351
|
+
return;
|
|
352
|
+
if (this.timers.now() - current.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {
|
|
353
|
+
this.failPendingSend(clientMessageId, current.lastRejectionCode, true);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// A server-retryable resend does NOT count toward the attempt cap (that cap is
|
|
357
|
+
// for blind reconnect resends only) — leave `attempts` untouched.
|
|
358
|
+
this.sendFrame(CLIENT_FRAME.SEND, {
|
|
359
|
+
clientMessageId: current.clientMessageId,
|
|
360
|
+
text: current.text,
|
|
361
|
+
replyTo: current.replyTo,
|
|
362
|
+
});
|
|
363
|
+
// Budget backstop: the contract says every send gets an ack or a rejection,
|
|
364
|
+
// and either one replaces/clears this timer. If the server goes silent (a
|
|
365
|
+
// contract violation), NOTHING else would ever re-check the budget on a
|
|
366
|
+
// stable socket — so arm a deadline check that fails the send at budget end.
|
|
367
|
+
// This makes SERVER_RETRYABLE_MAX_AGE_MS a hard ceiling, not a best effort.
|
|
368
|
+
const remainingMs = SERVER_RETRYABLE_MAX_AGE_MS - (this.timers.now() - current.sentAt) + SEND_RETRY_MIN_DELAY_MS;
|
|
369
|
+
current.retryTimer = this.timers.setTimeout(() => {
|
|
370
|
+
current.retryTimer = null;
|
|
371
|
+
if (this.closedByUs)
|
|
372
|
+
return;
|
|
373
|
+
const atDeadline = this.pendingSends.get(clientMessageId);
|
|
374
|
+
if (!atDeadline || atDeadline.failed)
|
|
375
|
+
return;
|
|
376
|
+
if (this.timers.now() - atDeadline.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {
|
|
377
|
+
this.failPendingSend(clientMessageId, atDeadline.lastRejectionCode, true);
|
|
378
|
+
}
|
|
379
|
+
}, remainingMs);
|
|
380
|
+
}, delayMs);
|
|
381
|
+
}
|
|
382
|
+
/** Clamp a server `retryAfterMs` hint into a sane resend delay (defaulting when absent). */
|
|
383
|
+
clampRetryDelay(retryAfterMs) {
|
|
384
|
+
const raw = typeof retryAfterMs === 'number' && retryAfterMs > 0
|
|
385
|
+
? retryAfterMs
|
|
386
|
+
: MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;
|
|
387
|
+
return Math.min(Math.max(raw, SEND_RETRY_MIN_DELAY_MS), SEND_RETRY_MAX_DELAY_MS);
|
|
388
|
+
}
|
|
228
389
|
/**
|
|
229
390
|
* Flip EVERY un-acked pending send to the visible failed state. Called on the
|
|
230
391
|
* terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send
|
|
@@ -248,7 +409,12 @@ export class ChannelClient {
|
|
|
248
409
|
const pending = this.pendingSends.get(clientMessageId);
|
|
249
410
|
if (!pending)
|
|
250
411
|
return false;
|
|
412
|
+
// A manual retry supersedes any scheduled server-retryable resend and starts a
|
|
413
|
+
// fresh budget from now; the server re-declares retryability if it rejects again.
|
|
414
|
+
this.clearSendRetryTimer(pending);
|
|
251
415
|
pending.failed = false;
|
|
416
|
+
pending.serverRetryable = false;
|
|
417
|
+
pending.lastRejectionCode = undefined;
|
|
252
418
|
pending.sentAt = this.timers.now();
|
|
253
419
|
const sent = this.sendFrame(CLIENT_FRAME.SEND, {
|
|
254
420
|
clientMessageId: pending.clientMessageId,
|
|
@@ -256,9 +422,14 @@ export class ChannelClient {
|
|
|
256
422
|
replyTo: pending.replyTo,
|
|
257
423
|
});
|
|
258
424
|
pending.attempts = sent ? 1 : 0;
|
|
259
|
-
const next = this.state.messages.map((m) =>
|
|
260
|
-
|
|
261
|
-
|
|
425
|
+
const next = this.state.messages.map((m) => {
|
|
426
|
+
if (!(m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed)) {
|
|
427
|
+
return m;
|
|
428
|
+
}
|
|
429
|
+
// Back to the normal pending look — drop every failure marker.
|
|
430
|
+
const { sendFailed: _sendFailed, sendFailureCode: _sendFailureCode, sendRetryable: _sendRetryable, ...restMeta } = m.meta;
|
|
431
|
+
return { ...m, meta: { ...restMeta } };
|
|
432
|
+
});
|
|
262
433
|
this.setState({ messages: next, lastErrorCode: null });
|
|
263
434
|
return true;
|
|
264
435
|
}
|
|
@@ -343,16 +514,26 @@ export class ChannelClient {
|
|
|
343
514
|
return this.applyTyping(payload.uid);
|
|
344
515
|
case 'snapshot':
|
|
345
516
|
return this.applyChannelSnapshot(payload);
|
|
517
|
+
case 'send-rejected':
|
|
518
|
+
return this.applySendRejected(payload);
|
|
346
519
|
case 'error': {
|
|
347
520
|
const code = String(payload.code ?? 'error');
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
521
|
+
// ROLLOUT COMPATIBILITY ONLY (not the target protocol): a Worker that predates
|
|
522
|
+
// the correlated `send-rejected` frame emits an UNCORRELATED
|
|
523
|
+
// error{code:'membership-pending'} with no clientMessageId, so we cannot target
|
|
524
|
+
// one send. Keep the send(s) pending (no visible error — the bubble stays
|
|
525
|
+
// "Sending…"), mark every in-flight send server-retryable so the wall-clock
|
|
526
|
+
// budget (NOT the 5-attempt cap) governs give-up, and re-send all after the
|
|
527
|
+
// server-suggested delay. The correlated path below is the target protocol.
|
|
353
528
|
if (code === MEMBERSHIP_PENDING_CODE) {
|
|
354
529
|
const raw = payload.retryAfterMs;
|
|
355
|
-
const delay = typeof raw === 'number'
|
|
530
|
+
const delay = this.clampRetryDelay(typeof raw === 'number' ? raw : undefined);
|
|
531
|
+
for (const pending of this.pendingSends.values()) {
|
|
532
|
+
if (!pending.failed) {
|
|
533
|
+
pending.serverRetryable = true;
|
|
534
|
+
pending.lastRejectionCode = MEMBERSHIP_PENDING_CODE;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
356
537
|
return this.scheduleMembershipPendingRetry(delay);
|
|
357
538
|
}
|
|
358
539
|
return this.setState({ lastErrorCode: code });
|
|
@@ -482,9 +663,9 @@ export class ChannelClient {
|
|
|
482
663
|
mergeMessage(mapped, row) {
|
|
483
664
|
// If this canonical row carries a clientMessageId we still have pending (the
|
|
484
665
|
// sender's own broadcast, or a resume delta that included our send), the send
|
|
485
|
-
// landed — stop tracking it for reconnect resend.
|
|
666
|
+
// landed — stop tracking it for reconnect resend (and cancel any retry timer).
|
|
486
667
|
if (row.clientMessageId)
|
|
487
|
-
this.
|
|
668
|
+
this.clearPendingSend(row.clientMessageId);
|
|
488
669
|
const next = this.state.messages.filter((m) => m.messageId !== mapped.messageId &&
|
|
489
670
|
// drop the optimistic placeholder this server row reconciles
|
|
490
671
|
(m.meta?.clientMessageId !== row.clientMessageId || !m.meta?.optimistic));
|
|
@@ -501,14 +682,16 @@ export class ChannelClient {
|
|
|
501
682
|
// ack guarantees the seq even if the broadcast is dropped/reordered.
|
|
502
683
|
const seq = payload.seq;
|
|
503
684
|
const cmid = payload.clientMessageId;
|
|
504
|
-
// The send is acked — stop tracking it for reconnect resend
|
|
505
|
-
|
|
685
|
+
// The send is acked — stop tracking it for reconnect resend (and cancel any
|
|
686
|
+
// per-send retry timer so a scheduled resend never fires after it landed).
|
|
687
|
+
this.clearPendingSend(cmid);
|
|
506
688
|
const next = this.state.messages.map((m) => {
|
|
507
689
|
if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid)
|
|
508
690
|
return m;
|
|
509
|
-
// Strip any failed
|
|
510
|
-
//
|
|
511
|
-
|
|
691
|
+
// Strip any failed markers — a late ack (e.g. a retry raced the cap, or a
|
|
692
|
+
// correlated rejection was superseded) means the send actually landed, so the
|
|
693
|
+
// row must render as delivered, not failed.
|
|
694
|
+
const { sendFailed: _sendFailed, sendFailureCode: _sendFailureCode, sendRetryable: _sendRetryable, ...restMeta } = m.meta;
|
|
512
695
|
return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };
|
|
513
696
|
});
|
|
514
697
|
next.sort(sortBySeq);
|
|
@@ -527,7 +710,7 @@ export class ChannelClient {
|
|
|
527
710
|
for (const r of rows) {
|
|
528
711
|
if (r.clientMessageId) {
|
|
529
712
|
incomingCmids.add(r.clientMessageId);
|
|
530
|
-
this.
|
|
713
|
+
this.clearPendingSend(r.clientMessageId);
|
|
531
714
|
}
|
|
532
715
|
}
|
|
533
716
|
const bySeq = new Map();
|
|
@@ -601,6 +784,8 @@ export class ChannelClient {
|
|
|
601
784
|
sentAt: now,
|
|
602
785
|
attempts: 1,
|
|
603
786
|
failed: false,
|
|
787
|
+
serverRetryable: false,
|
|
788
|
+
retryTimer: null,
|
|
604
789
|
});
|
|
605
790
|
const echo = optimisticMessage({
|
|
606
791
|
clientMessageId: args.clientMessageId,
|
|
@@ -681,6 +866,9 @@ export class ChannelClient {
|
|
|
681
866
|
this.typingTimers.clear();
|
|
682
867
|
this.overlays.clear();
|
|
683
868
|
this.originalRows.clear();
|
|
869
|
+
// Cancel every per-send retry timer before dropping the tracking map.
|
|
870
|
+
for (const pending of this.pendingSends.values())
|
|
871
|
+
this.clearSendRetryTimer(pending);
|
|
684
872
|
this.pendingSends.clear();
|
|
685
873
|
if (this.reconnectTimer != null) {
|
|
686
874
|
this.timers.clearTimeout(this.reconnectTimer);
|
|
@@ -1 +1 @@
|
|
|
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 type WireMessageRow,\n type WireChannelSnapshot,\n type RevisionKind,\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 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} from './shared.js';\n\n/** An un-acked send tracked for reconnect resend (clientMessageId idempotency). */\ninterface PendingSend {\n clientMessageId: string;\n text: string;\n replyTo: { messageSeq: number; preview: string } | null;\n /** When the send was first attempted (for the age cap). 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 cap flipped this send to the visible failed state.\n * A failed entry is RETAINED (not deleted) so retrySend() can re-queue it with\n * the SAME clientMessageId; it is skipped by the automatic reconnect resend. */\n failed: boolean;\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\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 constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\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.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\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 if (isChatAccessDeniedError(err)) return this.denyAccessTerminally();\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 // 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 // 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();\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(): 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 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 const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n if (sent) pending.attempts += 1;\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` so the default renderer\n * shows a failure with a retry affordance rather than an indistinguishable\n * \"sent\" bubble. A late ack/echo still reconciles + clears it.\n */\n private failPendingSend(clientMessageId: string): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending || pending.failed) return;\n pending.failed = true;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId\n ? { ...m, meta: { ...m.meta, sendFailed: true } }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: 'send-failed' });\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();\n }, delayMs);\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 pending.failed = false;\n pending.sentAt = this.timers.now();\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n pending.attempts = sent ? 1 : 0;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed\n ? { ...m, meta: { ...m.meta, sendFailed: false } }\n : m,\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 if (this.closedByUs) {\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.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.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\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) return;\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message);\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 'error': {\n const code = String((payload as { code?: unknown }).code ?? 'error');\n // Retryable: membership row not yet synced to the DO. Keep the send pending\n // (no visible error — the optimistic bubble stays \"Sending…\") and re-send\n // after the server-suggested delay via the normal resend machinery, whose\n // attempt/age caps flip it to the visible failed state if the window never\n // closes. Everything else surfaces as before.\n if (code === MEMBERSHIP_PENDING_CODE) {\n const raw = (payload as { retryAfterMs?: unknown }).retryAfterMs;\n const delay = typeof raw === 'number' && raw > 0 ? raw : MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;\n return this.scheduleMembershipPendingRetry(delay);\n }\n return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n default:\n return; // unknown/optional types tolerated (forward compat)\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') return;\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) return;\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 if (original && this.state.messages.some((m) => m.messageId === messageId)) {\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)) return;\n if (typeof snap.lastMessageSeq !== 'number') return;\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\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 const delta = Array.isArray(snap.delta) ? snap.delta : [];\n for (const row of delta) this.applyMessage(row);\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\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 keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n private applyMessage(row: WireMessageRow): void {\n if (!row || typeof row.seq !== 'number') return;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\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.\n if (row.clientMessageId) this.pendingSends.delete(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 // The send is acked — stop tracking it for reconnect resend.\n this.pendingSends.delete(cmid);\n const next = this.state.messages.map((m) => {\n if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid) return m;\n // Strip any failed marker — a late ack (e.g. a retry raced the cap) means the\n // send actually landed, so the row must render as delivered, not failed.\n const { sendFailed: _sendFailed, ...restMeta } = 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 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.pendingSends.delete(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 }\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; replyTo?: { messageSeq: number; preview: string } | null }): 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 replyTo: args.replyTo ?? null,\n });\n if (!sent) {\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 replyTo: args.replyTo ?? null,\n sentAt: now,\n attempts: 1,\n failed: false,\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 replyTo: args.replyTo ? { messageId: seqToMessageId(args.replyTo.messageSeq), messagePreview: args.replyTo.preview } : null,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\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 this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\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 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 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 replyTo: { messageSeq: number; preview: string } | null;\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\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 constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\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.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\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 if (isChatAccessDeniedError(err)) return this.denyAccessTerminally();\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 // 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 // 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();\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(): 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 replyTo: pending.replyTo,\n });\n if (sent) pending.attempts += 1;\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 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();\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) this.failPendingSend(rawId);\n }\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) return;\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.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.failPendingSend(clientMessageId, code, true);\n return;\n }\n this.scheduleSendRetry(clientMessageId, this.clampRetryDelay(retryAfterMs));\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 this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: current.clientMessageId,\n text: current.text,\n replyTo: current.replyTo,\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 replyTo: pending.replyTo,\n });\n pending.attempts = sent ? 1 : 0;\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 if (this.closedByUs) {\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.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.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\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) return;\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message);\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 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 return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n default:\n return; // unknown/optional types tolerated (forward compat)\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') return;\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) return;\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 if (original && this.state.messages.some((m) => m.messageId === messageId)) {\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)) return;\n if (typeof snap.lastMessageSeq !== 'number') return;\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\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 const delta = Array.isArray(snap.delta) ? snap.delta : [];\n for (const row of delta) this.applyMessage(row);\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\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 keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n private applyMessage(row: WireMessageRow): void {\n if (!row || typeof row.seq !== 'number') return;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\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 // 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 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 }\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; replyTo?: { messageSeq: number; preview: string } | null }): 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 replyTo: args.replyTo ?? null,\n });\n if (!sent) {\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 replyTo: args.replyTo ?? null,\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 replyTo: args.replyTo ? { messageId: seqToMessageId(args.replyTo.messageSeq), messagePreview: args.replyTo.preview } : null,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\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 this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\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"]}
|
|
@@ -51,4 +51,22 @@ export declare const MAX_PENDING_SEND_ATTEMPTS = 5;
|
|
|
51
51
|
* the next resume, even if the attempt cap hasn't been hit (a long offline window).
|
|
52
52
|
*/
|
|
53
53
|
export declare const PENDING_SEND_MAX_AGE_MS = 60000;
|
|
54
|
+
/**
|
|
55
|
+
* Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has
|
|
56
|
+
* rejected a send as retryable — a correlated `send-rejected` frame OR the legacy
|
|
57
|
+
* uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the
|
|
58
|
+
* server's own hint until this budget elapses, and is NEVER failed by the generic
|
|
59
|
+
* {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect
|
|
60
|
+
* resends). 90 s comfortably covers the backend projection/grant-bootstrap window
|
|
61
|
+
* without stranding an optimistic bubble on "Sending…" the way the old ~10 s
|
|
62
|
+
* five-attempt cap did.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SERVER_RETRYABLE_MAX_AGE_MS = 90000;
|
|
65
|
+
/**
|
|
66
|
+
* Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a
|
|
67
|
+
* resend, so a pathological (but schema-valid) hint can neither hammer the socket
|
|
68
|
+
* nor park a send for minutes.
|
|
69
|
+
*/
|
|
70
|
+
export declare const SEND_RETRY_MIN_DELAY_MS = 250;
|
|
71
|
+
export declare const SEND_RETRY_MAX_DELAY_MS = 30000;
|
|
54
72
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -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;AAIxF,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"}
|
|
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;AAIxF,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
|
@@ -57,4 +57,22 @@ export const MAX_PENDING_SEND_ATTEMPTS = 5;
|
|
|
57
57
|
* the next resume, even if the attempt cap hasn't been hit (a long offline window).
|
|
58
58
|
*/
|
|
59
59
|
export const PENDING_SEND_MAX_AGE_MS = 60_000;
|
|
60
|
+
/**
|
|
61
|
+
* Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has
|
|
62
|
+
* rejected a send as retryable — a correlated `send-rejected` frame OR the legacy
|
|
63
|
+
* uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the
|
|
64
|
+
* server's own hint until this budget elapses, and is NEVER failed by the generic
|
|
65
|
+
* {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect
|
|
66
|
+
* resends). 90 s comfortably covers the backend projection/grant-bootstrap window
|
|
67
|
+
* without stranding an optimistic bubble on "Sending…" the way the old ~10 s
|
|
68
|
+
* five-attempt cap did.
|
|
69
|
+
*/
|
|
70
|
+
export const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;
|
|
71
|
+
/**
|
|
72
|
+
* Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a
|
|
73
|
+
* resend, so a pathological (but schema-valid) hint can neither hammer the socket
|
|
74
|
+
* nor park a send for minutes.
|
|
75
|
+
*/
|
|
76
|
+
export const SEND_RETRY_MIN_DELAY_MS = 250;
|
|
77
|
+
export const SEND_RETRY_MAX_DELAY_MS = 30_000;
|
|
60
78
|
//# sourceMappingURL=shared.js.map
|
|
@@ -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","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"]}
|
|
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"]}
|
package/dist/realtime/wire.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME, } from '@ttt-productions/chat-schemas';
|
|
2
|
-
export type { ChatCloseCode, ChannelRefTuple } from '@ttt-productions/chat-schemas';
|
|
3
|
-
import type { ChannelRefTuple } from '@ttt-productions/chat-schemas';
|
|
1
|
+
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME, ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
|
|
2
|
+
export type { ChatCloseCode, ChannelRefTuple, ChatSendRejectedPayload, ChatSendRejectionCode, } from '@ttt-productions/chat-schemas';
|
|
3
|
+
import type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';
|
|
4
4
|
/**
|
|
5
5
|
* The moderation revision `kind` (the worker runtime's revision input + the
|
|
6
6
|
* `revisions.kind` column). The EFFECTIVE state of a message is its
|
|
@@ -130,6 +130,9 @@ export type ServerFrame = {
|
|
|
130
130
|
code: string;
|
|
131
131
|
retryAfterMs?: number;
|
|
132
132
|
};
|
|
133
|
+
} | {
|
|
134
|
+
type: 'send-rejected';
|
|
135
|
+
payload: ChatSendRejectedPayload;
|
|
133
136
|
} | {
|
|
134
137
|
type: 'revision';
|
|
135
138
|
payload: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAiBA,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,IAAI,YAAY,EAC5B,YAAY,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
|
package/dist/realtime/wire.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// Re-export the canonical wire contract so this package's public surface is
|
|
15
15
|
// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names
|
|
16
16
|
// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).
|
|
17
|
-
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME,
|
|
17
|
+
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME,
|
|
18
|
+
// Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).
|
|
19
|
+
ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
|
|
18
20
|
import { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';
|
|
19
21
|
/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */
|
|
20
22
|
export function buildFrame(type, payload) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,IAAI,YAAY,EAC5B,YAAY,IAAI,YAAY,
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mFAAmF;AACnF,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,2EAA2E;AAC3E,sEAAsE;AACtE,iFAAiF;AACjF,gFAAgF;AAChF,8EAA8E;AAE9E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,IAAI,YAAY,EAC5B,YAAY,IAAI,YAAY;AAC5B,2EAA2E;AAC3E,6BAA6B,EAC7B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,+BAA+B,CAAC;AAQvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA0HlE,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAA0E,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,eAAe,CAAC,CAA0C;IACxE,OAAO,KAAK,CAAC,OAAO,CAAE,CAAuB,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["// Chat realtime WIRE PROTOCOL (Contract C) — the client side of the Channel +\n// Inbox Durable Object protocol. The canonical protocol declarations (subprotocol,\n// frame-kind maps, close codes, channel-ref tuple, grant scope, client-agreed\n// limits) are owned by `@ttt-productions/chat-schemas`; this file consumes them\n// and adds the client-side raw row/frame shapes the transport maps into the UI\n// message shape. The client and the worker runtime agree on the wire by both\n// importing that one contract; neither imports the other.\n//\n// Frame shape on the socket is `{ v, type, payload }` (NOT realtime-core's\n// `{ v, kind, payload }`): the worker runtime `send`/`broadcast` emit\n// `JSON.stringify({ v: 1, type, payload })` and read `env.type` + `env.payload`.\n// We keep that field name here to match; realtime-core's envelope schema is the\n// generic transport primitive, the chat protocol chose `type` for its frames.\n\n// Re-export the canonical wire contract so this package's public surface is\n// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names\n// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).\nexport {\n CHAT_SUBPROTOCOL,\n CHAT_WIRE_VERSION,\n CHAT_CLOSE_CODES,\n CLIENT_KINDS as CLIENT_FRAME,\n SERVER_KINDS as SERVER_FRAME,\n // Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).\n ChatSendRejectedPayloadSchema,\n CHAT_SEND_REJECTION_CODES,\n CHAT_SEND_REJECTION_RETRYABLE,\n} from '@ttt-productions/chat-schemas';\nexport type {\n ChatCloseCode,\n ChannelRefTuple,\n ChatSendRejectedPayload,\n ChatSendRejectionCode,\n} from '@ttt-productions/chat-schemas';\n\nimport { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';\n\n// ---- raw wire row shapes (what the DO serializes; opaque-but-typed here) ----\n\n/**\n * The moderation revision `kind` (the worker runtime's revision input + the\n * `revisions.kind` column). The EFFECTIVE state of a message is its\n * MAX-`messageRevision` row's kind, so a `restore` (a later revision) supersedes\n * an earlier `moderate`/`delete`. `moderate`/`delete` blank the original text;\n * `edit` overlays new text; `restore` reverts to the original.\n */\nexport type RevisionKind = 'delete' | 'moderate' | 'edit' | 'restore';\n\n/**\n * A message row as the Channel DO stores + broadcasts it (the worker runtime's\n * message row). `replyTo`/`attachmentMeta` arrive as JSON STRINGS (or null) —\n * the DO stores them stringified and does not re-parse before broadcast. The\n * adapter mapping in `map.ts` parses them into the UI `ChatMessageV1` shape.\n */\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n replyTo: string | null;\n attachmentState: string | null;\n attachmentMeta: string | null;\n createdAt: number;\n epoch: number;\n /**\n * The effective (max-`messageRevision`) moderation kind for this row, or null\n * when never moderated. Populated by the DO's getHistory/getDeltaSince overlay\n * merge so a hidden/deleted/edited row arrives already-moderated and its\n * ORIGINAL text never reaches the renderer. Optional/absent on pre-overlay rows.\n */\n moderationKind?: RevisionKind | null;\n /**\n * The effective `messageRevision` that `moderationKind` reflects (the DO's\n * max-revision). Used to break ties against a live `revision` frame: a higher\n * `messageRevision` wins; a stale/older one is ignored. Optional/absent on\n * pre-overlay rows (treated as revision 0).\n */\n messageRevision?: number;\n}\n\n/**\n * The channel resume snapshot the DO returns for a `resume` frame (Contract C).\n * `resync` / `delta` are the durable gap-repair half of the contract:\n * - `resync: false` + `delta` = the contiguous ≤500-message backlog after the\n * client's cursor (oldest-first); the client APPLIES the delta and advances its\n * cursor to `lastMessageSeq` — a missed broadcast is never permanent.\n * - `resync: true` = the gap exceeds the backlog (or the client sent no cursor);\n * the client DROPS its local tail and re-pages history from the top.\n * Both are OPTIONAL/absent on a pre-contract DO (treated as `resync: true`, empty\n * delta — the safe re-page path) so a stale worker can't leave a client stuck.\n */\nexport interface WireChannelSnapshot {\n lastMessageSeq: number;\n readSeq: number;\n resync?: boolean;\n delta?: WireMessageRow[];\n}\n\n/** A registry entry in the inbox snapshot (the worker runtime's registry entry). */\nexport interface WireRegistryEntry {\n channelRef: string;\n kind: string;\n state: 'active' | 'tombstoned';\n registryVersion: number;\n /**\n * The typed channel-ref tuple — the OPENABLE identity for the inbox view (the\n * `channelRef` is an opaque DO-id dedup key). Null/absent on legacy or pre-tuple\n * rows; the consumer falls back to hiding the open action for those entries.\n */\n ref?: ChannelRefTuple | null;\n /**\n * Per-entry unread flag (drives the per-row dot). Optional/absent until the inbox\n * DO populates it; `deriveUnreadRefs` reads it as a typed field (no cast).\n */\n unread?: boolean;\n /**\n * Archived state — DISTINCT from `tombstoned`. An archived row is still an ACTIVE\n * membership (`state: 'active'`, history readable) but is grouped under the inbox\n * view's Archived toggle instead of the active list. The client must NOT count\n * archived rows in the unread roll-up (archive = done; the DO clears unread on\n * archive). Optional/absent on a pre-archive DO (treated as not-archived).\n */\n archived?: boolean;\n}\n\n/** The inbox snapshot payload pushed on connect / `resume` / live delta. */\nexport interface WireInboxSnapshot {\n registry: WireRegistryEntry[];\n hasUnread: boolean;\n}\n\n// ---- server → client frame payloads (discriminated on `type`) ----\n\nexport type ServerFrame =\n | { type: 'message'; payload: { message: WireMessageRow } }\n | { type: 'ack'; payload: { clientMessageId?: string; seq?: number; readSeq?: number } }\n | { type: 'history-page'; payload: { messages: WireMessageRow[] } }\n | { type: 'presence'; payload: { online: string[] } }\n | { type: 'typing'; payload: { uid: string; at: number } }\n | { type: 'snapshot'; payload: WireChannelSnapshot | WireInboxSnapshot }\n // A standalone unread push (inbox scope): either a full inbox snapshot or a\n // lightweight dock-dot patch. Represented in the union so the inbox client routes\n // it instead of silently dropping it (forward-compatible with a future DO push).\n | { type: 'unread'; payload: WireInboxSnapshot | { hasUnread: boolean } }\n | { type: 'error'; payload: { code: string; retryAfterMs?: number } }\n // A CORRELATED send rejection (SERVER_FRAME.SEND_REJECTED): names the SAME\n // clientMessageId as the rejected send + the canonical retry classification, so\n // exactly one optimistic bubble is failed/retried. See ChatSendRejectedPayload.\n | { type: 'send-rejected'; payload: ChatSendRejectedPayload }\n // A moderation revision was applied to one message. The Channel DO broadcasts\n // this on a NEWLY-applied revision (the worker runtime's moderation apply) so live\n // viewers hide/blank/edit/restore the message immediately with no extra fetch.\n // `messageSeq` is the target message id; `kind` is the applied revision kind;\n // `messageRevision` is its per-message revision number (max-revision wins).\n | { type: 'revision'; payload: { messageSeq: number; kind: RevisionKind; messageRevision: number } };\n\n/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */\nexport function buildFrame(type: string, payload: Record<string, unknown>): string {\n return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });\n}\n\n/** Parse an inbound frame, tolerating malformed JSON. Returns null on garbage. */\nexport function parseFrame(raw: string): { v?: number; type?: string; payload?: Record<string, unknown> } | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n return parsed as { v?: number; type?: string; payload?: Record<string, unknown> };\n } catch {\n return null;\n }\n}\n\n/** A snapshot with `registry` is an inbox snapshot; one with `lastMessageSeq` is a channel snapshot. */\nexport function isInboxSnapshot(p: WireChannelSnapshot | WireInboxSnapshot): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"MessageItemDefault.d.ts","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkB,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAiKpG,MAAM,MAAM,uBAAuB,GAAG;IACpC,CAAC,EAAE,aAAa,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAG9B,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;0DAEsD;IACtD,WAAW,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAEjD,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BA2GhE"}
|
|
@@ -20,6 +20,34 @@ function getAttachmentLabel(type) {
|
|
|
20
20
|
default: return "File attachment";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
// ============================================
|
|
24
|
+
// Correlated send-rejection copy + retry policy
|
|
25
|
+
// ============================================
|
|
26
|
+
// User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode
|
|
27
|
+
// by the realtime channel client). A failed send with no code (a transport/exhausted
|
|
28
|
+
// reconnect failure) falls back to the neutral "Couldn't send".
|
|
29
|
+
const SEND_FAILURE_COPY = {
|
|
30
|
+
"blocked-word": "Message contains blocked language",
|
|
31
|
+
"archived": "This channel is read-only",
|
|
32
|
+
"deleted": "This channel is read-only",
|
|
33
|
+
"membership-pending": "Chat is still preparing",
|
|
34
|
+
"wordlist-unavailable": "Chat safety check is temporarily unavailable",
|
|
35
|
+
"flood": "Please wait before sending again",
|
|
36
|
+
"slow-mode": "Please wait before sending again",
|
|
37
|
+
};
|
|
38
|
+
function sendFailureCopy(code) {
|
|
39
|
+
return (code && SEND_FAILURE_COPY[code]) || "Couldn't send";
|
|
40
|
+
}
|
|
41
|
+
// Retry policy comes from meta.sendRetryable, stamped by the channel client from
|
|
42
|
+
// the wire contract's canonical retryability table (chat-schemas enforces the
|
|
43
|
+
// frame's `retryable` matches the table, so this is the ONE source of truth —
|
|
44
|
+
// never a second hardcoded code list here, which would have to move in lockstep
|
|
45
|
+
// with the contract). `false` = terminal (an unchanged resend is guaranteed to
|
|
46
|
+
// fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted
|
|
47
|
+
// reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.
|
|
48
|
+
function isRetryableFailure(meta) {
|
|
49
|
+
return meta?.sendRetryable !== false;
|
|
50
|
+
}
|
|
23
51
|
function AttachmentView({ att }) {
|
|
24
52
|
// Display URL is built at render time by the app-injected resolver from
|
|
25
53
|
// att.mediaAssetId — attachments never store URLs (protected-gateway model).
|
|
@@ -91,9 +119,13 @@ export function MessageItemDefault(props) {
|
|
|
91
119
|
const sendFailed = m.meta?.sendFailed === true;
|
|
92
120
|
const sendPending = m.meta?.optimistic === true && !sendFailed;
|
|
93
121
|
const clientMessageId = typeof m.meta?.clientMessageId === "string" ? m.meta.clientMessageId : null;
|
|
122
|
+
// Correlated rejection reason (from the `send-rejected` frame), if any — drives
|
|
123
|
+
// both the failure copy and whether a Retry affordance is offered.
|
|
124
|
+
const sendFailureCode = typeof m.meta?.sendFailureCode === "string" ? m.meta.sendFailureCode : null;
|
|
125
|
+
const showRetry = sendFailed && isRetryableFailure(m.meta);
|
|
94
126
|
return (_jsxs("div", { className: cn("flex flex-col w-fit max-w-[85%]", mine ? "ml-auto items-end" : "mr-auto items-start", isContinuation ? "chat-continuation-gap" : "chat-group-gap"), children: [_jsxs("div", { className: cn("chat-bubble", mine ? "chat-bubble--mine" : "chat-bubble--theirs", sendPending && "chat-bubble--pending", sendFailed && "chat-bubble--failed"), children: [!isContinuation && (_jsxs("div", { className: "flex items-center gap-2 text-xs opacity-80 mb-1", children: [_jsx("span", { className: cn("font-medium", onSenderClick && "cursor-pointer hover:underline"), onClick: onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined, role: onSenderClick ? "button" : undefined, tabIndex: onSenderClick ? 0 : undefined, onKeyDown: onSenderClick ? (e) => {
|
|
95
127
|
if (e.key === "Enter" || e.key === " ")
|
|
96
128
|
onSenderClick(m.senderId, senderName);
|
|
97
|
-
} : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children:
|
|
129
|
+
} : undefined, children: senderName }), _jsx("span", { children: "\u00B7" }), _jsx("span", { children: new Date(m.createdAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) })] })), m.replyTo && _jsx(ReplyQuote, { replyTo: m.replyTo }), m.text && (_jsx("p", { className: "text-sm whitespace-pre-wrap", children: _jsx(MessageText, { text: m.text }) })), m.attachment && (_jsx("div", { className: "mt-2", children: _jsx(AttachmentView, { att: m.attachment }) }))] }), sendPending && (_jsxs("div", { className: "chat-send-status mt-0.5", role: "status", "aria-live": "polite", children: [_jsx(Loader2, { className: "h-3 w-3 shrink-0 animate-spin" }), _jsx("span", { children: "Sending\u2026" })] })), sendFailed && (_jsxs("div", { className: "chat-send-status chat-send-status--failed mt-0.5", role: "alert", children: [_jsx(AlertTriangle, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: sendFailureCopy(sendFailureCode) }), showRetry && onRetrySend && clientMessageId && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-5 px-1.5 text-[0.6875rem]", onClick: () => onRetrySend(clientMessageId), children: "Retry" }))] })), !isContinuation && (_jsx("div", { className: "mt-1", children: _jsx(MessageActions, { messageId: m.messageId, isAdmin: isAdmin, handlers: handlers }) }))] }));
|
|
98
130
|
}
|
|
99
131
|
//# sourceMappingURL=MessageItemDefault.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\nfunction AttachmentView({ att }: { att: ChatAttachment }) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, processing/moderation pending. No\n // `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`).\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Sending…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\">\n <AlertTriangle className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">\n {att.failureReason ?? \"Couldn't send this attachment\"}\n </span>\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>Couldn't send</span>\n {onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"MessageItemDefault.js","sourceRoot":"","sources":["../../src/ui/MessageItemDefault.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport type { ChatMessageV1, ChatAttachment, ModerationHandlers } from \"@ttt-productions/chat-core\";\nimport { MessageText } from \"../mentions/MessageText.js\";\nimport { cn } from \"@ttt-productions/ui-core\";\nimport { Button } from \"@ttt-productions/ui-core/react\";\nimport { MediaViewer } from \"@ttt-productions/media-viewer/react\";\nimport { FileText, Loader2, AlertTriangle } from \"lucide-react\";\nimport { MessageActions } from \"./menus.js\";\nimport { useResolvedSenderName } from \"../context/ChatNameResolverContext.js\";\nimport { useChatAttachmentUrlResolver } from \"../context/ChatAttachmentUrlContext.js\";\nimport { useChatAttachmentMediaComponent } from \"../context/ChatAttachmentMediaContext.js\";\n\n// ============================================\n// Attachment rendering\n// ============================================\n\nfunction getAttachmentLabel(type: ChatAttachment[\"type\"]): string {\n switch (type) {\n case \"image\": return \"Image attachment\";\n case \"video\": return \"Video attachment\";\n case \"audio\": return \"Audio attachment\";\n default: return \"File attachment\";\n }\n}\n\n// ============================================\n// Correlated send-rejection copy + retry policy\n// ============================================\n\n// User-facing copy per correlated `send-rejected` code (stamped on meta.sendFailureCode\n// by the realtime channel client). A failed send with no code (a transport/exhausted\n// reconnect failure) falls back to the neutral \"Couldn't send\".\nconst SEND_FAILURE_COPY: Record<string, string> = {\n \"blocked-word\": \"Message contains blocked language\",\n \"archived\": \"This channel is read-only\",\n \"deleted\": \"This channel is read-only\",\n \"membership-pending\": \"Chat is still preparing\",\n \"wordlist-unavailable\": \"Chat safety check is temporarily unavailable\",\n \"flood\": \"Please wait before sending again\",\n \"slow-mode\": \"Please wait before sending again\",\n};\n\nfunction sendFailureCopy(code: string | null): string {\n return (code && SEND_FAILURE_COPY[code]) || \"Couldn't send\";\n}\n\n// Retry policy comes from meta.sendRetryable, stamped by the channel client from\n// the wire contract's canonical retryability table (chat-schemas enforces the\n// frame's `retryable` matches the table, so this is the ONE source of truth —\n// never a second hardcoded code list here, which would have to move in lockstep\n// with the contract). `false` = terminal (an unchanged resend is guaranteed to\n// fail — no Retry action); `true` or ABSENT (a code-less transport/exhausted\n// reconnect failure) keeps Retry, reusing the ORIGINAL clientMessageId.\nfunction isRetryableFailure(meta: Record<string, unknown> | undefined): boolean {\n return meta?.sendRetryable !== false;\n}\n\nfunction AttachmentView({ att }: { att: ChatAttachment }) {\n // Display URL is built at render time by the app-injected resolver from\n // att.mediaAssetId — attachments never store URLs (protected-gateway model).\n const resolveAttachmentUrl = useChatAttachmentUrlResolver();\n // App-injected media component (optional) — lets chat attachments ride the\n // app's ONE display path (recovery adapter, diagnostics, telemetry). Falls\n // back to the package's own MediaViewer when no provider is present.\n const Media = useChatAttachmentMediaComponent() ?? MediaViewer;\n // In-flight placeholder — bytes uploaded, processing/moderation pending. No\n // `mediaAssetId` yet. Rendered only to the sender (other participants can't\n // read a pending doc — Firestore rules scope it until `status === 'ready'`).\n if (att.status === \"pending\") {\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Sending…</span>\n </div>\n );\n }\n // Processing/moderation rejected the attachment — sender-only.\n if (att.status === \"failed\") {\n return (\n <div className=\"chat-attachment-rejected\">\n <AlertTriangle className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">\n {att.failureReason ?? \"Couldn't send this attachment\"}\n </span>\n </div>\n );\n }\n // ready (or legacy/absent status) — resolve the display URL from the asset ref.\n const url = resolveAttachmentUrl(att);\n if (!url) {\n // Terminal-ready row, but the authorized URL is still SETTLING (the grant/URL\n // resolver has nothing yet). Returning null left only the surrounding message\n // metadata → an observed empty bubble; a broken/grant-less <img> would flash a\n // 403. Render a neutral loading placeholder in the existing loading visual\n // language (spinner + kind-appropriate generic label + \"Loading…\", never\n // \"Sending…\", NO filename) and do NOT mount the media/download renderer until a\n // non-empty authorized URL exists. Sender-only pending/processing/failed states\n // above are unchanged — this begins only after a terminal-ready row is received.\n return (\n <div className=\"chat-attachment-pending\">\n <Loader2 className=\"h-4 w-4 shrink-0 animate-spin\" />\n <span className=\"truncate\">{getAttachmentLabel(att.type)}</span>\n <span className=\"chat-attachment-status-label\">Loading…</span>\n </div>\n );\n }\n if (att.type === \"image\") {\n return <Media type=\"image\" url={url} alt=\"Image attachment\" className=\"chat-attachment-media\" />;\n }\n if (att.type === \"video\") {\n return <Media type=\"video\" url={url} controls className=\"chat-attachment-media\" />;\n }\n if (att.type === \"audio\") {\n return <Media type=\"audio\" url={url} controls className=\"chat-attachment-media\" />;\n }\n // text/markdown — download link\n return (\n <a\n href={url}\n download={att.name}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"chat-attachment-text-link\"\n >\n <FileText className=\"h-4 w-4 shrink-0\" />\n <span className=\"truncate\">Download attachment</span>\n </a>\n );\n}\n\n// ============================================\n// Reply-to quote\n// ============================================\n\nfunction ReplyQuote({ replyTo }: { replyTo: NonNullable<ChatMessageV1[\"replyTo\"]> }) {\n const replyName = useResolvedSenderName(replyTo.senderId);\n return (\n <div className=\"chat-reply-quote\">\n <span className=\"chat-reply-quote-sender\">{replyName}</span>\n <span className=\"chat-reply-quote-preview\"><MessageText text={replyTo.messagePreview} /></span>\n </div>\n );\n}\n\n// ============================================\n// System message\n// ============================================\n\nfunction SystemMessage({ m }: { m: ChatMessageV1 }) {\n return (\n <div className=\"chat-system-message\">\n {m.text && <span>{m.text}</span>}\n </div>\n );\n}\n\n// ============================================\n// Default message item\n// ============================================\n\nexport type MessageItemDefaultProps = {\n m: ChatMessageV1;\n currentUserId: string;\n isAdmin: boolean;\n handlers?: ModerationHandlers;\n\n // Grouping\n isContinuation?: boolean;\n\n // Sender interaction\n onSenderClick?: (senderId: string, displayName: string) => void;\n\n /** Retry a failed realtime send by its original clientMessageId (wired by\n * ChatShell on the realtime transport; absent on firestore). Enables the\n * retry affordance on a `meta.sendFailed` bubble. */\n onRetrySend?: (clientMessageId: string) => void;\n\n};\n\nexport function MessageItemDefault(props: MessageItemDefaultProps) {\n const { m, currentUserId, isAdmin, handlers, isContinuation, onSenderClick, onRetrySend } = props;\n const senderName = useResolvedSenderName(m.senderId);\n\n // System messages render differently\n if (m.isSystemMessage) {\n return <SystemMessage m={m} />;\n }\n\n const mine = m.senderId === currentUserId;\n\n // Realtime optimistic-send lifecycle (meta is stamped by the channel client):\n // an un-acked echo renders subtly pending; one past the retry/age cap renders a\n // visibly failed bubble with a retry affordance (same clientMessageId — the DO\n // dedups, so a retry can never double-send).\n const sendFailed = m.meta?.sendFailed === true;\n const sendPending = m.meta?.optimistic === true && !sendFailed;\n const clientMessageId =\n typeof m.meta?.clientMessageId === \"string\" ? m.meta.clientMessageId : null;\n // Correlated rejection reason (from the `send-rejected` frame), if any — drives\n // both the failure copy and whether a Retry affordance is offered.\n const sendFailureCode =\n typeof m.meta?.sendFailureCode === \"string\" ? m.meta.sendFailureCode : null;\n const showRetry = sendFailed && isRetryableFailure(m.meta);\n\n return (\n <div\n className={cn(\n \"flex flex-col w-fit max-w-[85%]\",\n mine ? \"ml-auto items-end\" : \"mr-auto items-start\",\n isContinuation ? \"chat-continuation-gap\" : \"chat-group-gap\"\n )}\n >\n <div\n className={cn(\n \"chat-bubble\",\n mine ? \"chat-bubble--mine\" : \"chat-bubble--theirs\",\n sendPending && \"chat-bubble--pending\",\n sendFailed && \"chat-bubble--failed\"\n )}\n >\n {!isContinuation && (\n <div className=\"flex items-center gap-2 text-xs opacity-80 mb-1\">\n <span\n className={cn(\"font-medium\", onSenderClick && \"cursor-pointer hover:underline\")}\n onClick={onSenderClick ? () => onSenderClick(m.senderId, senderName) : undefined}\n role={onSenderClick ? \"button\" : undefined}\n tabIndex={onSenderClick ? 0 : undefined}\n onKeyDown={onSenderClick ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onSenderClick(m.senderId, senderName);\n } : undefined}\n >\n {senderName}\n </span>\n <span>·</span>\n <span>{new Date(m.createdAt).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })}</span>\n </div>\n )}\n\n {m.replyTo && <ReplyQuote replyTo={m.replyTo} />}\n\n {m.text && (\n <p className=\"text-sm whitespace-pre-wrap\">\n <MessageText text={m.text} />\n </p>\n )}\n\n {m.attachment && (\n <div className=\"mt-2\">\n <AttachmentView att={m.attachment} />\n </div>\n )}\n </div>\n\n {/* Send-state status rows render regardless of grouping — a pending/failed\n send must never be indistinguishable from a delivered one. */}\n {sendPending && (\n <div className=\"chat-send-status mt-0.5\" role=\"status\" aria-live=\"polite\">\n <Loader2 className=\"h-3 w-3 shrink-0 animate-spin\" />\n <span>Sending…</span>\n </div>\n )}\n {sendFailed && (\n <div className=\"chat-send-status chat-send-status--failed mt-0.5\" role=\"alert\">\n <AlertTriangle className=\"h-3 w-3 shrink-0\" />\n <span>{sendFailureCopy(sendFailureCode)}</span>\n {showRetry && onRetrySend && clientMessageId && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-5 px-1.5 text-[0.6875rem]\"\n onClick={() => onRetrySend(clientMessageId)}\n >\n Retry\n </Button>\n )}\n </div>\n )}\n\n {!isContinuation && (\n <div className=\"mt-1\">\n <MessageActions messageId={m.messageId} isAdmin={isAdmin} handlers={handlers} />\n </div>\n )}\n </div>\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/chat-react",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
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",
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@ttt-productions/chat-core": "^0.10.2",
|
|
64
|
-
"@ttt-productions/chat-schemas": "^0.9.
|
|
64
|
+
"@ttt-productions/chat-schemas": "^0.9.3",
|
|
65
65
|
"@ttt-productions/realtime-core": "^0.1.3",
|
|
66
66
|
"@ttt-productions/media-schemas": "^0.14.2",
|
|
67
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
67
|
+
"@ttt-productions/ui-core": "^0.11.10",
|
|
68
68
|
"@ttt-productions/file-input": "^0.11.5",
|
|
69
69
|
"@ttt-productions/upload-ui": "^0.7.5",
|
|
70
70
|
"@ttt-productions/media-viewer": "^0.10.5",
|