@ttt-productions/chat-react 0.13.16 → 0.13.18

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 (85) hide show
  1. package/README.md +3 -3
  2. package/dist/context/ChatNameResolverContext.d.ts +0 -23
  3. package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
  4. package/dist/context/ChatNameResolverContext.js +0 -16
  5. package/dist/context/ChatNameResolverContext.js.map +1 -1
  6. package/dist/hooks/useChatMessages.d.ts +0 -4
  7. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  8. package/dist/hooks/useChatMessages.js +0 -10
  9. package/dist/hooks/useChatMessages.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/realtime/channel-client.d.ts +0 -248
  14. package/dist/realtime/channel-client.d.ts.map +1 -1
  15. package/dist/realtime/channel-client.js +5 -432
  16. package/dist/realtime/channel-client.js.map +1 -1
  17. package/dist/realtime/diagnostics.d.ts +0 -62
  18. package/dist/realtime/diagnostics.d.ts.map +1 -1
  19. package/dist/realtime/diagnostics.js +4 -89
  20. package/dist/realtime/diagnostics.js.map +1 -1
  21. package/dist/realtime/inbox-client.d.ts +0 -71
  22. package/dist/realtime/inbox-client.d.ts.map +1 -1
  23. package/dist/realtime/inbox-client.js +0 -98
  24. package/dist/realtime/inbox-client.js.map +1 -1
  25. package/dist/realtime/initial-load-budget.d.ts +1 -16
  26. package/dist/realtime/initial-load-budget.d.ts.map +1 -1
  27. package/dist/realtime/initial-load-budget.js +1 -20
  28. package/dist/realtime/initial-load-budget.js.map +1 -1
  29. package/dist/realtime/map.d.ts +0 -26
  30. package/dist/realtime/map.d.ts.map +1 -1
  31. package/dist/realtime/map.js +0 -33
  32. package/dist/realtime/map.js.map +1 -1
  33. package/dist/realtime/shared.d.ts +0 -78
  34. package/dist/realtime/shared.d.ts.map +1 -1
  35. package/dist/realtime/shared.js +2 -76
  36. package/dist/realtime/shared.js.map +1 -1
  37. package/dist/realtime/socket.d.ts +0 -15
  38. package/dist/realtime/socket.d.ts.map +1 -1
  39. package/dist/realtime/socket.js +0 -4
  40. package/dist/realtime/socket.js.map +1 -1
  41. package/dist/realtime/transport.d.ts +0 -23
  42. package/dist/realtime/transport.d.ts.map +1 -1
  43. package/dist/realtime/transport.js +0 -13
  44. package/dist/realtime/transport.js.map +1 -1
  45. package/dist/realtime/useRealtimeChatMessages.d.ts +0 -17
  46. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  47. package/dist/realtime/useRealtimeChatMessages.js +0 -22
  48. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  49. package/dist/realtime/wire.d.ts +0 -56
  50. package/dist/realtime/wire.d.ts.map +1 -1
  51. package/dist/realtime/wire.js +0 -16
  52. package/dist/realtime/wire.js.map +1 -1
  53. package/dist/types.d.ts +0 -39
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +0 -3
  56. package/dist/types.js.map +1 -1
  57. package/dist/ui/ChatShell.d.ts +0 -42
  58. package/dist/ui/ChatShell.d.ts.map +1 -1
  59. package/dist/ui/ChatShell.js +4 -41
  60. package/dist/ui/ChatShell.js.map +1 -1
  61. package/dist/ui/Composer.d.ts +0 -13
  62. package/dist/ui/Composer.d.ts.map +1 -1
  63. package/dist/ui/Composer.js +0 -13
  64. package/dist/ui/Composer.js.map +1 -1
  65. package/dist/ui/MessageItemDefault.d.ts +0 -5
  66. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  67. package/dist/ui/MessageItemDefault.js +2 -34
  68. package/dist/ui/MessageItemDefault.js.map +1 -1
  69. package/dist/ui/MessageList.d.ts +0 -24
  70. package/dist/ui/MessageList.d.ts.map +1 -1
  71. package/dist/ui/MessageList.js +4 -14
  72. package/dist/ui/MessageList.js.map +1 -1
  73. package/dist/ui/MessageText.d.ts +0 -13
  74. package/dist/ui/MessageText.d.ts.map +1 -1
  75. package/dist/ui/MessageText.js.map +1 -1
  76. package/dist/ui/SenderName.d.ts +0 -5
  77. package/dist/ui/SenderName.d.ts.map +1 -1
  78. package/dist/ui/SenderName.js +0 -5
  79. package/dist/ui/SenderName.js.map +1 -1
  80. package/dist/ui/menus.d.ts +0 -2
  81. package/dist/ui/menus.d.ts.map +1 -1
  82. package/dist/ui/menus.js +6 -10
  83. package/dist/ui/menus.js.map +1 -1
  84. package/package.json +10 -10
  85. package/src/styles/chat.css +2 -15
