@ttt-productions/chat-react 0.13.15 → 0.13.16

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.
Files changed (73) hide show
  1. package/dist/context/ChatNameResolverContext.d.ts +3 -3
  2. package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
  3. package/dist/context/ChatNameResolverContext.js +3 -3
  4. package/dist/context/ChatNameResolverContext.js.map +1 -1
  5. package/dist/firestore/queries.js +1 -1
  6. package/dist/firestore/queries.js.map +1 -1
  7. package/dist/hooks/useChatMessages.d.ts +3 -3
  8. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  9. package/dist/hooks/useChatMessages.js +20 -12
  10. package/dist/hooks/useChatMessages.js.map +1 -1
  11. package/dist/index.d.ts +32 -18
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +18 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/realtime/channel-client.d.ts.map +1 -1
  16. package/dist/realtime/channel-client.js +97 -32
  17. package/dist/realtime/channel-client.js.map +1 -1
  18. package/dist/realtime/inbox-client.d.ts.map +1 -1
  19. package/dist/realtime/inbox-client.js +29 -11
  20. package/dist/realtime/inbox-client.js.map +1 -1
  21. package/dist/realtime/initial-load-budget.d.ts.map +1 -1
  22. package/dist/realtime/initial-load-budget.js.map +1 -1
  23. package/dist/realtime/map.d.ts +0 -2
  24. package/dist/realtime/map.d.ts.map +1 -1
  25. package/dist/realtime/map.js +3 -4
  26. package/dist/realtime/map.js.map +1 -1
  27. package/dist/realtime/shared.d.ts +0 -1
  28. package/dist/realtime/shared.d.ts.map +1 -1
  29. package/dist/realtime/shared.js +2 -6
  30. package/dist/realtime/shared.js.map +1 -1
  31. package/dist/realtime/socket.d.ts.map +1 -1
  32. package/dist/realtime/socket.js +1 -11
  33. package/dist/realtime/socket.js.map +1 -1
  34. package/dist/realtime/transport.d.ts +1 -2
  35. package/dist/realtime/transport.d.ts.map +1 -1
  36. package/dist/realtime/transport.js +1 -1
  37. package/dist/realtime/transport.js.map +1 -1
  38. package/dist/realtime/useRealtimeChatMessages.d.ts +4 -4
  39. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  40. package/dist/realtime/useRealtimeChatMessages.js +5 -5
  41. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  42. package/dist/realtime/wire.d.ts +0 -2
  43. package/dist/realtime/wire.d.ts.map +1 -1
  44. package/dist/realtime/wire.js +0 -6
  45. package/dist/realtime/wire.js.map +1 -1
  46. package/dist/types.d.ts +3 -3
  47. package/dist/types.js.map +1 -1
  48. package/dist/ui/ChatShell.d.ts +5 -5
  49. package/dist/ui/ChatShell.d.ts.map +1 -1
  50. package/dist/ui/ChatShell.js +37 -34
  51. package/dist/ui/ChatShell.js.map +1 -1
  52. package/dist/ui/Composer.d.ts +1 -1
  53. package/dist/ui/Composer.js +12 -12
  54. package/dist/ui/Composer.js.map +1 -1
  55. package/dist/ui/MessageItemDefault.d.ts +2 -2
  56. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  57. package/dist/ui/MessageItemDefault.js +26 -24
  58. package/dist/ui/MessageItemDefault.js.map +1 -1
  59. package/dist/ui/MessageList.d.ts +3 -3
  60. package/dist/ui/MessageList.d.ts.map +1 -1
  61. package/dist/ui/MessageList.js +13 -16
  62. package/dist/ui/MessageList.js.map +1 -1
  63. package/dist/ui/SenderName.js +3 -3
  64. package/dist/ui/SenderName.js.map +1 -1
  65. package/dist/ui/menus.d.ts +1 -1
  66. package/dist/ui/menus.d.ts.map +1 -1
  67. package/dist/ui/menus.js +4 -4
  68. package/dist/ui/menus.js.map +1 -1
  69. package/package.json +10 -10
  70. package/dist/realtime/index.d.ts +0 -19
  71. package/dist/realtime/index.d.ts.map +0 -1
  72. package/dist/realtime/index.js +0 -15
  73. package/dist/realtime/index.js.map +0 -1
