@ttt-productions/chat-react 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +13 -11
  2. package/dist/context/ChatNameResolverContext.d.ts +3 -3
  3. package/dist/context/ChatNameResolverContext.js +3 -3
  4. package/dist/context/ChatNameResolverContext.js.map +1 -1
  5. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  6. package/dist/hooks/useChatMessages.js +4 -13
  7. package/dist/hooks/useChatMessages.js.map +1 -1
  8. package/dist/index.d.ts +2 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/realtime/channel-client.d.ts +21 -4
  13. package/dist/realtime/channel-client.d.ts.map +1 -1
  14. package/dist/realtime/channel-client.js +109 -12
  15. package/dist/realtime/channel-client.js.map +1 -1
  16. package/dist/realtime/diagnostics.d.ts +7 -0
  17. package/dist/realtime/diagnostics.d.ts.map +1 -1
  18. package/dist/realtime/diagnostics.js +7 -0
  19. package/dist/realtime/diagnostics.js.map +1 -1
  20. package/dist/realtime/inbox-client.d.ts.map +1 -1
  21. package/dist/realtime/inbox-client.js +29 -8
  22. package/dist/realtime/inbox-client.js.map +1 -1
  23. package/dist/realtime/map.d.ts +3 -5
  24. package/dist/realtime/map.d.ts.map +1 -1
  25. package/dist/realtime/map.js +3 -18
  26. package/dist/realtime/map.js.map +1 -1
  27. package/dist/realtime/shared.d.ts +21 -0
  28. package/dist/realtime/shared.d.ts.map +1 -1
  29. package/dist/realtime/shared.js +22 -0
  30. package/dist/realtime/shared.js.map +1 -1
  31. package/dist/realtime/useRealtimeChatMessages.d.ts +1 -4
  32. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  33. package/dist/realtime/useRealtimeChatMessages.js +1 -3
  34. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  35. package/dist/realtime/wire.d.ts +6 -5
  36. package/dist/realtime/wire.d.ts.map +1 -1
  37. package/dist/realtime/wire.js.map +1 -1
  38. package/dist/types.d.ts +1 -52
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js.map +1 -1
  41. package/dist/ui/ChatShell.d.ts +2 -2
  42. package/dist/ui/ChatShell.d.ts.map +1 -1
  43. package/dist/ui/ChatShell.js +6 -10
  44. package/dist/ui/ChatShell.js.map +1 -1
  45. package/dist/ui/Composer.d.ts +7 -6
  46. package/dist/ui/Composer.d.ts.map +1 -1
  47. package/dist/ui/Composer.js +2 -18
  48. package/dist/ui/Composer.js.map +1 -1
  49. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  50. package/dist/ui/MessageItemDefault.js +7 -12
  51. package/dist/ui/MessageItemDefault.js.map +1 -1
  52. package/dist/ui/MessageText.d.ts +19 -0
  53. package/dist/ui/MessageText.d.ts.map +1 -0
  54. package/dist/ui/MessageText.js +7 -0
  55. package/dist/ui/MessageText.js.map +1 -0
  56. package/package.json +6 -6
  57. package/src/styles/chat.css +0 -40
  58. package/dist/mentions/MentionAutocomplete.d.ts +0 -22
  59. package/dist/mentions/MentionAutocomplete.d.ts.map +0 -1
  60. package/dist/mentions/MentionAutocomplete.js +0 -21
  61. package/dist/mentions/MentionAutocomplete.js.map +0 -1
  62. package/dist/mentions/MessageText.d.ts +0 -27
  63. package/dist/mentions/MessageText.d.ts.map +0 -1
  64. package/dist/mentions/MessageText.js +0 -20
  65. package/dist/mentions/MessageText.js.map +0 -1
  66. package/dist/mentions/use-mention-autocomplete.d.ts +0 -82
  67. package/dist/mentions/use-mention-autocomplete.d.ts.map +0 -1
  68. package/dist/mentions/use-mention-autocomplete.js +0 -323
  69. package/dist/mentions/use-mention-autocomplete.js.map +0 -1
@@ -9,7 +9,7 @@
9
9
  // 4401 re-grant once, 4403 stop, auth-switch teardown).
10
10
  import { createReconnectController } from '@ttt-productions/realtime-core';
11
11
  import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
12
- import { defaultTimers, isChatAccessDeniedError } from './shared.js';
12
+ import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, } from './shared.js';
13
13
  import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
14
14
  const INITIAL = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };
15
15
  export class InboxClient {
@@ -49,6 +49,27 @@ export class InboxClient {
49
49
  fn(this.state);
50
50
  }
51
51
  async connect() {
52
+ // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle
53
+ // from a fresh (idle) or fully torn-down (closed) state. A second connect()
54
+ // while one is already connecting / open / reconnecting is a no-op, so a
55
+ // double-mounted dock cannot open a SECOND inbox socket against the per-uid
56
+ // cap. The reconnect path uses openSocket() / scheduleReconnect() directly
57
+ // (never connect()), so this never blocks a legitimate reconnect.
58
+ if (this.state.status !== 'idle' && this.state.status !== 'closed')
59
+ return;
60
+ // REVIVE the controller. `controller.close()` is PERMANENT: while it is in
61
+ // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`
62
+ // returns null — so without this reset a close() → connect() client opens a
63
+ // socket and works normally until its FIRST transient close, then parks
64
+ // terminally in `closed` with reconnect disabled and no surfaced error code.
65
+ this.controller.reset();
66
+ // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN
67
+ // its lifecycle; an explicit connect() is a fresh attempt the authority may
68
+ // legitimately re-deny, so the previous lifecycle's code must not persist.
69
+ // The registry/unread projection is deliberately KEPT — the DO's next snapshot
70
+ // is authoritative, and blanking the dock on every restart is pure UI churn.
71
+ if (isTerminalErrorCode(this.state.lastErrorCode))
72
+ this.setState({ lastErrorCode: null });
52
73
  this.closedByUs = false;
53
74
  this.reconnectCause = 'initial';
54
75
  this.controller.start();
@@ -130,7 +151,7 @@ export class InboxClient {
130
151
  this.closedByUs = true;
131
152
  this.controller.close();
132
153
  this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });
133
- this.setState({ status: 'closed', lastErrorCode: 'revoked' });
154
+ this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
134
155
  return;
135
156
  }
136
157
  this.reconnectCause = 'transient-close';
@@ -148,7 +169,7 @@ export class InboxClient {
148
169
  denyAccessTerminally() {
149
170
  this.closedByUs = true;
150
171
  this.controller.close();
151
- this.setState({ status: 'closed', lastErrorCode: 'access-denied' });
172
+ this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });
152
173
  }
153
174
  scheduleReconnect() {
154
175
  const delay = this.controller.onClose();
@@ -289,11 +310,11 @@ export class InboxClient {
289
310
  }
290
311
  }