@@ -1,144 +1,59 @@
1
- // OPT-IN structured client diagnostics for the realtime chat transport.
2
- //
3
- // The E2E/browser harness can already capture the raw WebSocket frames; what it
4
- // cannot see is what the CLIENT DECIDED about them — which frame it applied,
5
- // which it dropped and why, what resume cursor it sent, and whether a send was
6
- // reconciled or failed. This module is that missing half: a bounded, structured,
7
- // opt-in decision log.
8
- //
9
- // The connection-time, resume, presence, and socket-timing instruments are the
10
- // baseline-campaign metric families — they stay. Only the attachment-serving
11
- // instruments were retired with the chat-attachment architecture.
12
- //
13
- // Rules this module exists to enforce:
14
- // - OFF BY DEFAULT and zero behavior change. When the option is absent the
15
- // factory returns `null`, every call site is `this.diag?.(...)`, and JS
16
- // optional-call short-circuiting means the payload object is never even
17
- // constructed. The cost when off is one nullish check.
18
- // - NEVER user content. Ids, seqs, states, counts, codes, booleans only —
19
- // never message text, captions, filenames, URLs, tokens, or previews. The
20
- // INBOX events go one step further and log SIZES only: a `channelRef` names
21
- // one specific conversation, so registry/unread lines carry counts and
22
- // deltas, never the refs themselves.
23
- // - STABLE event names (`chat_client_*`) suitable for log queries and later
24
- // alert metrics; one line per DECISION, never per render or per heartbeat.
25
- // - A diagnostics sink can never break the client: every emit is wrapped, so a
26
- // throwing sink is swallowed rather than turned into a transport failure.
27
- /**
28
- * Stable event names. These are a QUERY CONTRACT — Cloud Logging filters, E2E
29
- * console greps, and any later alert metric key on them, so a name is added or
30
- * retired deliberately, never renamed in place.
31
- */
1
+ // Log queries key on these names: add or retire one, never rename it in place.
32
2
  export const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {
33
- /** A socket open is being attempted (per attempt, with the reconnect cause). */
34
3
  CONNECT_ATTEMPT: 'chat_client_connect_attempt',
35
- /** The grant mint failed before a socket existed (terminal vs. retryable). */
36
4
  GRANT_FAILED: 'chat_client_grant_failed',
37
- /** The socket reached OPEN (the point the resume cursor is sent from). */
38
5
  SOCKET_OPEN: 'chat_client_socket_open',
39
- /** The socket closed, with the code and what the client decided to do next. */
40
6
  SOCKET_CLOSE: 'chat_client_socket_close',
41
- /**
42
- * CHANNEL ONLY. The dead-socket watchdog closed a socket that had gone silent
43
- * past the client's inbound-silence threshold (the channel DO auto-acks every
44
- * heartbeat, so silence there is provable death). One line per detection —
45
- * never per heartbeat — followed by the ordinary socket_close/reconnect lines.
46
- */
47
7
  SOCKET_LIVENESS_TIMEOUT: 'chat_client_socket_liveness_timeout',
48
- /** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */
49
8
  RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',
50
- /** The resume cursor sent to the DO. */
51
9
  RESUME_REQUEST: 'chat_client_resume_request',
52
- /** The DO's snapshot answer + how the cursor moved because of it. */
53
10
  RESUME_RESULT: 'chat_client_resume_result',
54
- /** A `resync: true` snapshot dropped the local tail (removal, not upsert). */
55
11
  RESYNC_DROPPED_TAIL: 'chat_client_resync_dropped_tail',
56
- /** A frame was applied to UI state (insert / replace-by-seq / optimistic reconcile). */
57
12
  FRAME_APPLIED: 'chat_client_frame_applied',
58
- /** A frame was NOT applied, with the reason. */
59
13
  FRAME_DROPPED: 'chat_client_frame_dropped',
60
- /** A moderation revision overlay was recorded (and whether a loaded row re-rendered). */
61
14
  REVISION_APPLIED: 'chat_client_revision_applied',
62
- /** A history page was requested (cursor + limit). */
63
15
  HISTORY_REQUEST: 'chat_client_history_request',
64
- /** A history page was applied: size, seq range, cursor advance. */
65
16
  HISTORY_PAGE: 'chat_client_history_page',
66
- /** An optimistic send was rendered and its frame written. */
67
17
  SEND_OPTIMISTIC: 'chat_client_send_optimistic',
68
- /** A send was acked by the server (seq assigned). */
69
18
  SEND_ACK: 'chat_client_send_ack',
70
- /** A correlated `send-rejected` (or its legacy uncorrelated form) was handled. */
71
19
  SEND_REJECTED: 'chat_client_send_rejected',
72
- /** A send was re-emitted (reconnect resume / scheduled hint / manual retry). */
73
20
  SEND_RETRY: 'chat_client_send_retry',
74
- /** A send reached its visible failed state, with the reason. */
75
21
  SEND_FAILED: 'chat_client_send_failed',
76
- /** An uncorrelated server `error` frame code. */
77
22
  ERROR_FRAME: 'chat_client_error_frame',
78
- // ---- inbox socket (one per USER, separate from the per-thread channel socket) ----
79
- /** An inbox socket open is being attempted (per attempt, with the reconnect cause). */
80
23
  INBOX_CONNECT_ATTEMPT: 'chat_client_inbox_connect_attempt',
81
- /** The inbox grant mint failed before a socket existed (terminal vs. retryable). */
82
24
  INBOX_GRANT_FAILED: 'chat_client_inbox_grant_failed',
83
- /** The inbox socket reached OPEN (the point `resume` is sent from). */
84
25
  INBOX_SOCKET_OPEN: 'chat_client_inbox_socket_open',
85
- /** The inbox socket closed, with the code and what the client decided to do next. */
86
26
  INBOX_SOCKET_CLOSE: 'chat_client_inbox_socket_close',
87
- /** An inbox reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */
88
27
  INBOX_RECONNECT_SCHEDULED: 'chat_client_inbox_reconnect_scheduled',
89
- /** The cursorless inbox `resume` was sent (the DO answers with a full snapshot). */
90
28
  INBOX_RESUME_REQUEST: 'chat_client_inbox_resume_request',
91
- /** An authoritative inbox snapshot was applied — registry SIZES only, never refs. */
92
29
  INBOX_SNAPSHOT_APPLIED: 'chat_client_inbox_snapshot_applied',
93
- /** The unread projection actually CHANGED (dock dot and/or per-row dot count). */
94
30
  INBOX_UNREAD_UPDATED: 'chat_client_inbox_unread_updated',
95
- /** An inbox frame was NOT applied, with the reason. */
96
31
  INBOX_FRAME_DROPPED: 'chat_client_inbox_frame_dropped',
97
- /** A mark-read was written to the socket (no optimistic local clear). */
98
32
  INBOX_MARK_READ: 'chat_client_inbox_mark_read',
99
33
  };
100
- /** Max length of a server-supplied label (frame type, error code) we echo into a log line. */
101
34
  const MAX_LABEL_LEN = 48;