@@ -14,11 +14,12 @@
14
14
  // - error codes incl. 4401 AUTH_EXPIRED -> re-mint a grant ONCE via grantProvider
15
15
  //
16
16
  // Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.
17
- import { createReconnectController } from '@ttt-productions/realtime-core';
18
- import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, ChatSendRejectedPayloadSchema, } from './wire.js';
17
+ import { createReconnectController, } from '@ttt-productions/realtime-core';
18
+ import { CLIENT_KINDS, CHAT_CLOSE_CODES, ChatSendRejectedPayloadSchema, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, } from '@ttt-productions/chat-schemas';
19
+ import { buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
19
20
  import { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, } from './map.js';
20
21
  import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
21
- import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
22
+ import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
22
23
  import { InitialLoadBudget } from './initial-load-budget.js';
23
24
  const INITIAL = {
24
25
  status: 'idle',
@@ -335,15 +336,15 @@ export class ChannelClient {
335
336
  // Resume: ask the DO for the authoritative snapshot, then live deltas flow. The
336
337
  // field name MUST be `afterSeq` — the Channel DO reads `payload.afterSeq`; any other
337
338
  // name reads as cursorless and forces a full resync every reconnect.
338
- this.sendFrame(CLIENT_FRAME.RESUME, { afterSeq: this.resumeSeq });
339
+ this.sendFrame(CLIENT_KINDS.RESUME, { afterSeq: this.resumeSeq });
339
340
  this.diag?.(DIAG.RESUME_REQUEST, { afterSeq: this.resumeSeq, attempt: this.connectAttempt });
340
341
  // If the UI had presence open, re-subscribe after a reconnect.
341
342
  if (this.presenceSubscribed)
342
- this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});
343
+ this.sendFrame(CLIENT_KINDS.PRESENCE_SUBSCRIBE, {});
343
344
  // Re-send the last read cursor so a read-ack dropped during the disconnect window
344
345
  // (sendFrame returns false on a closed socket) is not lost until the next message.
345
346
  if (this.lastReadAck)
346
- this.sendFrame(CLIENT_FRAME.READ_ACK, { ...this.lastReadAck });
347
+ this.sendFrame(CLIENT_KINDS.READ_ACK, { ...this.lastReadAck });
347
348
  // Re-send every un-acked pending send with its ORIGINAL clientMessageId so a send
348
349
  // that lost its socket before the ack is not silently dropped. The DO dedups
349
350
  // (re-acks, no re-broadcast, no flood-token) — this is the client half of the
@@ -390,7 +391,7 @@ export class ChannelClient {
390
391
  continue;
391
392
  }
392
393
  }
393
- const sent = this.sendFrame(CLIENT_FRAME.SEND, {
394
+ const sent = this.sendFrame(CLIENT_KINDS.SEND, {
394
395
  clientMessageId: pending.clientMessageId,
395
396
  text: pending.text,
396
397
  });
@@ -501,7 +502,10 @@ export class ChannelClient {
501
502
  if (typeof rawId === 'string' && rawId.length > 0 && rawId.length <= 200) {
502
503
  const orphaned = this.pendingSends.get(rawId);
503
504
  if (orphaned && !orphaned.failed) {
504
- this.diag?.(DIAG.SEND_REJECTED, { clientMessageId: rawId, outcome: 'unparseable-fail-closed' });
505
+ this.diag?.(DIAG.SEND_REJECTED, {
506
+ clientMessageId: rawId,
507
+ outcome: 'unparseable-fail-closed',
508
+ });
505
509
  this.failPendingSend(rawId);
506
510
  return;
507
511
  }
@@ -514,13 +518,23 @@ export class ChannelClient {
514
518
  // No matching pending send — already acked/reconciled, or never ours. A late
515
519
  // ack/echo (which may have already landed) always wins; nothing to correlate.
516
520
  if (!pending || pending.failed) {
517
- this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable, outcome: 'no-pending-send' });
521
+ this.diag?.(DIAG.SEND_REJECTED, {
522
+ clientMessageId,
523
+ code,
524
+ retryable,
525
+ outcome: 'no-pending-send',
526
+ });
518
527
  return;
519
528
  }
520
529
  if (!retryable) {
521
530
  // Terminal: an unchanged resend cannot succeed (blocked-word / archived /
522
531
  // deleted). Fail ONLY this bubble, recording the reason + terminal class.
523
- this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: false, outcome: 'terminal' });
532
+ this.diag?.(DIAG.SEND_REJECTED, {
533
+ clientMessageId,
534
+ code,
535
+ retryable: false,
536
+ outcome: 'terminal',
537
+ });
524
538
  this.failPendingSend(clientMessageId, code, false);
525
539
  return;
526
540
  }
@@ -532,12 +546,23 @@ export class ChannelClient {
532
546
  // If the budget already elapsed (a long stream of retryable rejections), fail
533
547
  // now rather than scheduling yet another doomed resend.
534
548
  if (this.timers.now() - pending.sentAt >= SERVER_RETRYABLE_MAX_AGE_MS) {
535
- this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'budget-exhausted' });
549
+ this.diag?.(DIAG.SEND_REJECTED, {
550
+ clientMessageId,
551
+ code,
552
+ retryable: true,
553
+ outcome: 'budget-exhausted',
554
+ });
536
555
  this.failPendingSend(clientMessageId, code, true);
537
556
  return;
538
557
  }