291
312
  /**
292
- * Derive the per-channel unread set from a snapshot. The current inbox DO snapshot
293
- * exposes a single `hasUnread` boolean (dock dot) + the active registry; it does
294
- * NOT yet enumerate which entries are unread. We surface a per-entry `unread`
295
- * field when the DO provides it (forward-compatible), else fall back to empty
296
- * per-row dots (the dock dot still lights). See the ASSUMPTIONS note in the docs.
313
+ * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot
314
+ * carries a per-entry `unread` boolean on every active registry entry alongside
315
+ * the global `hasUnread` roll-up that drives the dock dot. An entry without the
316
+ * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot
317
+ * still lights off `hasUnread`.
297
318
  *
298
319
  * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears
299
320
  * unread on archive; the client also never counts them).
@@ -1 +1 @@
1
- {"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport { defaultTimers, isChatAccessDeniedError } from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n}\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => undefined,\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.setState({ status: 'closed', lastErrorCode: 'access-denied' });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n this.setState({ registry, hasUnread, unreadChannelRefs });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` on the inbox socket; the inbox DO advances the\n * member's read cursor to tail on the channel DO and then pushes a fresh\n * authoritative snapshot, which is what removes the row — there is NO optimistic\n * local clear, so a failed mark-read honestly leaves the dot in place.\n * Returns false when the socket is not open (the caller may surface a retry).\n */\n markRead(channelRef: string): boolean {\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n return sent;\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The current inbox DO snapshot\n * exposes a single `hasUnread` boolean (dock dot) + the active registry; it does\n * NOT yet enumerate which entries are unread. We surface a per-entry `unread`\n * field when the DO provides it (forward-compatible), else fall back to empty\n * per-row dots (the dock dot still lights). See the ASSUMPTIONS note in the docs.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
1
+ {"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n isTerminalErrorCode,\n TERMINAL_ERROR_CODE,\n} from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n}\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle\n // from a fresh (idle) or fully torn-down (closed) state. A second connect()\n // while one is already connecting / open / reconnecting is a no-op, so a\n // double-mounted dock cannot open a SECOND inbox socket against the per-uid\n // cap. The reconnect path uses openSocket() / scheduleReconnect() directly\n // (never connect()), so this never blocks a legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n // REVIVE the controller. `controller.close()` is PERMANENT: while it is in\n // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`\n // returns null — so without this reset a close() → connect() client opens a\n // socket and works normally until its FIRST transient close, then parks\n // terminally in `closed` with reconnect disabled and no surfaced error code.\n this.controller.reset();\n // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN\n // its lifecycle; an explicit connect() is a fresh attempt the authority may\n // legitimately re-deny, so the previous lifecycle's code must not persist.\n // The registry/unread projection is deliberately KEPT — the DO's next snapshot\n // is authoritative, and blanking the dock on every restart is pure UI churn.\n if (isTerminalErrorCode(this.state.lastErrorCode)) this.setState({ lastErrorCode: null });\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => undefined,\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n this.setState({ registry, hasUnread, unreadChannelRefs });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` on the inbox socket; the inbox DO advances the\n * member's read cursor to tail on the channel DO and then pushes a fresh\n * authoritative snapshot, which is what removes the row — there is NO optimistic\n * local clear, so a failed mark-read honestly leaves the dot in place.\n * Returns false when the socket is not open (the caller may surface a retry).\n */\n markRead(channelRef: string): boolean {\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n return sent;\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot\n * carries a per-entry `unread` boolean on every active registry entry alongside\n * the global `hasUnread` roll-up that drives the dock dot. An entry without the\n * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot\n * still lights off `hasUnread`.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
@@ -26,11 +26,13 @@ export declare function seqToMessageId(seq: number): string;
26
26
  * Convert a DO message row to the UI message. `threadId` is the caller's logical
27
27
  * thread id (the UI's thread key); the DO row carries no threadId of its own.
28
28
  * - `messageId` = the server seq (string) — stable + dedup key.
29
- * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
30
29
  *
31
30
  * A DO row carries TEXT only. Files are not part of the message timeline — they
32
31
  * live in the conversation's Conversation Files list, published through the
33
32
  * canonical media pipeline and read from Firestore, never mapped from a chat row.
33
+ * A legacy row's stored reply pointer is likewise NOT mapped: chat has no
34
+ * reply-authoring affordance, so the UI has no reply renderer to feed
35
+ * (DJ ruling 2026-07-29).
34
36
  */
35
37
  export declare function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1;
36
38
  /** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */
@@ -40,9 +42,5 @@ export declare function optimisticMessage(args: {
40
42
  senderId: string;
41
43
  text: string;
42
44
  createdAt: number;
43
- replyTo?: {
44
- messageId: string;
45
- messagePreview?: string;
46
- } | null;
47
45
  }): ChatMessageV1;
48
46
  //# sourceMappingURL=map.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAI9D,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,aAAa,CAc/G;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,GAAG,IAAI,CAG5E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAkBrF;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE,GAAG,aAAa,CAYhB"}
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAI9D,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,YAAY,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,aAAa,CAc/G;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,GAAG,IAAI,CAG5E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAarF;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,aAAa,CAShB"}
@@ -45,37 +45,25 @@ export function overlayFromRow(row) {
45
45
  export function seqToMessageId(seq) {
46
46
  return String(seq);
47
47
  }
48
- function parseJson(raw) {
49
- if (raw == null)
50
- return null;
51
- try {
52
- return JSON.parse(raw);
53
- }
54
- catch {
55
- return null;
56
- }
57
- }
58
48
  /**
59
49
  * Convert a DO message row to the UI message. `threadId` is the caller's logical
60
50
  * thread id (the UI's thread key); the DO row carries no threadId of its own.
61
51
  * - `messageId` = the server seq (string) — stable + dedup key.
62
- * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
63
52
  *
64
53
  * A DO row carries TEXT only. Files are not part of the message timeline — they
65
54
  * live in the conversation's Conversation Files list, published through the
66
55
  * canonical media pipeline and read from Firestore, never mapped from a chat row.
56
+ * A legacy row's stored reply pointer is likewise NOT mapped: chat has no
57
+ * reply-authoring affordance, so the UI has no reply renderer to feed
58
+ * (DJ ruling 2026-07-29).
67
59
  */
68
60
  export function wireRowToMessage(row, threadId) {
69
- const reply = parseJson(row.replyTo);
70
61
  return {
71
62
  messageId: seqToMessageId(row.seq),
72
63
  threadId,
73
64
  createdAt: row.createdAt,
74
65
  senderId: row.senderUid,
75
66
  text: row.text,
76
- replyTo: reply
77
- ? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }
78
- : undefined,
79
67
  isSystemMessage: row.senderUid === 'system' || undefined,
80
68
  meta: {
81
69
  seq: row.seq,
@@ -91,9 +79,6 @@ export function optimisticMessage(args) {
91
79
  createdAt: args.createdAt,
92
80
  senderId: args.senderId,
93
81
  text: args.text,
94
- replyTo: args.replyTo
95
- ? { messageId: args.replyTo.messageId, senderId: '', messagePreview: args.replyTo.messagePreview ?? '' }
96
- : undefined,
97
82
  meta: { clientMessageId: args.clientMessageId, optimistic: true },
98
83
  };
99
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"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","sourcesContent":["// Map the DO wire row (`WireMessageRow`) into the UI `ChatMessageV1` shape so the\n// UI stays transport-agnostic — the MessageList renders the same shape whether\n// the bytes came from Firestore or the DO socket.\n//\n// Identity invariant: the DO stores uid references only (`senderUid`), never a\n// display name — names resolve at render time through the ChatNameResolver\n// context, identical to the Firestore path.\n\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport { MODERATION_REDACTED_TEXT } from '@ttt-productions/chat-schemas';\nimport type { RevisionKind, WireMessageRow } from './wire.js';\n\n// The redacted-text constant lives in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { MODERATION_REDACTED_TEXT };\n\n/** The effective moderation overlay for a message: the max-`messageRevision` kind. */\nexport interface ModerationOverlay {\n kind: RevisionKind;\n messageRevision: number;\n}\n\n/**\n * Apply a moderation overlay to a mapped message so the ORIGINAL content never\n * renders for a hidden/deleted message. Pure — returns a new message; the input\n * is not mutated. The overlay state rides on `meta` (`meta` is opaque to\n * chat-core), so the renderer can branch on `meta.moderated` / `meta.moderationKind`.\n * - `delete` / `moderate` → blank/replace the text + mark moderated.\n * - `edit` → the row already carries the overlaid text (the DO rewrites `text`),\n * so only the marker is stamped.\n * - `restore` → original content (the DO row is the original); no redaction.\n */\nexport function applyModerationOverlay(message: ChatMessageV1, overlay: ModerationOverlay | null): ChatMessageV1 {\n if (!overlay) return message;\n const redacted = overlay.kind === 'delete' || overlay.kind === 'moderate';\n return {\n ...message,\n ...(redacted ? { text: MODERATION_REDACTED_TEXT } : {}),\n meta: {\n ...message.meta,\n moderationKind: overlay.kind,\n messageRevision: overlay.messageRevision,\n // `restore` is the absence of active moderation — it reverts to the original.\n moderated: overlay.kind !== 'restore',\n },\n };\n}\n\n/** The overlay carried inline on a DO row (the getHistory/getDeltaSince merge), or null. */\nexport function overlayFromRow(row: WireMessageRow): ModerationOverlay | null {\n if (row.moderationKind == null) return null;\n return { kind: row.moderationKind, messageRevision: row.messageRevision ?? 0 };\n}\n\n/** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */\nexport function seqToMessageId(seq: number): string {\n return String(seq);\n}\n\ninterface WireReplyTo {\n messageSeq: number;\n preview: string;\n}\n\nfunction parseJson<T>(raw: string | null): T | null {\n if (raw == null) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Convert a DO message row to the UI message. `threadId` is the caller's logical\n * thread id (the UI's thread key); the DO row carries no threadId of its own.\n * - `messageId` = the server seq (string) — stable + dedup key.\n * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.\n *\n * A DO row carries TEXT only. Files are not part of the message timeline — they\n * live in the conversation's Conversation Files list, published through the\n * canonical media pipeline and read from Firestore, never mapped from a chat row.\n */\nexport function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1 {\n const reply = parseJson<WireReplyTo>(row.replyTo);\n\n return {\n messageId: seqToMessageId(row.seq),\n threadId,\n createdAt: row.createdAt,\n senderId: row.senderUid,\n text: row.text,\n replyTo: reply\n ? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }\n : undefined,\n isSystemMessage: row.senderUid === 'system' || undefined,\n meta: {\n seq: row.seq,\n epoch: row.epoch,\n },\n };\n}\n\n/** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */\nexport function optimisticMessage(args: {\n clientMessageId: string;\n threadId: string;\n senderId: string;\n text: string;\n createdAt: number;\n replyTo?: { messageId: string; messagePreview?: string } | null;\n}): ChatMessageV1 {\n return {\n messageId: `optimistic:${args.clientMessageId}`,\n threadId: args.threadId,\n createdAt: args.createdAt,\n senderId: args.senderId,\n text: args.text,\n replyTo: args.replyTo\n ? { messageId: args.replyTo.messageId, senderId: '', messagePreview: args.replyTo.messagePreview ?? '' }\n : undefined,\n meta: { clientMessageId: args.clientMessageId, optimistic: true },\n };\n}\n"]}
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,+EAA+E;AAC/E,kDAAkD;AAClD,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,4CAA4C;AAG5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAGzE,gFAAgF;AAChF,kCAAkC;AAClC,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAQpC;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAsB,EAAE,OAAiC;IAC9F,IAAI,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC;IAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC;IAC1E,OAAO;QACL,GAAG,OAAO;QACV,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,IAAI,EAAE;YACJ,GAAG,OAAO,CAAC,IAAI;YACf,cAAc,EAAE,OAAO,CAAC,IAAI;YAC5B,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,8EAA8E;YAC9E,SAAS,EAAE,OAAO,CAAC,IAAI,KAAK,SAAS;SACtC;KACF,CAAC;AACJ,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,cAAc,CAAC,GAAmB;IAChD,IAAI,GAAG,CAAC,cAAc,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,cAAc,EAAE,eAAe,EAAE,GAAG,CAAC,eAAe,IAAI,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAmB,EAAE,QAAgB;IACpE,OAAO;QACL,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAClC,QAAQ;QACR,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,eAAe,EAAE,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,SAAS;QACxD,IAAI,EAAE;YACJ,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB;KACF,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,iBAAiB,CAAC,IAMjC;IACC,OAAO;QACL,SAAS,EAAE,cAAc,IAAI,CAAC,eAAe,EAAE;QAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,EAAE,IAAI,EAAE;KAClE,CAAC;AACJ,CAAC","sourcesContent":["// Map the DO wire row (`WireMessageRow`) into the UI `ChatMessageV1` shape so the\n// UI stays transport-agnostic — the MessageList renders the same shape whether\n// the bytes came from Firestore or the DO socket.\n//\n// Identity invariant: the DO stores uid references only (`senderUid`), never a\n// display name — names resolve at render time through the ChatNameResolver\n// context, identical to the Firestore path.\n\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport { MODERATION_REDACTED_TEXT } from '@ttt-productions/chat-schemas';\nimport type { RevisionKind, WireMessageRow } from './wire.js';\n\n// The redacted-text constant lives in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { MODERATION_REDACTED_TEXT };\n\n/** The effective moderation overlay for a message: the max-`messageRevision` kind. */\nexport interface ModerationOverlay {\n kind: RevisionKind;\n messageRevision: number;\n}\n\n/**\n * Apply a moderation overlay to a mapped message so the ORIGINAL content never\n * renders for a hidden/deleted message. Pure — returns a new message; the input\n * is not mutated. The overlay state rides on `meta` (`meta` is opaque to\n * chat-core), so the renderer can branch on `meta.moderated` / `meta.moderationKind`.\n * - `delete` / `moderate` → blank/replace the text + mark moderated.\n * - `edit` → the row already carries the overlaid text (the DO rewrites `text`),\n * so only the marker is stamped.\n * - `restore` → original content (the DO row is the original); no redaction.\n */\nexport function applyModerationOverlay(message: ChatMessageV1, overlay: ModerationOverlay | null): ChatMessageV1 {\n if (!overlay) return message;\n const redacted = overlay.kind === 'delete' || overlay.kind === 'moderate';\n return {\n ...message,\n ...(redacted ? { text: MODERATION_REDACTED_TEXT } : {}),\n meta: {\n ...message.meta,\n moderationKind: overlay.kind,\n messageRevision: overlay.messageRevision,\n // `restore` is the absence of active moderation — it reverts to the original.\n moderated: overlay.kind !== 'restore',\n },\n };\n}\n\n/** The overlay carried inline on a DO row (the getHistory/getDeltaSince merge), or null. */\nexport function overlayFromRow(row: WireMessageRow): ModerationOverlay | null {\n if (row.moderationKind == null) return null;\n return { kind: row.moderationKind, messageRevision: row.messageRevision ?? 0 };\n}\n\n/** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */\nexport function seqToMessageId(seq: number): string {\n return String(seq);\n}\n\n/**\n * Convert a DO message row to the UI message. `threadId` is the caller's logical\n * thread id (the UI's thread key); the DO row carries no threadId of its own.\n * - `messageId` = the server seq (string) — stable + dedup key.\n *\n * A DO row carries TEXT only. Files are not part of the message timeline — they\n * live in the conversation's Conversation Files list, published through the\n * canonical media pipeline and read from Firestore, never mapped from a chat row.\n * A legacy row's stored reply pointer is likewise NOT mapped: chat has no\n * reply-authoring affordance, so the UI has no reply renderer to feed\n * (DJ ruling 2026-07-29).\n */\nexport function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1 {\n return {\n messageId: seqToMessageId(row.seq),\n threadId,\n createdAt: row.createdAt,\n senderId: row.senderUid,\n text: row.text,\n isSystemMessage: row.senderUid === 'system' || undefined,\n meta: {\n seq: row.seq,\n epoch: row.epoch,\n },\n };\n}\n\n/** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */\nexport function optimisticMessage(args: {\n clientMessageId: string;\n threadId: string;\n senderId: string;\n text: string;\n createdAt: number;\n}): ChatMessageV1 {\n return {\n messageId: `optimistic:${args.clientMessageId}`,\n threadId: args.threadId,\n createdAt: args.createdAt,\n senderId: args.senderId,\n text: args.text,\n meta: { clientMessageId: args.clientMessageId, optimistic: true },\n };\n}\n"]}
@@ -39,6 +39,27 @@ export interface TransportTimers {
39
39
  export declare const defaultTimers: TransportTimers;
40
40
  /** Connection status surfaced to the UI. */
41
41
  export type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
42
+ /**
43
+ * The TERMINAL lifecycle verdicts both realtime clients surface on
44
+ * `lastErrorCode` — the two codes that mean "this lifecycle stopped on an
45
+ * authoritative NO", as opposed to a transient/protocol code (`send-failed`,
46
+ * `blocked-word`, …) that leaves the socket running.
47
+ *
48
+ * ONE declaration for both clients: each sets and clears the same two strings,
49
+ * and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.
50
+ * A terminal verdict is terminal WITHIN its lifecycle; a later explicit
51
+ * `connect()` is a fresh attempt the authority may legitimately re-deny, so the
52
+ * stale code is cleared there (never carried into the new lifecycle's state).
53
+ */
54
+ export declare const TERMINAL_ERROR_CODE: {
55
+ /** A 4403 REVOKED close: access was pulled while the socket was open. */
56
+ readonly REVOKED: "revoked";
57
+ /** The grant provider threw `ChatAccessDeniedError` — a terminal policy "no". */
58
+ readonly ACCESS_DENIED: "access-denied";
59
+ };
60
+ export type TerminalErrorCode = (typeof TERMINAL_ERROR_CODE)[keyof typeof TERMINAL_ERROR_CODE];
61
+ /** True when `code` is one of the terminal lifecycle verdicts above. */
62
+ export declare function isTerminalErrorCode(code: string | null | undefined): code is TerminalErrorCode;
42
63
  export { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';
43
64
  /**
44
65
  * Max reconnect resends of one un-acked send before its optimistic row is flipped
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,QAAQ;gBACvB,OAAO,SAAuB;CAI3C;AAED,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB,CAOlF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC1E,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;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"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,QAAQ;gBACvB,OAAO,SAAuB;CAI3C;AAED,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB,CAOlF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC1E,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAExF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB;IAC9B,yEAAyE;;IAEzE,iFAAiF;;CAEzE,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,OAAO,mBAAmB,CAAC,CAAC;AAE/F,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,iBAAiB,CAE9F;AAID,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,uBAAuB,QAAS,CAAC"}
@@ -43,6 +43,28 @@ export const defaultTimers = {
43
43
  clearTimeout: (h) => clearTimeout(h),
44
44
  now: () => Date.now(),
45
45
  };
46
+ /**
47
+ * The TERMINAL lifecycle verdicts both realtime clients surface on
48
+ * `lastErrorCode` — the two codes that mean "this lifecycle stopped on an
49
+ * authoritative NO", as opposed to a transient/protocol code (`send-failed`,
50
+ * `blocked-word`, …) that leaves the socket running.
51
+ *
52
+ * ONE declaration for both clients: each sets and clears the same two strings,
53
+ * and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.
54
+ * A terminal verdict is terminal WITHIN its lifecycle; a later explicit
55
+ * `connect()` is a fresh attempt the authority may legitimately re-deny, so the
56
+ * stale code is cleared there (never carried into the new lifecycle's state).
57
+ */
58
+ export const TERMINAL_ERROR_CODE = {
59
+ /** A 4403 REVOKED close: access was pulled while the socket was open. */
60
+ REVOKED: 'revoked',
61
+ /** The grant provider threw `ChatAccessDeniedError` — a terminal policy "no". */
62
+ ACCESS_DENIED: 'access-denied',
63
+ };
64
+ /** True when `code` is one of the terminal lifecycle verdicts above. */
65
+ export function isTerminalErrorCode(code) {
66
+ return code === TERMINAL_ERROR_CODE.REVOKED || code === TERMINAL_ERROR_CODE.ACCESS_DENIED;
67
+ }
46
68
  // The client-agreed limits live in the wire contract; re-export to keep this
47
69
  // module's public surface stable.
48
70
  export { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,yDAAyD;AACzD,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AAWjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IACzE,kBAAkB,GAAG,IAAI,CAAC;IACnC,YAAY,OAAO,GAAG,oBAAoB;QACxC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,OAAO,CACL,GAAG,YAAY,qBAAqB;QACpC,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,KAAK,IAAI;YACX,GAAwC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CACzE,CAAC;AACJ,CAAC;AASD,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAKF,6EAA6E;AAC7E,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC","sourcesContent":["// Shared injected contracts + tunable constants for the realtime transport.\n//\n// The client-agreed limits (`HEARTBEAT_MS`, `TYPING_COALESCE_MS`,\n// `HISTORY_PAGE_MAX`) come from the one wire contract in\n// `@ttt-productions/chat-schemas`; they are re-exported here so the transport's\n// existing import sites are unchanged. The worker runtime is the authority and\n// rejects anything out of bounds, but the client uses these to coalesce and\n// paginate politely. The remaining constants below are client-only retry policy.\n\n/**\n * Mints a short-lived chat grant token for the socket's scope. The app wires this\n * to its `mintChatGrant` callable (channel grant for a ChannelClient, inbox grant\n * for an InboxClient). Called on every connect AND once on a 4401 re-grant. The\n * app owns the Firestore permission check, the scope shape, caching to a React\n * Query staleTime < grant exp, and returning a FRESH token when re-invoked.\n */\nexport type GrantProvider = () => Promise<string>;\n\n/**\n * Package-owned, Firebase-FREE terminal access-denial signal. A grant provider\n * throws this (from `GrantProvider`) when the authoritative authorization answer\n * is a genuine, terminal \"no\" — as opposed to a transient/retryable mint failure.\n *\n * The transport treats it specially: instead of the normal reconnect backoff it\n * STOPS reconnecting, closes the lifecycle, fails any pending optimistic sends,\n * and surfaces the stable `access-denied` error code (which the React hook maps to\n * `allowed: false` so the shell renders its no-access surface instead of an eternal\n * loader). Every other thrown error stays transient and reconnects as before.\n *\n * This is deliberately generic: the consuming app translates its own backend\n * denial (e.g. a Firebase `functions/permission-denied`) into this class at the\n * grant-provider boundary. The package never imports or names Firebase. Consumers\n * that cannot subclass may instead throw any error carrying `isChatAccessDenied:\n * true` — `isChatAccessDeniedError` recognizes both (also cross-realm safe).\n */\nexport class ChatAccessDeniedError extends Error {\n /** Duck-typed marker so detection survives module-realm / re-throw boundaries. */\n readonly isChatAccessDenied = true;\n constructor(message = 'Chat access denied') {\n super(message);\n this.name = 'ChatAccessDeniedError';\n }\n}\n\n/** True when `err` is a terminal chat access denial (class instance or duck-typed marker). */\nexport function isChatAccessDeniedError(err: unknown): err is ChatAccessDeniedError {\n return (\n err instanceof ChatAccessDeniedError ||\n (typeof err === 'object' &&\n err !== null &&\n (err as { isChatAccessDenied?: unknown }).isChatAccessDenied === true)\n );\n}\n\n/** Injectable timers so the transport runs deterministically under fake timers in tests. */\nexport interface TransportTimers {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void;\n now: () => number;\n}\n\nexport const defaultTimers: TransportTimers = {\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n now: () => Date.now(),\n};\n\n/** Connection status surfaced to the UI. */\nexport type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n// The client-agreed limits live in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';\n\n/**\n * Max reconnect resends of one un-acked send before its optimistic row is flipped\n * to a visible failed state (retry affordance) instead of a permanent ghost. Each\n * reconnect that resumes with the pending send still un-acked counts one attempt.\n */\nexport const MAX_PENDING_SEND_ATTEMPTS = 5;\n\n/**\n * Max wall-clock age of an un-acked pending send before it is flipped to failed on\n * the next resume, even if the attempt cap hasn't been hit (a long offline window).\n */\nexport const PENDING_SEND_MAX_AGE_MS = 60_000;\n\n/**\n * Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has\n * rejected a send as retryable — a correlated `send-rejected` frame OR the legacy\n * uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the\n * server's own hint until this budget elapses, and is NEVER failed by the generic\n * {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect\n * resends). 90 s comfortably covers the backend projection/grant-bootstrap window\n * without stranding an optimistic bubble on \"Sending…\" the way the old ~10 s\n * five-attempt cap did.\n */\nexport const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;\n\n/**\n * Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a\n * resend, so a pathological (but schema-valid) hint can neither hammer the socket\n * nor park a send for minutes.\n */\nexport const SEND_RETRY_MIN_DELAY_MS = 250;\nexport const SEND_RETRY_MAX_DELAY_MS = 30_000;\n"]}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,kEAAkE;AAClE,yDAAyD;AACzD,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AAWjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,kFAAkF;IACzE,kBAAkB,GAAG,IAAI,CAAC;IACnC,YAAY,OAAO,GAAG,oBAAoB;QACxC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,OAAO,CACL,GAAG,YAAY,qBAAqB;QACpC,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,KAAK,IAAI;YACX,GAAwC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CACzE,CAAC;AACJ,CAAC;AASD,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAKF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,yEAAyE;IACzE,OAAO,EAAE,SAAS;IAClB,iFAAiF;IACjF,aAAa,EAAE,eAAe;CACtB,CAAC;AAIX,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,OAAO,IAAI,KAAK,mBAAmB,CAAC,OAAO,IAAI,IAAI,KAAK,mBAAmB,CAAC,aAAa,CAAC;AAC5F,CAAC;AAED,6EAA6E;AAC7E,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC","sourcesContent":["// Shared injected contracts + tunable constants for the realtime transport.\n//\n// The client-agreed limits (`HEARTBEAT_MS`, `TYPING_COALESCE_MS`,\n// `HISTORY_PAGE_MAX`) come from the one wire contract in\n// `@ttt-productions/chat-schemas`; they are re-exported here so the transport's\n// existing import sites are unchanged. The worker runtime is the authority and\n// rejects anything out of bounds, but the client uses these to coalesce and\n// paginate politely. The remaining constants below are client-only retry policy.\n\n/**\n * Mints a short-lived chat grant token for the socket's scope. The app wires this\n * to its `mintChatGrant` callable (channel grant for a ChannelClient, inbox grant\n * for an InboxClient). Called on every connect AND once on a 4401 re-grant. The\n * app owns the Firestore permission check, the scope shape, caching to a React\n * Query staleTime < grant exp, and returning a FRESH token when re-invoked.\n */\nexport type GrantProvider = () => Promise<string>;\n\n/**\n * Package-owned, Firebase-FREE terminal access-denial signal. A grant provider\n * throws this (from `GrantProvider`) when the authoritative authorization answer\n * is a genuine, terminal \"no\" — as opposed to a transient/retryable mint failure.\n *\n * The transport treats it specially: instead of the normal reconnect backoff it\n * STOPS reconnecting, closes the lifecycle, fails any pending optimistic sends,\n * and surfaces the stable `access-denied` error code (which the React hook maps to\n * `allowed: false` so the shell renders its no-access surface instead of an eternal\n * loader). Every other thrown error stays transient and reconnects as before.\n *\n * This is deliberately generic: the consuming app translates its own backend\n * denial (e.g. a Firebase `functions/permission-denied`) into this class at the\n * grant-provider boundary. The package never imports or names Firebase. Consumers\n * that cannot subclass may instead throw any error carrying `isChatAccessDenied:\n * true` — `isChatAccessDeniedError` recognizes both (also cross-realm safe).\n */\nexport class ChatAccessDeniedError extends Error {\n /** Duck-typed marker so detection survives module-realm / re-throw boundaries. */\n readonly isChatAccessDenied = true;\n constructor(message = 'Chat access denied') {\n super(message);\n this.name = 'ChatAccessDeniedError';\n }\n}\n\n/** True when `err` is a terminal chat access denial (class instance or duck-typed marker). */\nexport function isChatAccessDeniedError(err: unknown): err is ChatAccessDeniedError {\n return (\n err instanceof ChatAccessDeniedError ||\n (typeof err === 'object' &&\n err !== null &&\n (err as { isChatAccessDenied?: unknown }).isChatAccessDenied === true)\n );\n}\n\n/** Injectable timers so the transport runs deterministically under fake timers in tests. */\nexport interface TransportTimers {\n setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n clearTimeout: (h: ReturnType<typeof setTimeout>) => void;\n now: () => number;\n}\n\nexport const defaultTimers: TransportTimers = {\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n now: () => Date.now(),\n};\n\n/** Connection status surfaced to the UI. */\nexport type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\n/**\n * The TERMINAL lifecycle verdicts both realtime clients surface on\n * `lastErrorCode` — the two codes that mean \"this lifecycle stopped on an\n * authoritative NO\", as opposed to a transient/protocol code (`send-failed`,\n * `blocked-word`, …) that leaves the socket running.\n *\n * ONE declaration for both clients: each sets and clears the same two strings,\n * and `useRealtimeChatMessages` maps `access-denied` to `allowed: false`.\n * A terminal verdict is terminal WITHIN its lifecycle; a later explicit\n * `connect()` is a fresh attempt the authority may legitimately re-deny, so the\n * stale code is cleared there (never carried into the new lifecycle's state).\n */\nexport const TERMINAL_ERROR_CODE = {\n /** A 4403 REVOKED close: access was pulled while the socket was open. */\n REVOKED: 'revoked',\n /** The grant provider threw `ChatAccessDeniedError` — a terminal policy \"no\". */\n ACCESS_DENIED: 'access-denied',\n} as const;\n\nexport type TerminalErrorCode = (typeof TERMINAL_ERROR_CODE)[keyof typeof TERMINAL_ERROR_CODE];\n\n/** True when `code` is one of the terminal lifecycle verdicts above. */\nexport function isTerminalErrorCode(code: string | null | undefined): code is TerminalErrorCode {\n return code === TERMINAL_ERROR_CODE.REVOKED || code === TERMINAL_ERROR_CODE.ACCESS_DENIED;\n}\n\n// The client-agreed limits live in the wire contract; re-export to keep this\n// module's public surface stable.\nexport { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';\n\n/**\n * Max reconnect resends of one un-acked send before its optimistic row is flipped\n * to a visible failed state (retry affordance) instead of a permanent ghost. Each\n * reconnect that resumes with the pending send still un-acked counts one attempt.\n */\nexport const MAX_PENDING_SEND_ATTEMPTS = 5;\n\n/**\n * Max wall-clock age of an un-acked pending send before it is flipped to failed on\n * the next resume, even if the attempt cap hasn't been hit (a long offline window).\n */\nexport const PENDING_SEND_MAX_AGE_MS = 60_000;\n\n/**\n * Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has\n * rejected a send as retryable — a correlated `send-rejected` frame OR the legacy\n * uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the\n * server's own hint until this budget elapses, and is NEVER failed by the generic\n * {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect\n * resends). 90 s comfortably covers the backend projection/grant-bootstrap window\n * without stranding an optimistic bubble on \"Sending…\" the way the old ~10 s\n * five-attempt cap did.\n */\nexport const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;\n\n/**\n * Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a\n * resend, so a pathological (but schema-valid) hint can neither hammer the socket\n * nor park a send for minutes.\n */\nexport const SEND_RETRY_MIN_DELAY_MS = 250;\nexport const SEND_RETRY_MAX_DELAY_MS = 30_000;\n"]}
@@ -16,10 +16,7 @@ export type UseRealtimeChatMessagesResult = {
16
16
  typing: string[];
17
17
  presence: string[];
18
18
  /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */
19
- send: (text: string, replyTo?: {
20
- messageSeq: number;
21
- preview: string;
22
- } | null) => boolean;
19
+ send: (text: string) => boolean;
23
20
  /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the
24
21
  * failed row's `meta.clientMessageId`) — the transport re-queues the tracked
25
22
  * pending send with the SAME id so the DO's send-idempotency dedups any copy
@@ -1 +1 @@
1
- {"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC;4CACwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iHAAiH;IACjH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,OAAO,CAAC;IAC1F;;;;;uCAKmC;IACnC,SAAS,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,sIAAsI;IACtI,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB;;yEAEqE;IACrE,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,CA6EjG"}
1
+ {"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC;4CACwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iHAAiH;IACjH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC;;;;;uCAKmC;IACnC,SAAS,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,sIAAsI;IACtI,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB;;yEAEqE;IACrE,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,CA4EjG"}
@@ -25,8 +25,6 @@ export function useRealtimeChatMessages(client) {
25
25
  const set = new Set();
26
26
  for (const m of state.messages) {
27
27
  set.add(m.senderId);
28
- if (m.replyTo?.senderId)
29
- set.add(m.replyTo.senderId);
30
28
  }
31
29
  return Array.from(set);
32
30
  }, [state.messages]);
@@ -37,7 +35,7 @@ export function useRealtimeChatMessages(client) {
37
35
  const fetchOlder = React.useCallback(async () => {
38
36
  client.channel.fetchOlder();
39
37
  }, [client]);
40
- const send = React.useCallback((text, replyTo) => client.channel.send({ clientMessageId: makeClientMessageId(), text, replyTo: replyTo ?? null }), [client]);
38
+ const send = React.useCallback((text) => client.channel.send({ clientMessageId: makeClientMessageId(), text }), [client]);
41
39
  const retrySend = React.useCallback((clientMessageId) => client.channel.retrySend(clientMessageId), [client]);
42
40
  const readAck = React.useCallback((readSeq, focused) => client.channel.readAck(readSeq, focused), [client]);
43
41
  const signalTyping = React.useCallback(() => client.channel.typing(), [client]);
@@ -1 +1 @@
1
- {"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// React hook for the REALTIME transport, returning the SAME shape as\n// `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.\n// It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the\n// socket lifecycle: the caller (the app hook that CREATES the client, e.g.\n// `useRealtimeChannelClient`) is the single connect/close owner (C-B7) — connecting\n// here too opened a second WebSocket. The client instance is owned by the caller\n// (passed via config) so its identity is the subscription key — a new client for a\n// new uid replaces the old one (Contract A auth-user-switch teardown happens in the\n// owning hook).\n\nimport * as React from \"react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { RealtimeChatClient } from \"./transport.js\";\nimport type { ChannelClientState } from \"./channel-client.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\nexport type UseRealtimeChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** Realtime extras (not present on the firestore result) — optional for the UI. */\n status: ChannelClientState[\"status\"];\n /** The last structured error code the DO sent (e.g. a close reason). Drives the\n * disconnected-banner detail (R13). */\n lastErrorCode: string | null;\n typing: string[];\n presence: string[];\n /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */\n send: (text: string, replyTo?: { messageSeq: number; preview: string } | null) => boolean;\n /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the\n * failed row's `meta.clientMessageId`) — the transport re-queues the tracked\n * pending send with the SAME id so the DO's send-idempotency dedups any copy\n * that already landed. Returns false for an unknown id (already acked/\n * reconciled). ChatShell wires this to MessageItemDefault's failed-bubble\n * retry affordance by default. */\n retrySend: (clientMessageId: string) => boolean;\n /** Returns whether the ack frame was sent (false on a closed socket) so the caller advances its local cursor only on success (M2). */\n readAck: (readSeq: number, focused: boolean) => boolean;\n signalTyping: () => void;\n /** Watched-only presence (R14): the UI subscribes ONLY while a member-list / online\n * indicator is open, and unsubscribes when it closes, so an unwatched channel runs\n * zero presence alarms. `presence` stays empty until subscribed. */\n presenceSubscribe: () => void;\n presenceUnsubscribe: () => void;\n};\n\nexport function useRealtimeChatMessages(client: RealtimeChatClient): UseRealtimeChatMessagesResult {\n const [state, setState] = React.useState<ChannelClientState>(() => client.getState());\n\n // Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).\n // Re-subscribes on client identity change (auth-user switch).\n React.useEffect(() => {\n const unsub = client.subscribe(setState);\n setState(client.getState());\n return unsub;\n }, [client]);\n\n // Pre-warm the name cache for visible senders (parity with the firestore hook).\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of state.messages) {\n set.add(m.senderId);\n if (m.replyTo?.senderId) set.add(m.replyTo.senderId);\n }\n return Array.from(set);\n }, [state.messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) resolverCtx.prewarm(senderIds);\n }, [resolverCtx, senderIds]);\n\n const fetchOlder = React.useCallback(async () => {\n client.channel.fetchOlder();\n }, [client]);\n\n const send = React.useCallback(\n (text: string, replyTo?: { messageSeq: number; preview: string } | null): boolean =>\n client.channel.send({ clientMessageId: makeClientMessageId(), text, replyTo: replyTo ?? null }),\n [client],\n );\n\n const retrySend = React.useCallback(\n (clientMessageId: string): boolean => client.channel.retrySend(clientMessageId),\n [client],\n );\n\n const readAck = React.useCallback(\n (readSeq: number, focused: boolean) => client.channel.readAck(readSeq, focused),\n [client],\n );\n\n const signalTyping = React.useCallback(() => client.channel.typing(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(() => client.channel.presenceUnsubscribe(), [client]);\n\n return {\n // On the realtime transport DATA ACCESS is enforced by the DO grant, so the hook\n // is \"allowed\" client-side UNLESS the grant provider surfaced a TERMINAL access\n // denial (ChatAccessDeniedError → stable 'access-denied' code). That single case\n // maps to allowed:false so ChatShell renders its no-access surface instead of an\n // eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the\n // reconnect/disconnected banner. Otherwise accessMode is presentation-only here.\n allowed: state.lastErrorCode !== \"access-denied\",\n // Derive initial loading from authoritative-data completion, NOT connection status:\n // a socket can be 'open' with no snapshot yet, and a post-load reconnect must not\n // fall back into the opening state. `hasLoadedInitialData` encodes the full table.\n isInitialLoading: !state.hasLoadedInitialData,\n messages: state.messages,\n fetchOlder,\n hasOlder: state.hasOlder,\n isFetchingOlder: state.isFetchingOlder,\n status: state.status,\n lastErrorCode: state.lastErrorCode,\n typing: state.typing,\n presence: state.presence,\n send,\n retrySend,\n readAck,\n signalTyping,\n presenceSubscribe,\n presenceUnsubscribe,\n };\n}\n\n/** A client message id for optimistic-send idempotency (SQLite unique (senderUid, clientMessageId)). */\nfunction makeClientMessageId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cmid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
1
+ {"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,qEAAqE;AACrE,+EAA+E;AAC/E,kFAAkF;AAClF,2EAA2E;AAC3E,oFAAoF;AACpF,iFAAiF;AACjF,mFAAmF;AACnF,oFAAoF;AACpF,gBAAgB;AAEhB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AAmCpF,MAAM,UAAU,uBAAuB,CAAC,MAA0B;IAChE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAqB,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAEtF,uFAAuF;IACvF,8DAA8D;IAC9D,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACzC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,gFAAgF;IAChF,MAAM,WAAW,GAAG,2BAA2B,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC/B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtB,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAErB,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,WAAW,EAAE,OAAO,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACnF,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7B,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,IAAI,EAAE;QAC9C,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAC9B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAC5B,CAAC,IAAY,EAAW,EAAE,CACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,mBAAmB,EAAE,EAAE,IAAI,EAAE,CAAC,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CACjC,CAAC,eAAuB,EAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,EAC/E,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAC/B,CAAC,OAAe,EAAE,OAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,EAC/E,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAChF,MAAM,iBAAiB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAChG,MAAM,mBAAmB,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEpG,OAAO;QACL,iFAAiF;QACjF,gFAAgF;QAChF,iFAAiF;QACjF,iFAAiF;QACjF,mFAAmF;QACnF,iFAAiF;QACjF,OAAO,EAAE,KAAK,CAAC,aAAa,KAAK,eAAe;QAChD,oFAAoF;QACpF,kFAAkF;QAClF,mFAAmF;QACnF,gBAAgB,EAAE,CAAC,KAAK,CAAC,oBAAoB;QAC7C,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU;QACV,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI;QACJ,SAAS;QACT,OAAO;QACP,YAAY;QACZ,iBAAiB;QACjB,mBAAmB;KACpB,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,SAAS,mBAAmB;IAC1B,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,EAAE,UAAU;QAAE,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IACzC,OAAO,QAAQ,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACrE,CAAC","sourcesContent":["\"use client\";\n\n// React hook for the REALTIME transport, returning the SAME shape as\n// `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.\n// It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the\n// socket lifecycle: the caller (the app hook that CREATES the client, e.g.\n// `useRealtimeChannelClient`) is the single connect/close owner (C-B7) — connecting\n// here too opened a second WebSocket. The client instance is owned by the caller\n// (passed via config) so its identity is the subscription key — a new client for a\n// new uid replaces the old one (Contract A auth-user-switch teardown happens in the\n// owning hook).\n\nimport * as React from \"react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { RealtimeChatClient } from \"./transport.js\";\nimport type { ChannelClientState } from \"./channel-client.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\nexport type UseRealtimeChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n /** Realtime extras (not present on the firestore result) — optional for the UI. */\n status: ChannelClientState[\"status\"];\n /** The last structured error code the DO sent (e.g. a close reason). Drives the\n * disconnected-banner detail (R13). */\n lastErrorCode: string | null;\n typing: string[];\n presence: string[];\n /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */\n send: (text: string) => boolean;\n /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the\n * failed row's `meta.clientMessageId`) — the transport re-queues the tracked\n * pending send with the SAME id so the DO's send-idempotency dedups any copy\n * that already landed. Returns false for an unknown id (already acked/\n * reconciled). ChatShell wires this to MessageItemDefault's failed-bubble\n * retry affordance by default. */\n retrySend: (clientMessageId: string) => boolean;\n /** Returns whether the ack frame was sent (false on a closed socket) so the caller advances its local cursor only on success (M2). */\n readAck: (readSeq: number, focused: boolean) => boolean;\n signalTyping: () => void;\n /** Watched-only presence (R14): the UI subscribes ONLY while a member-list / online\n * indicator is open, and unsubscribes when it closes, so an unwatched channel runs\n * zero presence alarms. `presence` stays empty until subscribed. */\n presenceSubscribe: () => void;\n presenceUnsubscribe: () => void;\n};\n\nexport function useRealtimeChatMessages(client: RealtimeChatClient): UseRealtimeChatMessagesResult {\n const [state, setState] = React.useState<ChannelClientState>(() => client.getState());\n\n // Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).\n // Re-subscribes on client identity change (auth-user switch).\n React.useEffect(() => {\n const unsub = client.subscribe(setState);\n setState(client.getState());\n return unsub;\n }, [client]);\n\n // Pre-warm the name cache for visible senders (parity with the firestore hook).\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of state.messages) {\n set.add(m.senderId);\n }\n return Array.from(set);\n }, [state.messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) resolverCtx.prewarm(senderIds);\n }, [resolverCtx, senderIds]);\n\n const fetchOlder = React.useCallback(async () => {\n client.channel.fetchOlder();\n }, [client]);\n\n const send = React.useCallback(\n (text: string): boolean =>\n client.channel.send({ clientMessageId: makeClientMessageId(), text }),\n [client],\n );\n\n const retrySend = React.useCallback(\n (clientMessageId: string): boolean => client.channel.retrySend(clientMessageId),\n [client],\n );\n\n const readAck = React.useCallback(\n (readSeq: number, focused: boolean) => client.channel.readAck(readSeq, focused),\n [client],\n );\n\n const signalTyping = React.useCallback(() => client.channel.typing(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(() => client.channel.presenceUnsubscribe(), [client]);\n\n return {\n // On the realtime transport DATA ACCESS is enforced by the DO grant, so the hook\n // is \"allowed\" client-side UNLESS the grant provider surfaced a TERMINAL access\n // denial (ChatAccessDeniedError → stable 'access-denied' code). That single case\n // maps to allowed:false so ChatShell renders its no-access surface instead of an\n // eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the\n // reconnect/disconnected banner. Otherwise accessMode is presentation-only here.\n allowed: state.lastErrorCode !== \"access-denied\",\n // Derive initial loading from authoritative-data completion, NOT connection status:\n // a socket can be 'open' with no snapshot yet, and a post-load reconnect must not\n // fall back into the opening state. `hasLoadedInitialData` encodes the full table.\n isInitialLoading: !state.hasLoadedInitialData,\n messages: state.messages,\n fetchOlder,\n hasOlder: state.hasOlder,\n isFetchingOlder: state.isFetchingOlder,\n status: state.status,\n lastErrorCode: state.lastErrorCode,\n typing: state.typing,\n presence: state.presence,\n send,\n retrySend,\n readAck,\n signalTyping,\n presenceSubscribe,\n presenceUnsubscribe,\n };\n}\n\n/** A client message id for optimistic-send idempotency (SQLite unique (senderUid, clientMessageId)). */\nfunction makeClientMessageId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cmid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
@@ -11,18 +11,19 @@ import type { ChannelRefTuple, ChatSendRejectedPayload } from '@ttt-productions/
11
11
  export type RevisionKind = 'delete' | 'moderate' | 'edit' | 'restore';
12
12
  /**
13
13
  * A message row as the Channel DO stores + broadcasts it (the worker runtime's
14
- * message row). `replyTo` arrives as a JSON STRING (or null) — the DO stores it
15
- * stringified and does not re-parse before broadcast. The adapter mapping in
16
- * `map.ts` parses it into the UI `ChatMessageV1` shape.
14
+ * message row), declared as the fields the client actually CONSUMES. The adapter
15
+ * mapping in `map.ts` turns it into the UI `ChatMessageV1` shape.
17
16
  *
18
- * The row is TEXT-only: there is no attachment lifecycle on a chat message.
17
+ * The row is TEXT-only: there is no attachment lifecycle on a chat message, and
18
+ * no reply pointer — chat has no reply-authoring affordance, so the client
19
+ * neither sends nor reads one (DJ ruling 2026-07-29). A DO that still broadcasts
20
+ * a legacy stringified `replyTo` key is simply ignored here.
19
21
  */
20
22
  export interface WireMessageRow {
21
23
  seq: number;
22
24
  senderUid: string;
23
25
  clientMessageId: string;
24
26
  text: string;
25
- replyTo: string | null;
26
27
  createdAt: number;
27
28
  epoch: number;
28
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}