102
- /**
103
- * Clamp a server-supplied label before it enters a log line. Frame types and
104
- * error codes are protocol tokens, not user content, but they arrive from the
105
- * wire — bound them so a hostile/buggy peer cannot emit an unbounded line.
106
- */
107
35
  export function safeDiagnosticLabel(raw) {
108
36
  if (typeof raw !== 'string')
109
37
  return 'unknown';
110
38
  const trimmed = raw.slice(0, MAX_LABEL_LEN);
111
39
  return trimmed.length > 0 ? trimmed : 'unknown';
112
40
  }
113
- /**
114
- * The default sink: ONE `console.debug` line per decision, shaped
115
- * `chat_client_<event> {"compact":"json"}` so a browser console capture or a log
116
- * query can filter on the stable prefix and parse the payload without a
117
- * multi-line join.
118
- */
119
41
  const consoleDebugSink = (event, data) => {
120
42
  if (typeof console === 'undefined' || typeof console.debug !== 'function')
121
43
  return;
122
44
  console.debug(`${event} ${JSON.stringify(data)}`);
123
45
  };
124
- /**
125
- * Build the emitter for a `diagnostics` config value. Returns `null` when
126
- * diagnostics are off so call sites can use `this.diag?.(...)` — with optional
127
- * call syntax the argument object is not constructed at all on the off path.
128
- */
129
46
  export function createDiagnosticsEmitter(option) {
130
47
  if (option == null || option === false)
131
48
  return null;
132
49
  const sink = option === true ? consoleDebugSink : option;
133
50
  return (event, data) => {
134
- // Diagnostics observe; they must never alter transport behavior — a sink that
135
- // throws (or a payload that cannot be serialized) is swallowed here.
51
+ // RULE-STD-ENG-009 exception: diagnostics must never fail the client, so a throwing sink is
52
+ // swallowed.
136
53
  try {
137
54
  sink(event, data);
138
55
  }
139
- catch {
140
- /* diagnostics are never allowed to fail the client */
141
- }
56
+ catch { }
142
57
  };
143
58
  }