539
558
  const delayMs = this.clampRetryDelay(retryAfterMs);
540
- this.diag?.(DIAG.SEND_REJECTED, { clientMessageId, code, retryable: true, outcome: 'retry-scheduled', delayMs });
559
+ this.diag?.(DIAG.SEND_REJECTED, {
560
+ clientMessageId,
561
+ code,
562
+ retryable: true,
563
+ outcome: 'retry-scheduled',
564
+ delayMs,
565
+ });
541
566
  this.scheduleSendRetry(clientMessageId, delayMs);
542
567
  }
543
568
  /**
@@ -564,7 +589,7 @@ export class ChannelClient {
564
589
  }
565
590
  // A server-retryable resend does NOT count toward the attempt cap (that cap is
566
591
  // for blind reconnect resends only) — leave `attempts` untouched.
567
- const sent = this.sendFrame(CLIENT_FRAME.SEND, {
592
+ const sent = this.sendFrame(CLIENT_KINDS.SEND, {
568
593
  clientMessageId: current.clientMessageId,
569
594
  text: current.text,
570
595
  });
@@ -581,7 +606,9 @@ export class ChannelClient {
581
606
  // contract violation), NOTHING else would ever re-check the budget on a
582
607
  // stable socket — so arm a deadline check that fails the send at budget end.
583
608
  // This makes SERVER_RETRYABLE_MAX_AGE_MS a hard ceiling, not a best effort.
584
- const remainingMs = SERVER_RETRYABLE_MAX_AGE_MS - (this.timers.now() - current.sentAt) + SEND_RETRY_MIN_DELAY_MS;
609
+ const remainingMs = SERVER_RETRYABLE_MAX_AGE_MS -
610
+ (this.timers.now() - current.sentAt) +
611
+ SEND_RETRY_MIN_DELAY_MS;
585
612
  current.retryTimer = this.timers.setTimeout(() => {
586
613
  current.retryTimer = null;
587
614
  if (this.closedByUs)
@@ -632,7 +659,7 @@ export class ChannelClient {
632
659
  pending.serverRetryable = false;
633
660
  pending.lastRejectionCode = undefined;
634
661
  pending.sentAt = this.timers.now();
635
- const sent = this.sendFrame(CLIENT_FRAME.SEND, {
662
+ const sent = this.sendFrame(CLIENT_KINDS.SEND, {
636
663
  clientMessageId: pending.clientMessageId,
637
664
  text: pending.text,
638
665
  });
@@ -646,7 +673,9 @@ export class ChannelClient {
646
673
  sent,
647
674
  });
648
675
  const next = this.state.messages.map((m) => {
649
- if (!(m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed)) {
676
+ if (!(m.meta?.optimistic &&
677
+ m.meta?.clientMessageId === clientMessageId &&
678
+ m.meta?.sendFailed)) {
650
679
  return m;
651
680
  }
652
681
  // Back to the normal pending look — drop every failure marker.
@@ -668,7 +697,12 @@ export class ChannelClient {
668
697
  if (this.state.isFetchingOlder)
669
698
  this.setState({ isFetchingOlder: false });
670
699
  if (this.closedByUs) {
671
- this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed', resumeSeq: this.resumeSeq });
700
+ this.diag?.(DIAG.SOCKET_CLOSE, {
701
+ code,
702
+ closedByUs: true,
703
+ outcome: 'closed',
704
+ resumeSeq: this.resumeSeq,
705
+ });
672
706
  this.setState({ status: 'closed' });
673
707
  return;
674
708
  }
@@ -676,7 +710,12 @@ export class ChannelClient {
676
710
  if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {
677
711
  this.reauthAttempted = true;
678
712
  this.reconnectCause = 'auth-expired';
679
- this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant', resumeSeq: this.resumeSeq });
713
+ this.diag?.(DIAG.SOCKET_CLOSE, {
714
+ code,
715
+ closedByUs: false,
716
+ outcome: 're-grant',
717
+ resumeSeq: this.resumeSeq,
718
+ });
680
719
  this.setState({ status: 'reconnecting' });
681
720
  void this.openSocket();
682
721
  return;
@@ -688,7 +727,12 @@ export class ChannelClient {
688
727
  if (code === CHAT_CLOSE_CODES.REVOKED) {
689
728
  this.closedByUs = true;
690
729
  this.controller.close();
691
- this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked', resumeSeq: this.resumeSeq });
730
+ this.diag?.(DIAG.SOCKET_CLOSE, {
731
+ code,
732
+ closedByUs: false,
733
+ outcome: 'revoked',
734
+ resumeSeq: this.resumeSeq,
735
+ });
692
736
  this.failAllPendingSends();
693
737
  this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
694
738
  // Revoked before anything loaded: the first open has failed, and nothing retries it.
@@ -698,7 +742,12 @@ export class ChannelClient {
698
742
  this.reconnectCause = 'transient-close';
699
743
  if (!this.state.hasLoadedInitialData)
700
744
  this.initialLoad.noteFailedAttempt();
701
- this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect', resumeSeq: this.resumeSeq });
745
+ this.diag?.(DIAG.SOCKET_CLOSE, {
746
+ code,
747
+ closedByUs: false,
748
+ outcome: 'reconnect',
749
+ resumeSeq: this.resumeSeq,
750
+ });
702
751
  this.scheduleReconnect();
703
752
  }
704
753
  scheduleReconnect() {
@@ -815,7 +864,10 @@ export class ChannelClient {
815
864
  return;
816
865
  default:
817
866
  // unknown/optional types tolerated (forward compat)
818
- this.diag?.(DIAG.FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });
867
+ this.diag?.(DIAG.FRAME_DROPPED, {
868
+ kind: safeDiagnosticLabel(frame.type),
869
+ reason: 'unknown-type',
870
+ });
819
871
  return;
820
872
  }
821
873
  }
@@ -937,7 +989,10 @@ export class ChannelClient {
937
989
  // A non-resync snapshot IS the first authoritative data (even an empty delta =
938
990
  // caught up) — end initial loading. A resync snapshot instead defers to the
939
991
  // re-paged first history page below, so it must NOT end it here.
940
- this.setState({ ...(this.state.hasLoadedInitialData ? {} : this.initialDataLoaded()), isSynced: true });
992
+ this.setState({
993
+ ...(this.state.hasLoadedInitialData ? {} : this.initialDataLoaded()),
994
+ isSynced: true,
995
+ });
941
996
  return;
942
997
  }
943
998
  // Resync: drop the local tail and re-page from the top per the pinned contract.
@@ -999,7 +1054,11 @@ export class ChannelClient {
999
1054
  seq: row.seq,
1000
1055
  hasClientMessageId: Boolean(row.clientMessageId),
1001
1056
  moderationKind: row.moderationKind ?? null,
1002
- op: prior.reconciledOptimistic ? 'optimistic-reconcile' : prior.hadRow ? 'replace-by-seq' : 'insert',
1057
+ op: prior.reconciledOptimistic
1058
+ ? 'optimistic-reconcile'
1059
+ : prior.hadRow
1060
+ ? 'replace-by-seq'
1061
+ : 'insert',
1003
1062
  cursorBefore,
1004
1063
  cursorAfter: this.resumeSeq,
1005
1064
  });
@@ -1062,7 +1121,11 @@ export class ChannelClient {
1062
1121
  // correlated rejection was superseded) means the send actually landed, so the
1063
1122
  // row must render as delivered, not failed.
1064
1123
  const { sendFailed: _sendFailed, sendFailureCode: _sendFailureCode, sendRetryable: _sendRetryable, ...restMeta } = m.meta;
1065
- return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };
1124
+ return {
1125
+ ...m,
1126
+ messageId: seqToMessageId(seq),
1127
+ meta: { ...restMeta, optimistic: false, seq },
1128
+ };
1066
1129
  });
1067
1130
  next.sort(sortBySeq);
1068
1131
  if (seq > this.resumeSeq)
@@ -1089,7 +1152,9 @@ export class ChannelClient {
1089
1152
  }
1090
1153
  const bySeq = new Map();
1091
1154
  for (const m of this.state.messages) {
1092
- if (m.meta?.optimistic && typeof m.meta?.clientMessageId === 'string' && incomingCmids.has(m.meta.clientMessageId)) {
1155
+ if (m.meta?.optimistic &&
1156
+ typeof m.meta?.clientMessageId === 'string' &&
1157
+ incomingCmids.has(m.meta.clientMessageId)) {
1093
1158
  continue; // reconciled by the incoming server row
1094
1159
  }
1095
1160
  bySeq.set(m.messageId, m);
@@ -1153,7 +1218,7 @@ export class ChannelClient {
1153
1218
  // actually sent over an open socket. A closed socket returns false — no phantom
1154
1219
  // "sent" bubble, and the caller keeps the composer text — so a disconnect between
1155
1220
  // render and click can't silently swallow the message.
1156
- const sent = this.sendFrame(CLIENT_FRAME.SEND, {
1221
+ const sent = this.sendFrame(CLIENT_KINDS.SEND, {
1157
1222
  clientMessageId: args.clientMessageId,
1158
1223
  text: args.text,
1159
1224
  });
@@ -1203,7 +1268,7 @@ export class ChannelClient {
1203
1268
  */