144
59
  //# sourceMappingURL=diagnostics.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"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","sourcesContent":["// OPT-IN structured client diagnostics for the realtime chat transport.\n//\n// The E2E/browser harness can already capture the raw WebSocket frames; what it\n// cannot see is what the CLIENT DECIDED about them — which frame it applied,\n// which it dropped and why, what resume cursor it sent, and whether a send was\n// reconciled or failed. This module is that missing half: a bounded, structured,\n// opt-in decision log.\n//\n// The connection-time, resume, presence, and socket-timing instruments are the\n// baseline-campaign metric families — they stay. Only the attachment-serving\n// instruments were retired with the chat-attachment architecture.\n//\n// Rules this module exists to enforce:\n// - OFF BY DEFAULT and zero behavior change. When the option is absent the\n// factory returns `null`, every call site is `this.diag?.(...)`, and JS\n// optional-call short-circuiting means the payload object is never even\n// constructed. The cost when off is one nullish check.\n// - NEVER user content. Ids, seqs, states, counts, codes, booleans only —\n// never message text, captions, filenames, URLs, tokens, or previews. The\n// INBOX events go one step further and log SIZES only: a `channelRef` names\n// one specific conversation, so registry/unread lines carry counts and\n// deltas, never the refs themselves.\n// - STABLE event names (`chat_client_*`) suitable for log queries and later\n// alert metrics; one line per DECISION, never per render or per heartbeat.\n// - A diagnostics sink can never break the client: every emit is wrapped, so a\n// throwing sink is swallowed rather than turned into a transport failure.\n\n/**\n * A structured diagnostic sink. `event` is one of the stable\n * {@link CHAT_CLIENT_DIAGNOSTIC_EVENTS} names; `data` is a compact, content-free\n * payload (ids/seqs/states/counts/codes only). Supplying a sink lets the app\n * route entries into its own logger; `true` uses the console.debug default.\n */\nexport type ChatClientDiagnosticsSink = (event: string, data: Record<string, unknown>) => void;\n\n/**\n * The additive `diagnostics` config value: `false`/absent = off (the default and\n * the only production posture), `true` = the single-line `console.debug` sink,\n * or a custom {@link ChatClientDiagnosticsSink}.\n */\nexport type ChatClientDiagnosticsOption = boolean | ChatClientDiagnosticsSink;\n\n/** The bound emitter, or `null` when diagnostics are off (the zero-overhead path). */\nexport type ChatClientDiagnosticsEmitter = ChatClientDiagnosticsSink | null;\n\n/**\n * Stable event names. These are a QUERY CONTRACT — Cloud Logging filters, E2E\n * console greps, and any later alert metric key on them, so a name is added or\n * retired deliberately, never renamed in place.\n */\nexport const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {\n /** A socket open is being attempted (per attempt, with the reconnect cause). */\n CONNECT_ATTEMPT: 'chat_client_connect_attempt',\n /** The grant mint failed before a socket existed (terminal vs. retryable). */\n GRANT_FAILED: 'chat_client_grant_failed',\n /** The socket reached OPEN (the point the resume cursor is sent from). */\n SOCKET_OPEN: 'chat_client_socket_open',\n /** The socket closed, with the code and what the client decided to do next. */\n SOCKET_CLOSE: 'chat_client_socket_close',\n /**\n * CHANNEL ONLY. The dead-socket watchdog closed a socket that had gone silent\n * past the client's inbound-silence threshold (the channel DO auto-acks every\n * heartbeat, so silence there is provable death). One line per detection —\n * never per heartbeat — followed by the ordinary socket_close/reconnect lines.\n */\n SOCKET_LIVENESS_TIMEOUT: 'chat_client_socket_liveness_timeout',\n /** A reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',\n /** The resume cursor sent to the DO. */\n RESUME_REQUEST: 'chat_client_resume_request',\n /** The DO's snapshot answer + how the cursor moved because of it. */\n RESUME_RESULT: 'chat_client_resume_result',\n /** A `resync: true` snapshot dropped the local tail (removal, not upsert). */\n RESYNC_DROPPED_TAIL: 'chat_client_resync_dropped_tail',\n /** A frame was applied to UI state (insert / replace-by-seq / optimistic reconcile). */\n FRAME_APPLIED: 'chat_client_frame_applied',\n /** A frame was NOT applied, with the reason. */\n FRAME_DROPPED: 'chat_client_frame_dropped',\n /** A moderation revision overlay was recorded (and whether a loaded row re-rendered). */\n REVISION_APPLIED: 'chat_client_revision_applied',\n /** A history page was requested (cursor + limit). */\n HISTORY_REQUEST: 'chat_client_history_request',\n /** A history page was applied: size, seq range, cursor advance. */\n HISTORY_PAGE: 'chat_client_history_page',\n /** An optimistic send was rendered and its frame written. */\n SEND_OPTIMISTIC: 'chat_client_send_optimistic',\n /** A send was acked by the server (seq assigned). */\n SEND_ACK: 'chat_client_send_ack',\n /** A correlated `send-rejected` (or its legacy uncorrelated form) was handled. */\n SEND_REJECTED: 'chat_client_send_rejected',\n /** A send was re-emitted (reconnect resume / scheduled hint / manual retry). */\n SEND_RETRY: 'chat_client_send_retry',\n /** A send reached its visible failed state, with the reason. */\n SEND_FAILED: 'chat_client_send_failed',\n /** An uncorrelated server `error` frame code. */\n ERROR_FRAME: 'chat_client_error_frame',\n\n // ---- inbox socket (one per USER, separate from the per-thread channel socket) ----\n\n /** An inbox socket open is being attempted (per attempt, with the reconnect cause). */\n INBOX_CONNECT_ATTEMPT: 'chat_client_inbox_connect_attempt',\n /** The inbox grant mint failed before a socket existed (terminal vs. retryable). */\n INBOX_GRANT_FAILED: 'chat_client_inbox_grant_failed',\n /** The inbox socket reached OPEN (the point `resume` is sent from). */\n INBOX_SOCKET_OPEN: 'chat_client_inbox_socket_open',\n /** The inbox socket closed, with the code and what the client decided to do next. */\n INBOX_SOCKET_CLOSE: 'chat_client_inbox_socket_close',\n /** An inbox reconnect was scheduled (`delayMs`) or given up on (`delayMs: null`). */\n INBOX_RECONNECT_SCHEDULED: 'chat_client_inbox_reconnect_scheduled',\n /** The cursorless inbox `resume` was sent (the DO answers with a full snapshot). */\n INBOX_RESUME_REQUEST: 'chat_client_inbox_resume_request',\n /** An authoritative inbox snapshot was applied — registry SIZES only, never refs. */\n INBOX_SNAPSHOT_APPLIED: 'chat_client_inbox_snapshot_applied',\n /** The unread projection actually CHANGED (dock dot and/or per-row dot count). */\n INBOX_UNREAD_UPDATED: 'chat_client_inbox_unread_updated',\n /** An inbox frame was NOT applied, with the reason. */\n INBOX_FRAME_DROPPED: 'chat_client_inbox_frame_dropped',\n /** A mark-read was written to the socket (no optimistic local clear). */\n INBOX_MARK_READ: 'chat_client_inbox_mark_read',\n} as const;\n\nexport type ChatClientDiagnosticEventName =\n (typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS)[keyof typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS];\n\n/** Max length of a server-supplied label (frame type, error code) we echo into a log line. */\nconst MAX_LABEL_LEN = 48;\n\n/**\n * Clamp a server-supplied label before it enters a log line. Frame types and\n * error codes are protocol tokens, not user content, but they arrive from the\n * wire — bound them so a hostile/buggy peer cannot emit an unbounded line.\n */\nexport function safeDiagnosticLabel(raw: unknown): string {\n if (typeof raw !== 'string') return 'unknown';\n const trimmed = raw.slice(0, MAX_LABEL_LEN);\n return trimmed.length > 0 ? trimmed : 'unknown';\n}\n\n/**\n * The default sink: ONE `console.debug` line per decision, shaped\n * `chat_client_<event> {\"compact\":\"json\"}` so a browser console capture or a log\n * query can filter on the stable prefix and parse the payload without a\n * multi-line join.\n */\nconst consoleDebugSink: ChatClientDiagnosticsSink = (event, data) => {\n if (typeof console === 'undefined' || typeof console.debug !== 'function') return;\n console.debug(`${event} ${JSON.stringify(data)}`);\n};\n\n/**\n * Build the emitter for a `diagnostics` config value. Returns `null` when\n * diagnostics are off so call sites can use `this.diag?.(...)` — with optional\n * call syntax the argument object is not constructed at all on the off path.\n */\nexport function createDiagnosticsEmitter(\n option: ChatClientDiagnosticsOption | undefined,\n): ChatClientDiagnosticsEmitter {\n if (option == null || option === false) return null;\n const sink = option === true ? consoleDebugSink : option;\n return (event, data) => {\n // Diagnostics observe; they must never alter transport behavior — a sink that\n // throws (or a payload that cannot be serialized) is swallowed here.\n try {\n sink(event, data);\n } catch {\n /* diagnostics are never allowed to fail the client */\n }\n };\n}\n"]}
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/realtime/diagnostics.ts"],"names":[],"mappings":"AAMA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,eAAe,EAAE,6BAA6B;IAC9C,YAAY,EAAE,0BAA0B;IACxC,WAAW,EAAE,yBAAyB;IACtC,YAAY,EAAE,0BAA0B;IACxC,uBAAuB,EAAE,qCAAqC;IAC9D,mBAAmB,EAAE,iCAAiC;IACtD,cAAc,EAAE,4BAA4B;IAC5C,aAAa,EAAE,2BAA2B;IAC1C,mBAAmB,EAAE,iCAAiC;IACtD,aAAa,EAAE,2BAA2B;IAC1C,aAAa,EAAE,2BAA2B;IAC1C,gBAAgB,EAAE,8BAA8B;IAChD,eAAe,EAAE,6BAA6B;IAC9C,YAAY,EAAE,0BAA0B;IACxC,eAAe,EAAE,6BAA6B;IAC9C,QAAQ,EAAE,sBAAsB;IAChC,aAAa,EAAE,2BAA2B;IAC1C,UAAU,EAAE,wBAAwB;IACpC,WAAW,EAAE,yBAAyB;IACtC,WAAW,EAAE,yBAAyB;IAEtC,qBAAqB,EAAE,mCAAmC;IAC1D,kBAAkB,EAAE,gCAAgC;IACpD,iBAAiB,EAAE,+BAA+B;IAClD,kBAAkB,EAAE,gCAAgC;IACpD,yBAAyB,EAAE,uCAAuC;IAClE,oBAAoB,EAAE,kCAAkC;IACxD,sBAAsB,EAAE,oCAAoC;IAC5D,oBAAoB,EAAE,kCAAkC;IACxD,mBAAmB,EAAE,iCAAiC;IACtD,eAAe,EAAE,6BAA6B;CACtC,CAAC;AAKX,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,MAAM,gBAAgB,GAA8B,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IAClE,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,OAAO,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO;IAClF,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC;AAEF,MAAM,UAAU,wBAAwB,CACtC,MAA+C;IAE/C,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpD,MAAM,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC;IACzD,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACrB,4FAA4F;QAC5F,aAAa;QACb,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC,CAAC;AACJ,CAAC","sourcesContent":["export type ChatClientDiagnosticsSink = (event: string, data: Record<string, unknown>) => void;\n\nexport type ChatClientDiagnosticsOption = boolean | ChatClientDiagnosticsSink;\n\nexport type ChatClientDiagnosticsEmitter = ChatClientDiagnosticsSink | null;\n\n// Log queries key on these names: add or retire one, never rename it in place.\nexport const CHAT_CLIENT_DIAGNOSTIC_EVENTS = {\n CONNECT_ATTEMPT: 'chat_client_connect_attempt',\n GRANT_FAILED: 'chat_client_grant_failed',\n SOCKET_OPEN: 'chat_client_socket_open',\n SOCKET_CLOSE: 'chat_client_socket_close',\n SOCKET_LIVENESS_TIMEOUT: 'chat_client_socket_liveness_timeout',\n RECONNECT_SCHEDULED: 'chat_client_reconnect_scheduled',\n RESUME_REQUEST: 'chat_client_resume_request',\n RESUME_RESULT: 'chat_client_resume_result',\n RESYNC_DROPPED_TAIL: 'chat_client_resync_dropped_tail',\n FRAME_APPLIED: 'chat_client_frame_applied',\n FRAME_DROPPED: 'chat_client_frame_dropped',\n REVISION_APPLIED: 'chat_client_revision_applied',\n HISTORY_REQUEST: 'chat_client_history_request',\n HISTORY_PAGE: 'chat_client_history_page',\n SEND_OPTIMISTIC: 'chat_client_send_optimistic',\n SEND_ACK: 'chat_client_send_ack',\n SEND_REJECTED: 'chat_client_send_rejected',\n SEND_RETRY: 'chat_client_send_retry',\n SEND_FAILED: 'chat_client_send_failed',\n ERROR_FRAME: 'chat_client_error_frame',\n\n INBOX_CONNECT_ATTEMPT: 'chat_client_inbox_connect_attempt',\n INBOX_GRANT_FAILED: 'chat_client_inbox_grant_failed',\n INBOX_SOCKET_OPEN: 'chat_client_inbox_socket_open',\n INBOX_SOCKET_CLOSE: 'chat_client_inbox_socket_close',\n INBOX_RECONNECT_SCHEDULED: 'chat_client_inbox_reconnect_scheduled',\n INBOX_RESUME_REQUEST: 'chat_client_inbox_resume_request',\n INBOX_SNAPSHOT_APPLIED: 'chat_client_inbox_snapshot_applied',\n INBOX_UNREAD_UPDATED: 'chat_client_inbox_unread_updated',\n INBOX_FRAME_DROPPED: 'chat_client_inbox_frame_dropped',\n INBOX_MARK_READ: 'chat_client_inbox_mark_read',\n} as const;\n\nexport type ChatClientDiagnosticEventName =\n (typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS)[keyof typeof CHAT_CLIENT_DIAGNOSTIC_EVENTS];\n\nconst MAX_LABEL_LEN = 48;\n\nexport function safeDiagnosticLabel(raw: unknown): string {\n if (typeof raw !== 'string') return 'unknown';\n const trimmed = raw.slice(0, MAX_LABEL_LEN);\n return trimmed.length > 0 ? trimmed : 'unknown';\n}\n\nconst consoleDebugSink: ChatClientDiagnosticsSink = (event, data) => {\n if (typeof console === 'undefined' || typeof console.debug !== 'function') return;\n console.debug(`${event} ${JSON.stringify(data)}`);\n};\n\nexport function createDiagnosticsEmitter(\n option: ChatClientDiagnosticsOption | undefined,\n): ChatClientDiagnosticsEmitter {\n if (option == null || option === false) return null;\n const sink = option === true ? consoleDebugSink : option;\n return (event, data) => {\n // RULE-STD-ENG-009 exception: diagnostics must never fail the client, so a throwing sink is\n // swallowed.\n try {\n sink(event, data);\n } catch {}\n };\n}\n"]}
@@ -5,39 +5,13 @@ import type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js
5
5
  import { type ChatClientDiagnosticsOption } from './diagnostics.js';
6
6
  export interface InboxClientState {
7
7
  status: RealtimeStatus;
8
- /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */
9
8
  registry: WireRegistryEntry[];
10
- /** True if ANY active channel/invite has unread (drives the dock dot). */
11
9
  hasUnread: boolean;
12
- /** The set of channelRefs that currently carry an unread dot (per-row dots). */
13
10
  unreadChannelRefs: string[];
14
- /**
15
- * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):
16
- * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`
17
- * (a genuine, terminal policy "no" — e.g. a banned/suspended account), and
18
- * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient
19
- * reconnects. The app subscribes to inbox state directly (there is no inbox React
20
- * hook in this package), so this stable code is the surface — it invents no new UI.
21
- */
22
11
  lastErrorCode: string | null;
23
- /**
24
- * True once the first authoritative snapshot has been applied — the registry and dots
25
- * are then the inbox runtime's answer, so an empty registry means "nothing here"
26
- * rather than "not loaded yet". Once true it stays true.
27
- */
28
12
  hasLoadedInitialData: boolean;
29
- /**
30
- * True once the first open has failed for long enough to say so — the same rule as
31
- * `ChannelClientState.initialLoadFailed`. Cleared by the first snapshot or `retryNow()`.
32
- */
33
13
  initialLoadFailed: boolean;
34
14
  }