1204
1269
  readAck(readSeq, focused) {
1205
1270
  this.lastReadAck = { readSeq, focused };
1206
- return this.sendFrame(CLIENT_FRAME.READ_ACK, { readSeq, focused });
1271
+ return this.sendFrame(CLIENT_KINDS.READ_ACK, { readSeq, focused });
1207
1272
  }
1208
1273
  /** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */
1209
1274
  typing() {
@@ -1211,15 +1276,15 @@ export class ChannelClient {
1211
1276
  if (now - this.lastTypingSentAt < TYPING_COALESCE_MS)
1212
1277
  return;
1213
1278
  this.lastTypingSentAt = now;
1214
- this.sendFrame(CLIENT_FRAME.TYPING, {});
1279
+ this.sendFrame(CLIENT_KINDS.TYPING, {});
1215
1280
  }
1216
1281
  presenceSubscribe() {
1217
1282
  this.presenceSubscribed = true;
1218
- this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});
1283
+ this.sendFrame(CLIENT_KINDS.PRESENCE_SUBSCRIBE, {});
1219
1284
  }
1220
1285
  presenceUnsubscribe() {
1221
1286
  this.presenceSubscribed = false;
1222
- this.sendFrame(CLIENT_FRAME.PRESENCE_UNSUBSCRIBE, {});
1287
+ this.sendFrame(CLIENT_KINDS.PRESENCE_UNSUBSCRIBE, {});
1223
1288
  this.setState({ presence: [] });
1224
1289
  }
1225
1290
  /** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */
@@ -1234,7 +1299,7 @@ export class ChannelClient {
1234
1299
  this.setState({ isFetchingOlder: false });
1235
1300
  }
1236
1301
  requestHistory(beforeSeq, kind) {
1237
- const sent = this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });
1302
+ const sent = this.sendFrame(CLIENT_KINDS.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });
1238
1303
  if (sent)
1239
1304
  this.historyRequests.push(kind);
1240
1305
  this.diag?.(DIAG.HISTORY_REQUEST, { beforeSeq, limit: HISTORY_PAGE_MAX, sent });
@@ -1251,7 +1316,7 @@ export class ChannelClient {
1251
1316
  // stops the heartbeat and owns what happens next.
1252
1317
  if (this.isInboundSilent())
1253
1318
  return this.closeDeadSocket();
1254
- this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});
1319
+ this.sendFrame(CLIENT_KINDS.HEARTBEAT, {});
1255
1320
  this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);
1256
1321
  };
1257
1322
  this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);