35
- /**
36
- * How one `markRead` ended: `ok` — the inbox runtime advanced the read cursor (its
37
- * fresh snapshot clears the dot); otherwise `code` says why not — the runtime's own
38
- * reason, `not-sent` (no open socket, nothing was sent), or `connection-lost` (the
39
- * socket closed before the answer arrived, so the outcome is unknown and the dot stays).
40
- */
41
15
  export type ChatMarkReadOutcome = {
42
16
  ok: true;
43
17
  } | {
@@ -46,9 +20,7 @@ export type ChatMarkReadOutcome = {
46
20
  };
47
21
  export interface InboxClientConfig {
48
22
  endpoint: string;
49
- /** The authenticated uid (the inbox owner; the DO serves no one else's data). */
50
23
  currentUserId: string;
51
- /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */
52
24
  grantProvider: GrantProvider;
53
25
  socketFactory: SocketFactory;
54
26
  timers?: TransportTimers;
@@ -58,15 +30,6 @@ export interface InboxClientConfig {
58
30
  maxAttempts?: number | null;
59
31
  random?: () => number;
60
32
  };
61
- /**
62
- * OPT-IN structured client diagnostics — the SAME option type and emitter the
63
- * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or
64
- * `false` changes nothing and costs one nullish check. `true` emits one
65
- * single-line `console.debug` entry per decision under the stable
66
- * `chat_client_inbox_*` names; a function routes them to the app's logger.
67
- * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a
68
- * `channelRef` (which names one specific conversation).
69
- */
70
33
  diagnostics?: ChatClientDiagnosticsOption;
71
34
  }
72
35
  export declare class InboxClient {
@@ -79,67 +42,33 @@ export declare class InboxClient {
79
42
  private reauthAttempted;
80
43
  private reconnectTimer;
81
44
  private closedByUs;
82
- /**
83
- * The current lifecycle's number, advanced by every `connect()` and `close()`; a grant,
84
- * timer, or socket callback of an older lifecycle is ignored (FRONTEND-108).
85
- */
86
45
  private lifecycle;
87
46
  private readonly initialLoad;
88
- /** Mark-reads sent on the current socket, awaiting their correlated result, by requestId. */
89
47
  private readonly pendingMarkReads;
90
- /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */
91
48
  private readonly diag;
92
- /** Monotonic socket-open attempt counter (diagnostics correlation only). */
93
49
  private connectAttempt;
94
- /** Why the CURRENT connect attempt is happening (diagnostics only). */
95
50
  private reconnectCause;
96
- /** The last close code observed, carried into the next attempt's cause line. */
97
51
  private lastCloseCode;
98
52
  constructor(config: InboxClientConfig);
99
53
  getState(): InboxClientState;
100
54
  subscribe(fn: (s: InboxClientState) => void): () => void;
101
55
  private setState;
102
56
  connect(): Promise<void>;
103
- /**
104
- * Retry now (the first-open failure's Retry) — the same rule as `ChannelClient.retryNow`:
105
- * a reconnect waiting out its backoff opens at once; a stopped lifecycle starts again; an
106
- * attempt in flight is left to finish; the first-open failure clears either way.
107
- */
108
57
  retryNow(): void;
109
58
  private startLifecycle;
110
59
  private reportInitialLoadFailed;
111
60
  private openSocket;
112
61
  private onOpen;
113
62
  private onClose;
114
- /**
115
- * Terminal access denial surfaced by the inbox grant provider
116
- * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark
117
- * closed-by-us, close the reconnect controller, and surface the stable
118
- * `access-denied` code. The inbox client tracks NO optimistic/pending sends
119
- * (mark-read is fire-and-forget with no local pending state), so — unlike
120
- * ChannelClient — there is nothing to fail here; only the lifecycle stops.
121
- */
122
63
  private denyAccessTerminally;
123
64
  private scheduleReconnect;
124
65
  private onMessage;
125
66
  private applySnapshot;
126
67
  private sendFrame;
127
- /** True if a specific channel/invite currently carries an unread dot. */
128
68
  channelHasUnread(channelRef: string): boolean;
129
- /**
130
- * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read
131
- * controls). Sends `mark-read` with a fresh `requestId`; the inbox runtime advances the
132
- * member's read cursor, pushes a fresh authoritative snapshot (which is what removes
133
- * the dot — there is NO optimistic local clear), and answers with a
134
- * `mark-read-result` naming the same request. The promise settles on that answer —
135
- * never on a timer — or with `not-sent` when no socket is open, or `connection-lost`
136
- * when the socket closes first.
137
- */
138
69
  markRead(channelRef: string): Promise<ChatMarkReadOutcome>;
139
- /** Settle the mark-read the result names; a result naming no pending request is ignored. */
140
70
  private applyMarkReadResult;
141
71
  private settlePendingMarkReads;
142
- /** Permanently close (auth-user switch / unmount). Idempotent. */
143
72
  close(): void;
144
73
  }
145
74
  //# sourceMappingURL=inbox-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,3 @@
1
- // The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's
2
- // inbox DO. It maintains the channel/invite registry + unread projection that
3
- // drives the inbox view — DOTS ONLY, no counts (Contract C / "Unread + inbox").
4
- //
5
- // The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on
6
- // `resume`, and on every live delta (a projection apply). This client just
7
- // mirrors the latest snapshot into an observable store + a per-channel unread set.
8
- // Same connection lifecycle as the channel client (subprotocol auth, reconnect,
9
- // 4401 re-grant once, 4403 stop, auth-switch teardown).
10
1
  import { createReconnectController, } from '@ttt-productions/realtime-core';
11
2
  import { ChatMarkReadResultPayloadSchema, SERVER_KINDS, CLIENT_KINDS, CHAT_CLOSE_CODES, } from '@ttt-productions/chat-schemas';
12
3
  import { buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
@@ -32,21 +23,12 @@ export class InboxClient {
32
23
  reauthAttempted = false;
33
24
  reconnectTimer = null;
34
25
  closedByUs = false;
35
- /**
36
- * The current lifecycle's number, advanced by every `connect()` and `close()`; a grant,
37
- * timer, or socket callback of an older lifecycle is ignored (FRONTEND-108).
38
- */
39
26
  lifecycle = 0;
40
27
  initialLoad;
41
- /** Mark-reads sent on the current socket, awaiting their correlated result, by requestId. */
42
28
  pendingMarkReads = new Map();
43
- /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */
44
29
  diag;
45
- /** Monotonic socket-open attempt counter (diagnostics correlation only). */
46
30
  connectAttempt = 0;
47
- /** Why the CURRENT connect attempt is happening (diagnostics only). */
48
31
  reconnectCause = 'initial';
49
- /** The last close code observed, carried into the next attempt's cause line. */
50
32
  lastCloseCode = null;
51
33
  constructor(config) {
52
34
  this.config = config;
@@ -70,11 +52,6 @@ export class InboxClient {
70
52
  async connect() {
71
53
  return this.startLifecycle(true);
72
54
  }
73
- /**
74
- * Retry now (the first-open failure's Retry) — the same rule as `ChannelClient.retryNow`:
75
- * a reconnect waiting out its backoff opens at once; a stopped lifecycle starts again; an
76
- * attempt in flight is left to finish; the first-open failure clears either way.
77
- */
78
55
  retryNow() {
79
56
  if (!this.state.hasLoadedInitialData)
80
57
  this.initialLoad.manualRetry();
@@ -96,25 +73,9 @@ export class InboxClient {
96
73
  void this.openSocket();
97
74
  }
98
75
  async startLifecycle(freshBudget) {
99
- // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle
100
- // from a fresh (idle) or fully torn-down (closed) state. A second connect()
101
- // while one is already connecting / open / reconnecting is a no-op, so a
102
- // double-mounted dock cannot open a SECOND inbox socket against the per-uid
103
- // cap. The reconnect path uses openSocket() / scheduleReconnect() directly
104
- // (never connect()), so this never blocks a legitimate reconnect.
105
76
  if (this.state.status !== 'idle' && this.state.status !== 'closed')
106
77
  return;
107
- // REVIVE the controller. `controller.close()` is PERMANENT: while it is in
108
- // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`
109
- // returns null — so without this reset a close() → connect() client opens a
110
- // socket and works normally until its FIRST transient close, then parks
111
- // terminally in `closed` with reconnect disabled and no surfaced error code.
112
78
  this.controller.reset();
113
- // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN
114
- // its lifecycle; an explicit connect() is a fresh attempt the authority may
115
- // legitimately re-deny, so the previous lifecycle's code must not persist.
116
- // The registry/unread projection is deliberately KEPT — the DO's next snapshot
117
- // is authoritative, and blanking the dock on every restart is pure UI churn.
118
79
  if (isTerminalErrorCode(this.state.lastErrorCode))
119
80
  this.setState({ lastErrorCode: null });
120
81
  this.closedByUs = false;
@@ -147,11 +108,6 @@ export class InboxClient {
147
108
  catch (err) {
148
109
  if (lifecycle !== this.lifecycle)
149
110
  return;
150
- // A TERMINAL access denial (the app translated an authoritative policy "no"
151
- // — e.g. Firebase `permission-denied` for a banned/suspended account — into the
152
- // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would
153
- // just re-deny, reconnect-looping forever and warning every cycle. Every other
154
- // mint failure (network/transient) stays retryable and backs off as before.
155
111
  const terminal = isChatAccessDeniedError(err);
156
112
  this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });
157
113
  if (terminal)
@@ -161,12 +117,9 @@ export class InboxClient {
161
117
  this.initialLoad.noteFailedAttempt();
162
118
  return this.scheduleReconnect();
163
119
  }
164
- // A grant minted for an earlier lifecycle must never open a socket in this one.
165
120
  if (lifecycle !== this.lifecycle || this.closedByUs)
166
121
  return;
167
122
  const url = `${this.config.endpoint.replace(/\/$/, '')}/inbox`;
168
- // Every callback is fenced to THIS socket: once it has been replaced or torn down, its
169
- // late open, frames, and close are ignored.
170
123
  let socket = null;
171
124
  const isCurrent = () => socket !== null && this.socket === socket;
172
125
  socket = this.config.socketFactory({
@@ -200,10 +153,7 @@ export class InboxClient {
200
153
  registryCount: this.state.registry.length,
201
154
  hasUnread: this.state.hasUnread,
202
155
  });
203
- // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.
204
156
  const sent = this.sendFrame(CLIENT_KINDS.RESUME, {});
205
- // The inbox resume is CURSORLESS by contract (the DO always answers with a full
206
- // authoritative snapshot) — there is no `afterSeq` half to record here.
207
157
  this.diag?.(DIAG.INBOX_RESUME_REQUEST, {
208
158
  attempt: this.connectAttempt,
209
159
  cursorless: true,
@@ -213,7 +163,6 @@ export class InboxClient {
213
163
  onClose(code, _reason) {
214
164
  this.socket = null;
215
165
  this.lastCloseCode = code;
216
- // A mark-read sent on this socket can no longer be answered.
217
166
  this.settlePendingMarkReads({ ok: false, code: 'connection-lost' });
218
167
  if (this.closedByUs) {
219
168
  this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });
@@ -242,14 +191,6 @@ export class InboxClient {
242
191
  this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });
243
192
  this.scheduleReconnect();
244
193
  }
245
- /**
246
- * Terminal access denial surfaced by the inbox grant provider
247
- * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark
248
- * closed-by-us, close the reconnect controller, and surface the stable
249
- * `access-denied` code. The inbox client tracks NO optimistic/pending sends
250
- * (mark-read is fire-and-forget with no local pending state), so — unlike
251
- * ChannelClient — there is nothing to fail here; only the lifecycle stops.
252
- */
253
194
  denyAccessTerminally() {
254
195
  this.closedByUs = true;
255
196
  this.controller.close();
@@ -285,9 +226,6 @@ export class InboxClient {
285
226
  }
286
227
  if (frame.type === SERVER_KINDS.MARK_READ_RESULT)
287
228
  return this.applyMarkReadResult(frame.payload);
288
- // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push
289
- // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both
290
- // instead of silently dropping the `unread` type (C-M2).
291
229
  if (frame.type !== 'snapshot' && frame.type !== 'unread') {
292
230
  this.diag?.(DIAG.INBOX_FRAME_DROPPED, {
293
231
  kind: safeDiagnosticLabel(frame.type),
@@ -316,8 +254,6 @@ export class InboxClient {
316
254
  }
317
255
  }
318
256
  else {
319
- // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —
320
- // deliberately inert, but never silently invisible under diagnostics.
321
257
  this.diag?.(DIAG.INBOX_FRAME_DROPPED, {
322
258
  kind: safeDiagnosticLabel(frame.type),
323
259
  reason: 'not-an-inbox-payload',
@@ -325,13 +261,7 @@ export class InboxClient {
325
261
  }
326
262
  }
327
263
  applySnapshot(snap, source) {
328
- // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can
329
- // render both the active list and the Archived toggle. `archived` is a distinct
330
- // dimension from `tombstoned`; archived rows are still `state: 'active'`.
331
264
  const registry = snap.registry.filter((e) => e.state === 'active');
332
- // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from
333
- // the same projection but exclude ARCHIVED rows (archive = done — an archived row
334
- // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.
335
265
  const unreadChannelRefs = deriveUnreadRefs(snap);
336
266
  const hasUnread = Boolean(snap.hasUnread);
337
267
  const before = this.state;
@@ -346,12 +276,9 @@ export class InboxClient {
346
276
  });
347
277
  if (!this.diag)
348
278
  return;
349
- // SIZES ONLY — a channelRef names one specific conversation and never enters a
350
- // diagnostic line.
351
279
  this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {
352
280
  source: safeDiagnosticLabel(source),
353
281
  received: snap.registry.length,
354
- // `received - active` is the tombstoned count the client filtered out.
355
282
  active: registry.length,
356
283
  archived: registry.filter((e) => e.archived === true).length,
357
284
  hasUnread,
@@ -377,30 +304,17 @@ export class InboxClient {
377
304
  this.socket.send(buildFrame(type, payload));
378
305
  return true;
379
306
  }
380
- /** True if a specific channel/invite currently carries an unread dot. */
381
307
  channelHasUnread(channelRef) {
382
308
  return this.state.unreadChannelRefs.includes(channelRef);
383
309
  }
384
- /**
385
- * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read
386
- * controls). Sends `mark-read` with a fresh `requestId`; the inbox runtime advances the
387
- * member's read cursor, pushes a fresh authoritative snapshot (which is what removes
388
- * the dot — there is NO optimistic local clear), and answers with a
389
- * `mark-read-result` naming the same request. The promise settles on that answer —
390
- * never on a timer — or with `not-sent` when no socket is open, or `connection-lost`
391
- * when the socket closes first.
392
- */
393
310
  markRead(channelRef) {
394
311
  const requestId = newCorrelationId();
395
312
  const sent = this.sendFrame(CLIENT_KINDS.MARK_READ, { channelRef, requestId });
396
- // The ref itself is never logged — only that a mark-read was written and what the
397
- // unread projection looked like at that moment (the DO's snapshot is what clears it).
398
313
  this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });
399
314
  if (!sent)
400
315
  return Promise.resolve({ ok: false, code: 'not-sent' });
401
316
  return new Promise((resolve) => this.pendingMarkReads.set(requestId, resolve));
402
317
  }
403
- /** Settle the mark-read the result names; a result naming no pending request is ignored. */
404
318
  applyMarkReadResult(payload) {
405
319
  const parsed = ChatMarkReadResultPayloadSchema.safeParse(payload);
406
320
  if (!parsed.success) {
@@ -422,7 +336,6 @@ export class InboxClient {
422
336
  for (const resolve of pending)
423
337
  resolve(outcome);
424
338
  }
425
- /** Permanently close (auth-user switch / unmount). Idempotent. */
426
339
  close() {
427
340
  this.closedByUs = true;
428
341
  this.lifecycle += 1;
@@ -433,7 +346,6 @@ export class InboxClient {
433
346
  this.reconnectTimer = null;
434
347
  }
435
348
  if (this.socket) {
436
- // Detach first: the socket's own late close is not this lifecycle's event.
437
349
  const socket = this.socket;
438
350
  this.socket = null;
439
351
  socket.close(1000, 'client teardown');
@@ -442,16 +354,6 @@ export class InboxClient {
442
354
  this.setState({ status: 'closed' });
443
355
  }
444
356
  }
445
- /**
446
- * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot
447
- * carries a per-entry `unread` boolean on every active registry entry alongside
448
- * the global `hasUnread` roll-up that drives the dock dot. An entry without the
449
- * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot
450
- * still lights off `hasUnread`.
451
- *
452
- * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears
453
- * unread on archive; the client also never counts them).
454
- */
455
357
  function deriveUnreadRefs(snap) {
456
358
  const refs = [];
457
359
  for (const e of snap.registry) {