@ttt-productions/chat-react 0.13.15 → 0.13.17

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 (92) hide show
  1. package/README.md +3 -3
  2. package/dist/context/ChatNameResolverContext.d.ts +3 -26
  3. package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
  4. package/dist/context/ChatNameResolverContext.js +3 -19
  5. package/dist/context/ChatNameResolverContext.js.map +1 -1
  6. package/dist/firestore/queries.js +1 -1
  7. package/dist/firestore/queries.js.map +1 -1
  8. package/dist/hooks/useChatMessages.d.ts +3 -7
  9. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  10. package/dist/hooks/useChatMessages.js +20 -22
  11. package/dist/hooks/useChatMessages.js.map +1 -1
  12. package/dist/index.d.ts +32 -18
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +18 -14
  15. package/dist/index.js.map +1 -1
  16. package/dist/realtime/channel-client.d.ts +0 -248
  17. package/dist/realtime/channel-client.d.ts.map +1 -1
  18. package/dist/realtime/channel-client.js +102 -464
  19. package/dist/realtime/channel-client.js.map +1 -1
  20. package/dist/realtime/diagnostics.d.ts +0 -62
  21. package/dist/realtime/diagnostics.d.ts.map +1 -1
  22. package/dist/realtime/diagnostics.js +4 -89
  23. package/dist/realtime/diagnostics.js.map +1 -1
  24. package/dist/realtime/inbox-client.d.ts +0 -71
  25. package/dist/realtime/inbox-client.d.ts.map +1 -1
  26. package/dist/realtime/inbox-client.js +29 -109
  27. package/dist/realtime/inbox-client.js.map +1 -1
  28. package/dist/realtime/initial-load-budget.d.ts +1 -16
  29. package/dist/realtime/initial-load-budget.d.ts.map +1 -1
  30. package/dist/realtime/initial-load-budget.js +1 -20
  31. package/dist/realtime/initial-load-budget.js.map +1 -1
  32. package/dist/realtime/map.d.ts +0 -28
  33. package/dist/realtime/map.d.ts.map +1 -1
  34. package/dist/realtime/map.js +3 -37
  35. package/dist/realtime/map.js.map +1 -1
  36. package/dist/realtime/shared.d.ts +0 -79
  37. package/dist/realtime/shared.d.ts.map +1 -1
  38. package/dist/realtime/shared.js +2 -80
  39. package/dist/realtime/shared.js.map +1 -1
  40. package/dist/realtime/socket.d.ts +0 -15
  41. package/dist/realtime/socket.d.ts.map +1 -1
  42. package/dist/realtime/socket.js +1 -15
  43. package/dist/realtime/socket.js.map +1 -1
  44. package/dist/realtime/transport.d.ts +1 -25
  45. package/dist/realtime/transport.d.ts.map +1 -1
  46. package/dist/realtime/transport.js +1 -14
  47. package/dist/realtime/transport.js.map +1 -1
  48. package/dist/realtime/useRealtimeChatMessages.d.ts +4 -21
  49. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  50. package/dist/realtime/useRealtimeChatMessages.js +5 -27
  51. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  52. package/dist/realtime/wire.d.ts +0 -58
  53. package/dist/realtime/wire.d.ts.map +1 -1
  54. package/dist/realtime/wire.js +0 -22
  55. package/dist/realtime/wire.js.map +1 -1
  56. package/dist/types.d.ts +3 -41
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js +0 -3
  59. package/dist/types.js.map +1 -1
  60. package/dist/ui/ChatShell.d.ts +5 -47
  61. package/dist/ui/ChatShell.d.ts.map +1 -1
  62. package/dist/ui/ChatShell.js +39 -73
  63. package/dist/ui/ChatShell.js.map +1 -1
  64. package/dist/ui/Composer.d.ts +1 -14
  65. package/dist/ui/Composer.d.ts.map +1 -1
  66. package/dist/ui/Composer.js +12 -25
  67. package/dist/ui/Composer.js.map +1 -1
  68. package/dist/ui/MessageItemDefault.d.ts +2 -6
  69. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  70. package/dist/ui/MessageItemDefault.js +26 -56
  71. package/dist/ui/MessageItemDefault.js.map +1 -1
  72. package/dist/ui/MessageList.d.ts +3 -26
  73. package/dist/ui/MessageList.d.ts.map +1 -1
  74. package/dist/ui/MessageList.js +15 -28
  75. package/dist/ui/MessageList.js.map +1 -1
  76. package/dist/ui/MessageText.d.ts +0 -13
  77. package/dist/ui/MessageText.d.ts.map +1 -1
  78. package/dist/ui/MessageText.js.map +1 -1
  79. package/dist/ui/SenderName.d.ts +0 -5
  80. package/dist/ui/SenderName.d.ts.map +1 -1
  81. package/dist/ui/SenderName.js +3 -8
  82. package/dist/ui/SenderName.js.map +1 -1
  83. package/dist/ui/menus.d.ts +1 -1
  84. package/dist/ui/menus.d.ts.map +1 -1
  85. package/dist/ui/menus.js +5 -7
  86. package/dist/ui/menus.js.map +1 -1
  87. package/package.json +10 -10
  88. package/src/styles/chat.css +2 -15
  89. package/dist/realtime/index.d.ts +0 -19
  90. package/dist/realtime/index.d.ts.map +0 -1
  91. package/dist/realtime/index.js +0 -15
  92. package/dist/realtime/index.js.map +0 -1
@@ -1,37 +1,11 @@
1
- // Shared injected contracts + tunable constants for the realtime transport.
2
- //
3
- // The client-agreed limits (`HEARTBEAT_MS`, `TYPING_COALESCE_MS`,
4
- // `HISTORY_PAGE_MAX`) come from the one wire contract in
5
- // `@ttt-productions/chat-schemas`; they are re-exported here so the transport's
6
- // existing import sites are unchanged. The worker runtime is the authority and
7
- // rejects anything out of bounds, but the client uses these to coalesce and
8
- // paginate politely. The remaining constants below are client-only retry policy.
9
- /**
10
- * Package-owned, Firebase-FREE terminal access-denial signal. A grant provider
11
- * throws this (from `GrantProvider`) when the authoritative authorization answer
12
- * is a genuine, terminal "no" — as opposed to a transient/retryable mint failure.
13
- *
14
- * The transport treats it specially: instead of the normal reconnect backoff it
15
- * STOPS reconnecting, closes the lifecycle, fails any pending optimistic sends,
16
- * and surfaces the stable `access-denied` error code (which the React hook maps to
17
- * `allowed: false` so the shell renders its no-access surface instead of an eternal
18
- * loader). Every other thrown error stays transient and reconnects as before.
19
- *
20
- * This is deliberately generic: the consuming app translates its own backend
21
- * denial (e.g. a Firebase `functions/permission-denied`) into this class at the
22
- * grant-provider boundary. The package never imports or names Firebase. Consumers
23
- * that cannot subclass may instead throw any error carrying `isChatAccessDenied:
24
- * true` — `isChatAccessDeniedError` recognizes both (also cross-realm safe).
25
- */
26
1
  export class ChatAccessDeniedError extends Error {
27
- /** Duck-typed marker so detection survives module-realm / re-throw boundaries. */
2
+ // Duck-typed so detection survives module-realm and re-throw boundaries.
28
3
  isChatAccessDenied = true;
29
4
  constructor(message = 'Chat access denied') {
30
5
  super(message);
31
6
  this.name = 'ChatAccessDeniedError';
32
7
  }
33
8
  }
34
- /** True when `err` is a terminal chat access denial (class instance or duck-typed marker). */
35
9
  export function isChatAccessDeniedError(err) {
36
10
  return (err instanceof ChatAccessDeniedError ||
37
11
  (typeof err === 'object' &&
@@ -43,72 +17,20 @@ export const defaultTimers = {
43
17
  clearTimeout: (h) => clearTimeout(h),
44
18
  now: () => Date.now(),
45
19
  };
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
20
  export const TERMINAL_ERROR_CODE = {
59
- /** A 4403 REVOKED close: access was pulled while the socket was open. */
60
21
  REVOKED: 'revoked',
61
- /** The grant provider threw `ChatAccessDeniedError` — a terminal policy "no". */
62
22
  ACCESS_DENIED: 'access-denied',
63
23
  };
64
- /** True when `code` is one of the terminal lifecycle verdicts above. */
65
24
  export function isTerminalErrorCode(code) {
66
25
  return code === TERMINAL_ERROR_CODE.REVOKED || code === TERMINAL_ERROR_CODE.ACCESS_DENIED;
67
26
  }
68
- /**
69
- * How long a client keeps failing to open for the first time before it reports the
70
- * first open as failed (`initialLoadFailed`) — so a chat or inbox that cannot connect
71
- * shows a failure the user can retry instead of an opening state that never ends. The
72
- * client keeps retrying in the background after it reports. Client policy, not a wire
73
- * constant.
74
- */
75
27
  export const CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS = 15_000;
76
- // The client-agreed limits live in the wire contract; re-export to keep this
77
- // module's public surface stable.
78
- export { HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from '@ttt-productions/chat-schemas';
79
- /**
80
- * Max reconnect resends of one un-acked send before its optimistic row is flipped
81
- * to a visible failed state (retry affordance) instead of a permanent ghost. Each
82
- * reconnect that resumes with the pending send still un-acked counts one attempt.
83
- */
84
28
  export const MAX_PENDING_SEND_ATTEMPTS = 5;
85
- /**
86
- * Max wall-clock age of an un-acked pending send before it is flipped to failed on
87
- * the next resume, even if the attempt cap hasn't been hit (a long offline window).
88
- */
89
29
  export const PENDING_SEND_MAX_AGE_MS = 60_000;
90
- /**
91
- * Wall-clock budget (ms) for a SERVER-DECLARED retryable send. Once the Worker has
92
- * rejected a send as retryable — a correlated `send-rejected` frame OR the legacy
93
- * uncorrelated `error{code:'membership-pending'}` — that send keeps retrying on the
94
- * server's own hint until this budget elapses, and is NEVER failed by the generic
95
- * {@link MAX_PENDING_SEND_ATTEMPTS} cap (which governs only blind reconnect
96
- * resends). 90 s comfortably covers the backend projection/grant-bootstrap window
97
- * without stranding an optimistic bubble on "Sending…" the way the old ~10 s
98
- * five-attempt cap did.
99
- */
30
+ // Covers the backend's projection window, which the five-attempt cap (~10 s) does not.
100
31
  export const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;
101
- /**
102
- * Lower/upper clamp (ms) applied to a server `retryAfterMs` hint before scheduling a
103
- * resend, so a pathological (but schema-valid) hint can neither hammer the socket
104
- * nor park a send for minutes.
105
- */
106
32
  export const SEND_RETRY_MIN_DELAY_MS = 250;
107
33
  export const SEND_RETRY_MAX_DELAY_MS = 30_000;
108
- /**
109
- * A fresh client-minted id for a command the client correlates with its answer (a send's
110
- * `clientMessageId`, a mark-read's `requestId`). Random, so two tabs never collide.
111
- */
112
34
  export function newCorrelationId() {
113
35
  const c = globalThis.crypto;
114
36
  if (c?.randomUUID)
@@ -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;;;;;;;;;;;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;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,MAAM,CAAC;AAE1D,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;AAE9C;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,EAAE,UAAU;QAAE,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IACzC,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACpE,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/**\n * How long a client keeps failing to open for the first time before it reports the\n * first open as failed (`initialLoadFailed`) — so a chat or inbox that cannot connect\n * shows a failure the user can retry instead of an opening state that never ends. The\n * client keeps retrying in the background after it reports. Client policy, not a wire\n * constant.\n */\nexport const CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS = 15_000;\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\n/**\n * A fresh client-minted id for a command the client correlates with its answer (a send's\n * `clientMessageId`, a mark-read's `requestId`). Random, so two tabs never collide.\n */\nexport function newCorrelationId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,yEAAyE;IAChE,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,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;AAQD,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;AAIF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,OAAO,EAAE,SAAS;IAClB,aAAa,EAAE,eAAe;CACtB,CAAC;AAIX,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,OAAO,IAAI,KAAK,mBAAmB,CAAC,OAAO,IAAI,IAAI,KAAK,mBAAmB,CAAC,aAAa,CAAC;AAC5F,CAAC;AAED,MAAM,CAAC,MAAM,mCAAmC,GAAG,MAAM,CAAC;AAE1D,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,uFAAuF;AACvF,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAElD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,MAAM,UAAU,gBAAgB;IAC9B,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,EAAE,UAAU;QAAE,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IACzC,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["export type GrantProvider = () => Promise<string>;\n\nexport class ChatAccessDeniedError extends Error {\n // Duck-typed so detection survives module-realm and re-throw boundaries.\n readonly isChatAccessDenied = true;\n constructor(message = 'Chat access denied') {\n super(message);\n this.name = 'ChatAccessDeniedError';\n }\n}\n\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\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\nexport type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';\n\nexport const TERMINAL_ERROR_CODE = {\n REVOKED: 'revoked',\n ACCESS_DENIED: 'access-denied',\n} as const;\n\nexport type TerminalErrorCode = (typeof TERMINAL_ERROR_CODE)[keyof typeof TERMINAL_ERROR_CODE];\n\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\nexport const CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS = 15_000;\n\nexport const MAX_PENDING_SEND_ATTEMPTS = 5;\n\nexport const PENDING_SEND_MAX_AGE_MS = 60_000;\n\n// Covers the backend's projection window, which the five-attempt cap (~10 s) does not.\nexport const SERVER_RETRYABLE_MAX_AGE_MS = 90_000;\n\nexport const SEND_RETRY_MIN_DELAY_MS = 250;\nexport const SEND_RETRY_MAX_DELAY_MS = 30_000;\n\nexport function newCorrelationId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `cid-${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n"]}
@@ -1,33 +1,18 @@
1
- /** Lifecycle events a socket surfaces to its owning client. */
2
1
  export interface SocketHandlers {
3
2
  onOpen: () => void;
4
3
  onMessage: (data: string) => void;
5
- /** code/reason from the close frame (4401/4403/4408/4413/4429/1013 or transport codes). */
6
4
  onClose: (code: number, reason: string) => void;
7
5
  onError: () => void;
8
6
  }
9
- /** The transport-neutral socket the clients use. A mock implements the same shape in tests. */
10
7
  export interface RealtimeSocket {
11
- /** Queue/send a text frame. No-op if not open. */
12
8
  send(data: string): void;
13
- /** Close the socket with an optional app code + reason. Idempotent. */
14
9
  close(code?: number, reason?: string): void;
15
- /** True once the underlying transport is OPEN (ready to send). */
16
10
  readonly isOpen: boolean;
17
11
  }
18
- /**
19
- * A factory that opens a socket to `url`, offering the chat subprotocol + the
20
- * grant token, and wires the handlers. Injected into the transport so tests
21
- * substitute a mock. The default factory uses the global `WebSocket`.
22
- */
23
12
  export type SocketFactory = (args: {
24
13
  url: string;
25
14
  grantToken: string;
26
15
  handlers: SocketHandlers;
27
16
  }) => RealtimeSocket;
28
- /**
29
- * The default browser socket factory. Offers `[CHAT_SUBPROTOCOL, grantToken]` as the
30
- * subprotocol list so the grant rides in `Sec-WebSocket-Protocol`, never the URL.
31
- */
32
17
  export declare const browserSocketFactory: SocketFactory;
33
18
  //# sourceMappingURL=socket.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAaA,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2FAA2F;IAC3F,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uEAAuE;IACvE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;CAC1B,KAAK,cAAc,CAAC;AAiBrB;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAuBlC,CAAC"}
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;CAC1B,KAAK,cAAc,CAAC;AAiBrB,eAAO,MAAM,oBAAoB,EAAE,aAyBlC,CAAC"}
@@ -1,19 +1,5 @@
1
- // The minimal socket abstraction the realtime clients drive, plus the default
2
- // browser-WebSocket factory. Injectable so unit tests pass a MOCK socket and no
3
- // real network is touched.
4
- //
5
- // Contract A transport: the grant token is offered as the SECOND WebSocket
6
- // subprotocol — `Sec-WebSocket-Protocol: <CHAT_SUBPROTOCOL>, <grant-token>` —
7
- // never in the URL (tokens never appear in URLs; Contract A). The browser sets
8
- // that header from `new WebSocket(url, [CHAT_SUBPROTOCOL, grantToken])`. The
9
- // worker validates cookie + grant + Origin BEFORE accept and echoes back only
10
- // CHAT_SUBPROTOCOL.
11
- import { CHAT_SUBPROTOCOL } from './wire.js';
1
+ import { CHAT_SUBPROTOCOL } from '@ttt-productions/chat-schemas';
12
2
  const WS_OPEN = 1;
13
- /**
14
- * The default browser socket factory. Offers `[CHAT_SUBPROTOCOL, grantToken]` as the
15
- * subprotocol list so the grant rides in `Sec-WebSocket-Protocol`, never the URL.
16
- */
17
3
  export const browserSocketFactory = ({ url, grantToken, handlers }) => {
18
4
  const Ctor = globalThis.WebSocket;
19
5
  if (!Ctor)
@@ -1 +1 @@
1
- {"version":3,"file":"socket.js","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,2BAA2B;AAC3B,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,8EAA8E;AAC9E,oBAAoB;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AA6C7C,MAAM,OAAO,GAAG,CAAC,CAAC;AAElB;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE;IACnF,MAAM,IAAI,GAAI,UAA4C,CAAC,SAAS,CAAC;IACrE,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACpG,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC,CAAC;IACzD,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACpC,EAAE,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACnG,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IACtC,OAAO;QACL,IAAI,CAAC,IAAY;YACf,IAAI,EAAE,CAAC,UAAU,KAAK,OAAO;gBAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,IAAa,EAAE,MAAe;YAClC,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,4BAA4B;YAC9B,CAAC;QACH,CAAC;QACD,IAAI,MAAM;YACR,OAAO,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["// The minimal socket abstraction the realtime clients drive, plus the default\n// browser-WebSocket factory. Injectable so unit tests pass a MOCK socket and no\n// real network is touched.\n//\n// Contract A transport: the grant token is offered as the SECOND WebSocket\n// subprotocol — `Sec-WebSocket-Protocol: <CHAT_SUBPROTOCOL>, <grant-token>` —\n// never in the URL (tokens never appear in URLs; Contract A). The browser sets\n// that header from `new WebSocket(url, [CHAT_SUBPROTOCOL, grantToken])`. The\n// worker validates cookie + grant + Origin BEFORE accept and echoes back only\n// CHAT_SUBPROTOCOL.\n\nimport { CHAT_SUBPROTOCOL } from './wire.js';\n\n/** Lifecycle events a socket surfaces to its owning client. */\nexport interface SocketHandlers {\n onOpen: () => void;\n onMessage: (data: string) => void;\n /** code/reason from the close frame (4401/4403/4408/4413/4429/1013 or transport codes). */\n onClose: (code: number, reason: string) => void;\n onError: () => void;\n}\n\n/** The transport-neutral socket the clients use. A mock implements the same shape in tests. */\nexport interface RealtimeSocket {\n /** Queue/send a text frame. No-op if not open. */\n send(data: string): void;\n /** Close the socket with an optional app code + reason. Idempotent. */\n close(code?: number, reason?: string): void;\n /** True once the underlying transport is OPEN (ready to send). */\n readonly isOpen: boolean;\n}\n\n/**\n * A factory that opens a socket to `url`, offering the chat subprotocol + the\n * grant token, and wires the handlers. Injected into the transport so tests\n * substitute a mock. The default factory uses the global `WebSocket`.\n */\nexport type SocketFactory = (args: {\n url: string;\n grantToken: string;\n handlers: SocketHandlers;\n}) => RealtimeSocket;\n\n/** A WebSocket-like minimal surface (so we don't hard-depend on lib.dom in node-safe builds). */\ninterface MinimalWebSocket {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readyState: number;\n onopen: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n onclose: ((ev: { code: number; reason: string }) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n}\n\ntype WebSocketCtor = new (url: string, protocols?: string | string[]) => MinimalWebSocket;\n\nconst WS_OPEN = 1;\n\n/**\n * The default browser socket factory. Offers `[CHAT_SUBPROTOCOL, grantToken]` as the\n * subprotocol list so the grant rides in `Sec-WebSocket-Protocol`, never the URL.\n */\nexport const browserSocketFactory: SocketFactory = ({ url, grantToken, handlers }) => {\n const Ctor = (globalThis as { WebSocket?: WebSocketCtor }).WebSocket;\n if (!Ctor) throw new Error('[chat-realtime] no global WebSocket available; inject a SocketFactory');\n const ws = new Ctor(url, [CHAT_SUBPROTOCOL, grantToken]);\n ws.onopen = () => handlers.onOpen();\n ws.onmessage = (ev) => handlers.onMessage(typeof ev.data === 'string' ? ev.data : String(ev.data));\n ws.onclose = (ev) => handlers.onClose(ev.code, ev.reason);\n ws.onerror = () => handlers.onError();\n return {\n send(data: string) {\n if (ws.readyState === WS_OPEN) ws.send(data);\n },\n close(code?: number, reason?: string) {\n try {\n ws.close(code, reason);\n } catch {\n /* already closing/closed */\n }\n },\n get isOpen() {\n return ws.readyState === WS_OPEN;\n },\n };\n};\n"]}
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAkCjE,MAAM,OAAO,GAAG,CAAC,CAAC;AAElB,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE;IACnF,MAAM,IAAI,GAAI,UAA4C,CAAC,SAAS,CAAC;IACrE,IAAI,CAAC,IAAI;QACP,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC,CAAC;IACzD,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACpC,EAAE,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE,CACpB,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9E,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IACtC,OAAO;QACL,IAAI,CAAC,IAAY;YACf,IAAI,EAAE,CAAC,UAAU,KAAK,OAAO;gBAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,IAAa,EAAE,MAAe;YAClC,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,4BAA4B;YAC9B,CAAC;QACH,CAAC;QACD,IAAI,MAAM;YACR,OAAO,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { CHAT_SUBPROTOCOL } from '@ttt-productions/chat-schemas';\n\nexport interface SocketHandlers {\n onOpen: () => void;\n onMessage: (data: string) => void;\n onClose: (code: number, reason: string) => void;\n onError: () => void;\n}\n\nexport interface RealtimeSocket {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readonly isOpen: boolean;\n}\n\nexport type SocketFactory = (args: {\n url: string;\n grantToken: string;\n handlers: SocketHandlers;\n}) => RealtimeSocket;\n\n// Structural, so node-safe builds need no lib.dom.\ninterface MinimalWebSocket {\n send(data: string): void;\n close(code?: number, reason?: string): void;\n readyState: number;\n onopen: ((ev: unknown) => void) | null;\n onmessage: ((ev: { data: unknown }) => void) | null;\n onclose: ((ev: { code: number; reason: string }) => void) | null;\n onerror: ((ev: unknown) => void) | null;\n}\n\ntype WebSocketCtor = new (url: string, protocols?: string | string[]) => MinimalWebSocket;\n\nconst WS_OPEN = 1;\n\nexport const browserSocketFactory: SocketFactory = ({ url, grantToken, handlers }) => {\n const Ctor = (globalThis as { WebSocket?: WebSocketCtor }).WebSocket;\n if (!Ctor)\n throw new Error('[chat-realtime] no global WebSocket available; inject a SocketFactory');\n const ws = new Ctor(url, [CHAT_SUBPROTOCOL, grantToken]);\n ws.onopen = () => handlers.onOpen();\n ws.onmessage = (ev) =>\n handlers.onMessage(typeof ev.data === 'string' ? ev.data : String(ev.data));\n ws.onclose = (ev) => handlers.onClose(ev.code, ev.reason);\n ws.onerror = () => handlers.onError();\n return {\n send(data: string) {\n if (ws.readyState === WS_OPEN) ws.send(data);\n },\n close(code?: number, reason?: string) {\n try {\n ws.close(code, reason);\n } catch {\n /* already closing/closed */\n }\n },\n get isOpen() {\n return ws.readyState === WS_OPEN;\n },\n };\n};\n"]}
@@ -1,17 +1,11 @@
1
1
  import { ChannelClient, type ChannelClientConfig, type ChannelClientState } from './channel-client.js';
2
- import { InboxClient, type InboxClientConfig, type InboxClientState } from './inbox-client.js';
2
+ import { InboxClient, type InboxClientConfig } from './inbox-client.js';
3
3
  import { type SocketFactory } from './socket.js';
4
4
  import type { GrantProvider, TransportTimers } from './shared.js';
5
5
  import type { ChatClientDiagnosticsOption } from './diagnostics.js';
6
6
  import type { ChatConversationRef } from '@ttt-productions/chat-schemas';
7
- /**
8
- * Everything the channel transport needs. The app supplies `grantProvider`
9
- * (its `mintChatGrant` callable, channel scope) + `endpoint`; `socketFactory`
10
- * defaults to the real browser WebSocket and is overridden in tests.
11
- */
12
7
  export interface RealtimeTransportConfig {
13
8
  endpoint: string;
14
- /** The conversation the socket is for, as the app maps it onto the neutral reference. */
15
9
  channelRef: ChatConversationRef;
16
10
  threadId: string;
17
11
  currentUserId: string;
@@ -19,35 +13,19 @@ export interface RealtimeTransportConfig {
19
13
  socketFactory?: SocketFactory;
20
14
  timers?: TransportTimers;
21
15
  reconnect?: ChannelClientConfig['reconnect'];
22
- /**
23
- * OPT-IN structured client diagnostics, forwarded to the ChannelClient. Absent
24
- * or `false` (the default) changes nothing and costs one nullish check; `true`
25
- * emits one single-line `console.debug` entry per client DECISION with a stable
26
- * `chat_client_*` event name; a function routes those entries into the app's own
27
- * logger. Content-free by construction (ids/seqs/states/counts/codes only).
28
- */
29
16
  diagnostics?: ChatClientDiagnosticsOption;
30
17
  }
31
- /**
32
- * The opaque realtime client handle stored on `ChatRealtimeTransportConfig.client`.
33
- * The UI hook (`useRealtimeChatMessages`) drives it; the UI never reaches into the
34
- * socket directly.
35
- */
36
18
  export interface RealtimeChatClient {
37
19
  readonly channel: ChannelClient;
38
20
  readonly channelRef: ChatConversationRef;
39
21
  readonly currentUserId: string;
40
22
  connect(): Promise<void>;
41
- /** Retry the connection now (the first-open failure's Retry) — see `ChannelClient.retryNow`. */
42
23
  retry(): void;
43
- /** Tear down every socket (auth-user switch / unmount). Idempotent. */
44
24
  close(): void;
45
25
  getState(): ChannelClientState;
46
26
  subscribe(fn: (s: ChannelClientState) => void): () => void;
47
27
  }
48
- /** Create the per-thread realtime client (the value placed on `config.realtime.client`). */
49
28
  export declare function createRealtimeChatClient(config: RealtimeTransportConfig): RealtimeChatClient;
50
- /** Create the per-USER inbox client (mount once at the dock; not per-thread). */
51
29
  export declare function createInboxClient(config: {
52
30
  endpoint: string;
53
31
  currentUserId: string;
@@ -55,8 +33,6 @@ export declare function createInboxClient(config: {
55
33
  socketFactory?: SocketFactory;
56
34
  timers?: TransportTimers;
57
35
  reconnect?: InboxClientConfig['reconnect'];
58
- /** OPT-IN structured diagnostics, forwarded to the InboxClient (default OFF). */
59
36
  diagnostics?: ChatClientDiagnosticsOption;
60
37
  }): InboxClient;
61
- export type { ChannelClientState, InboxClientState };
62
38
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,UAAU,EAAE,mBAAmB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC7C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,gGAAgG;IAChG,KAAK,IAAI,IAAI,CAAC;IACd,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,IAAI,kBAAkB,CAAC;IAC/B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5D;AAED,4FAA4F;AAC5F,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,kBAAkB,CAsB5F;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAC3C,iFAAiF;IACjF,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C,GAAG,WAAW,CAUd;AAED,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,mBAAmB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC7C,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,IAAI,kBAAkB,CAAC;IAC/B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5D;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,kBAAkB,CAsB5F;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C,GAAG,WAAW,CAUd"}
@@ -1,18 +1,6 @@
1
- // The realtime transport HANDLE the chat-react config carries on the `realtime`
2
- // transport (`ChatRealtimeTransportConfig.client`). It owns:
3
- // - a ChannelClient for the active thread (send/read/typing/presence/history)
4
- // - the grant-mint wiring (channel scope) via the injected grantProvider
5
- // - auth-user-switch teardown (close every socket before connecting as a new uid)
6
- //
7
- // The INBOX socket is created SEPARATELY (one per user, app-wide) via
8
- // `createInboxClient` — it is not tied to a single thread, so the app owns its
9
- // lifetime (mount it once at the dock, not per ChatShell). This keeps the channel
10
- // transport (per-thread) and the inbox transport (per-user) cleanly split, exactly
11
- // as the worker splits Channel DOs from the Inbox DO.
12
- import { ChannelClient } from './channel-client.js';
1
+ import { ChannelClient, } from './channel-client.js';
13
2
  import { InboxClient } from './inbox-client.js';
14
3
  import { browserSocketFactory } from './socket.js';
15
- /** Create the per-thread realtime client (the value placed on `config.realtime.client`). */
16
4
  export function createRealtimeChatClient(config) {
17
5
  const channel = new ChannelClient({
18
6
  endpoint: config.endpoint,
@@ -35,7 +23,6 @@ export function createRealtimeChatClient(config) {
35
23
  subscribe: (fn) => channel.subscribe(fn),
36
24
  };
37
25
  }
38
- /** Create the per-USER inbox client (mount once at the dock; not per-thread). */
39
26
  export function createInboxClient(config) {
40
27
  return new InboxClient({
41
28
  endpoint: config.endpoint,
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAC7D,gFAAgF;AAChF,2EAA2E;AAC3E,oFAAoF;AACpF,EAAE;AACF,sEAAsE;AACtE,+EAA+E;AAC/E,kFAAkF;AAClF,mFAAmF;AACnF,sDAAsD;AAEtD,OAAO,EAAE,aAAa,EAAqD,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAiD,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AAgDvE,4FAA4F;AAC5F,MAAM,UAAU,wBAAwB,CAAC,MAA+B;IACtE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QAChC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAC/B,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QAC5B,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,MASjC;IACC,OAAO,IAAI,WAAW,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;AACL,CAAC","sourcesContent":["// The realtime transport HANDLE the chat-react config carries on the `realtime`\n// transport (`ChatRealtimeTransportConfig.client`). It owns:\n// - a ChannelClient for the active thread (send/read/typing/presence/history)\n// - the grant-mint wiring (channel scope) via the injected grantProvider\n// - auth-user-switch teardown (close every socket before connecting as a new uid)\n//\n// The INBOX socket is created SEPARATELY (one per user, app-wide) via\n// `createInboxClient` — it is not tied to a single thread, so the app owns its\n// lifetime (mount it once at the dock, not per ChatShell). This keeps the channel\n// transport (per-thread) and the inbox transport (per-user) cleanly split, exactly\n// as the worker splits Channel DOs from the Inbox DO.\n\nimport { ChannelClient, type ChannelClientConfig, type ChannelClientState } from './channel-client.js';\nimport { InboxClient, type InboxClientConfig, type InboxClientState } from './inbox-client.js';\nimport { browserSocketFactory, type SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers } from './shared.js';\nimport type { ChatClientDiagnosticsOption } from './diagnostics.js';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\n/**\n * Everything the channel transport needs. The app supplies `grantProvider`\n * (its `mintChatGrant` callable, channel scope) + `endpoint`; `socketFactory`\n * defaults to the real browser WebSocket and is overridden in tests.\n */\nexport interface RealtimeTransportConfig {\n endpoint: string;\n /** The conversation the socket is for, as the app maps it onto the neutral reference. */\n channelRef: ChatConversationRef;\n threadId: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: ChannelClientConfig['reconnect'];\n /**\n * OPT-IN structured client diagnostics, forwarded to the ChannelClient. Absent\n * or `false` (the default) changes nothing and costs one nullish check; `true`\n * emits one single-line `console.debug` entry per client DECISION with a stable\n * `chat_client_*` event name; a function routes those entries into the app's own\n * logger. Content-free by construction (ids/seqs/states/counts/codes only).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\n/**\n * The opaque realtime client handle stored on `ChatRealtimeTransportConfig.client`.\n * The UI hook (`useRealtimeChatMessages`) drives it; the UI never reaches into the\n * socket directly.\n */\nexport interface RealtimeChatClient {\n readonly channel: ChannelClient;\n readonly channelRef: ChatConversationRef;\n readonly currentUserId: string;\n connect(): Promise<void>;\n /** Retry the connection now (the first-open failure's Retry) — see `ChannelClient.retryNow`. */\n retry(): void;\n /** Tear down every socket (auth-user switch / unmount). Idempotent. */\n close(): void;\n getState(): ChannelClientState;\n subscribe(fn: (s: ChannelClientState) => void): () => void;\n}\n\n/** Create the per-thread realtime client (the value placed on `config.realtime.client`). */\nexport function createRealtimeChatClient(config: RealtimeTransportConfig): RealtimeChatClient {\n const channel = new ChannelClient({\n endpoint: config.endpoint,\n threadId: config.threadId,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n\n return {\n channel,\n channelRef: config.channelRef,\n currentUserId: config.currentUserId,\n connect: () => channel.connect(),\n retry: () => channel.retryNow(),\n close: () => channel.close(),\n getState: () => channel.getState(),\n subscribe: (fn) => channel.subscribe(fn),\n };\n}\n\n/** Create the per-USER inbox client (mount once at the dock; not per-thread). */\nexport function createInboxClient(config: {\n endpoint: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: InboxClientConfig['reconnect'];\n /** OPT-IN structured diagnostics, forwarded to the InboxClient (default OFF). */\n diagnostics?: ChatClientDiagnosticsOption;\n}): InboxClient {\n return new InboxClient({\n endpoint: config.endpoint,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n}\n\nexport type { ChannelClientState, InboxClientState };\n"]}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAGd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,WAAW,EAA0B,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AA4BvE,MAAM,UAAU,wBAAwB,CAAC,MAA+B;IACtE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QAChC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAC/B,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QAC5B,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAQjC;IACC,OAAO,IAAI,WAAW,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import {\n ChannelClient,\n type ChannelClientConfig,\n type ChannelClientState,\n} from './channel-client.js';\nimport { InboxClient, type InboxClientConfig } from './inbox-client.js';\nimport { browserSocketFactory, type SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers } from './shared.js';\nimport type { ChatClientDiagnosticsOption } from './diagnostics.js';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\nexport interface RealtimeTransportConfig {\n endpoint: string;\n channelRef: ChatConversationRef;\n threadId: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: ChannelClientConfig['reconnect'];\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nexport interface RealtimeChatClient {\n readonly channel: ChannelClient;\n readonly channelRef: ChatConversationRef;\n readonly currentUserId: string;\n connect(): Promise<void>;\n retry(): void;\n close(): void;\n getState(): ChannelClientState;\n subscribe(fn: (s: ChannelClientState) => void): () => void;\n}\n\nexport function createRealtimeChatClient(config: RealtimeTransportConfig): RealtimeChatClient {\n const channel = new ChannelClient({\n endpoint: config.endpoint,\n threadId: config.threadId,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n\n return {\n channel,\n channelRef: config.channelRef,\n currentUserId: config.currentUserId,\n connect: () => channel.connect(),\n retry: () => channel.retryNow(),\n close: () => channel.close(),\n getState: () => channel.getState(),\n subscribe: (fn) => channel.subscribe(fn),\n };\n}\n\nexport function createInboxClient(config: {\n endpoint: string;\n currentUserId: string;\n grantProvider: GrantProvider;\n socketFactory?: SocketFactory;\n timers?: TransportTimers;\n reconnect?: InboxClientConfig['reconnect'];\n diagnostics?: ChatClientDiagnosticsOption;\n}): InboxClient {\n return new InboxClient({\n endpoint: config.endpoint,\n currentUserId: config.currentUserId,\n grantProvider: config.grantProvider,\n socketFactory: config.socketFactory ?? browserSocketFactory,\n timers: config.timers,\n reconnect: config.reconnect,\n diagnostics: config.diagnostics,\n });\n}\n"]}
@@ -1,6 +1,6 @@
1
- import type { ChatMessageV1 } from "@ttt-productions/chat-core";
2
- import type { RealtimeChatClient } from "./transport.js";
3
- import type { ChannelClientState } from "./channel-client.js";
1
+ import type { ChatMessageV1 } from '@ttt-productions/chat-core';
2
+ import type { RealtimeChatClient } from './transport.js';
3
+ import type { ChannelClientState } from './channel-client.js';
4
4
  export type UseRealtimeChatMessagesResult = {
5
5
  allowed: boolean;
6
6
  isInitialLoading: boolean;
@@ -8,34 +8,17 @@ export type UseRealtimeChatMessagesResult = {
8
8
  fetchOlder: () => Promise<void>;
9
9
  hasOlder: boolean;
10
10
  isFetchingOlder: boolean;
11
- /** True once the first open has failed (see `ChannelClientState.initialLoadFailed`). */
12
11
  initialLoadFailed: boolean;
13
- /** The channel is current (see `ChannelClientState.isSynced`): act on its terms only while true. */
14
12
  isSynced: boolean;
15
- /** Retry the connection now — the first-open failure's Retry. */
16
13
  retry: () => void;
17
- /** Realtime extras (not present on the firestore result) — optional for the UI. */
18
- status: ChannelClientState["status"];
19
- /** The last structured error code the DO sent (e.g. a close reason). Drives the
20
- * disconnected-banner detail (R13). */
14
+ status: ChannelClientState['status'];
21
15
  lastErrorCode: string | null;
22
16
  typing: string[];
23
17
  presence: string[];
24
- /** Returns false when the socket was closed and nothing was sent (C-B8) — the caller keeps the composer text. */
25
18
  send: (text: string) => boolean;
26
- /** Retry a failed/un-acked send by its ORIGINAL clientMessageId (read from the
27
- * failed row's `meta.clientMessageId`) — the transport re-queues the tracked
28
- * pending send with the SAME id so the DO's send-idempotency dedups any copy
29
- * that already landed. Returns false for an unknown id (already acked/
30
- * reconciled). ChatShell wires this to MessageItemDefault's failed-bubble
31
- * retry affordance by default. */
32
19
  retrySend: (clientMessageId: string) => boolean;
33
- /** Returns whether the ack frame was sent (false on a closed socket) so the caller advances its local cursor only on success (M2). */
34
20
  readAck: (readSeq: number, focused: boolean) => boolean;
35
21
  signalTyping: () => void;
36
- /** Watched-only presence (R14): the UI subscribes ONLY while a member-list / online
37
- * indicator is open, and unsubscribes when it closes, so an unwatched channel runs
38
- * zero presence alarms. `presence` stays empty until subscribed. */
39
22
  presenceSubscribe: () => void;
40
23
  presenceUnsubscribe: () => void;
41
24
  };
@@ -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;AAI9D,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,wFAAwF;IACxF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oGAAoG;IACpG,QAAQ,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,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,CAkFjG"}
1
+ {"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAGA,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;AAI9D,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,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,SAAS,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,CAuEjG"}
@@ -1,26 +1,14 @@
1
- "use client";
2
- // React hook for the REALTIME transport, returning the SAME shape as
3
- // `useChatMessages` (the firestore path) so `ChatShell` is transport-agnostic.
4
- // It SUBSCRIBES to a `RealtimeChatClient`'s observable state. It does NOT own the
5
- // socket lifecycle: the caller (the app hook that CREATES the client, e.g.
6
- // `useRealtimeChannelClient`) is the single connect/close owner (C-B7) — connecting
7
- // here too opened a second WebSocket. The client instance is owned by the caller
8
- // (passed via config) so its identity is the subscription key — a new client for a
9
- // new uid replaces the old one (Contract A auth-user-switch teardown happens in the
10
- // owning hook).
11
- import * as React from "react";
12
- import { useOptionalChatNameResolver } from "../context/ChatNameResolverContext.js";
13
- import { newCorrelationId } from "./shared.js";
1
+ 'use client';
2
+ import * as React from 'react';
3
+ import { useOptionalChatNameResolver } from '../context/ChatNameResolverContext.js';
4
+ import { newCorrelationId } from './shared.js';
14
5
  export function useRealtimeChatMessages(client) {
15
6
  const [state, setState] = React.useState(() => client.getState());
16
- // Subscribe only; the owning hook drives connect/close (single lifecycle owner, C-B7).
17
- // Re-subscribes on client identity change (auth-user switch).
18
7
  React.useEffect(() => {
19
8
  const unsub = client.subscribe(setState);
20
9
  setState(client.getState());
21
10
  return unsub;
22
11
  }, [client]);
23
- // Pre-warm the name cache for visible senders (parity with the firestore hook).
24
12
  const resolverCtx = useOptionalChatNameResolver();
25
13
  const senderIds = React.useMemo(() => {
26
14
  const set = new Set();
@@ -46,19 +34,9 @@ export function useRealtimeChatMessages(client) {
46
34
  const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);
47
35
  const presenceUnsubscribe = React.useCallback(() => client.channel.presenceUnsubscribe(), [client]);
48
36
  return {
49
- // On the realtime transport DATA ACCESS is enforced by the DO grant, so the hook
50
- // is "allowed" client-side UNLESS the grant provider surfaced a TERMINAL access
51
- // denial (ChatAccessDeniedError → stable 'access-denied' code). That single case
52
- // maps to allowed:false so ChatShell renders its no-access surface instead of an
53
- // eternal loader; a transient close / 4403-revoke keeps allowed:true and rides the
54
- // reconnect/disconnected banner. Otherwise accessMode is presentation-only here.
55
- allowed: state.lastErrorCode !== "access-denied",
56
- // Derive initial loading from authoritative-data completion, NOT connection status:
57
- // a socket can be 'open' with no snapshot yet, and a post-load reconnect must not
58
- // fall back into the opening state. `hasLoadedInitialData` encodes the full table.
37
+ allowed: state.lastErrorCode !== 'access-denied',
59
38
  isInitialLoading: !state.hasLoadedInitialData,
60
39
  initialLoadFailed: state.initialLoadFailed,
61
- // Current: open, loaded, and resynced since this socket opened (never sticky).
62
40
  isSynced: state.isSynced,
63
41
  retry,
64
42
  messages: state.messages,
@@ -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\";\nimport { newCorrelationId } from \"./shared.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 /** True once the first open has failed (see `ChannelClientState.initialLoadFailed`). */\n initialLoadFailed: boolean;\n /** The channel is current (see `ChannelClientState.isSynced`): act on its terms only while true. */\n isSynced: boolean;\n /** Retry the connection now — the first-open failure's Retry. */\n retry: () => void;\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 for (const uid of m.referencedUids ?? []) set.add(uid);\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: newCorrelationId(), 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 retry = React.useCallback(() => client.retry(), [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 initialLoadFailed: state.initialLoadFailed,\n // Current: open, loaded, and resynced since this socket opened (never sticky).\n isSynced: state.isSynced,\n retry,\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"]}
1
+ {"version":3,"file":"useRealtimeChatMessages.js","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\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';\nimport { newCorrelationId } from './shared.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 initialLoadFailed: boolean;\n isSynced: boolean;\n retry: () => void;\n status: ChannelClientState['status'];\n lastErrorCode: string | null;\n typing: string[];\n presence: string[];\n send: (text: string) => boolean;\n retrySend: (clientMessageId: string) => boolean;\n readAck: (readSeq: number, focused: boolean) => boolean;\n signalTyping: () => void;\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 React.useEffect(() => {\n const unsub = client.subscribe(setState);\n setState(client.getState());\n return unsub;\n }, [client]);\n\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 for (const uid of m.referencedUids ?? []) set.add(uid);\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 => client.channel.send({ clientMessageId: newCorrelationId(), 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 retry = React.useCallback(() => client.retry(), [client]);\n const presenceSubscribe = React.useCallback(() => client.channel.presenceSubscribe(), [client]);\n const presenceUnsubscribe = React.useCallback(\n () => client.channel.presenceUnsubscribe(),\n [client],\n );\n\n return {\n allowed: state.lastErrorCode !== 'access-denied',\n isInitialLoading: !state.hasLoadedInitialData,\n initialLoadFailed: state.initialLoadFailed,\n isSynced: state.isSynced,\n retry,\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"]}
@@ -1,18 +1,5 @@
1
- export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME, ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
2
- export type { ChatCloseCode, ChatSendRejectedPayload, ChatSendRejectionCode, } from '@ttt-productions/chat-schemas';
3
1
  import type { ChatConversationRef, ChatMarkReadResultPayload, ChatMessageRevisionKind, ChatSendRejectedPayload } from '@ttt-productions/chat-schemas';
4
- /** A revision's kind (chat-schemas `ChatMessageRevisionKind`), under this package's historical name. */
5
2
  export type RevisionKind = ChatMessageRevisionKind;
6
- /**
7
- * A message row as the Channel DO stores + broadcasts it (the worker runtime's
8
- * message row), declared as the fields the client actually CONSUMES. The adapter
9
- * mapping in `map.ts` turns it into the UI `ChatMessageV1` shape.
10
- *
11
- * The row is TEXT-only: there is no attachment lifecycle on a chat message, and
12
- * no reply pointer — chat has no reply-authoring affordance, so the client
13
- * neither sends nor reads one (DJ ruling 2026-07-29). A DO that still broadcasts
14
- * a legacy stringified `replyTo` key is simply ignored here.
15
- */
16
3
  export interface WireMessageRow {
17
4
  seq: number;
18
5
  senderUid: string;
@@ -20,67 +7,25 @@ export interface WireMessageRow {
20
7
  text: string;
21
8
  createdAt: number;
22
9
  epoch: number;
23
- /** Account ids a server-written row's text refers to (absent on a member's own message). */
24
10
  referencedUids?: string[];
25
- /**
26
- * The effective (max-`messageRevision`) moderation kind for this row, or null
27
- * when never moderated. Populated by the DO's getHistory/getDeltaSince overlay
28
- * merge so a hidden/deleted/edited row arrives already-moderated and its
29
- * ORIGINAL text never reaches the renderer. Optional/absent on pre-overlay rows.
30
- */
31
11
  moderationKind?: RevisionKind | null;
32
- /**
33
- * The effective `messageRevision` that `moderationKind` reflects (the DO's
34
- * max-revision). Used to break ties against a live `revision` frame: a higher
35
- * `messageRevision` wins; a stale/older one is ignored. Optional/absent on
36
- * pre-overlay rows (treated as revision 0).
37
- */
38
12
  messageRevision?: number;
39
13
  }
40
- /**
41
- * The channel resume snapshot the DO returns for a `resume` frame (Contract C).
42
- * `resync` / `delta` are the durable gap-repair half of the contract:
43
- * - `resync: false` + `delta` = the contiguous ≤500-message backlog after the
44
- * client's cursor (oldest-first); the client APPLIES the delta and advances its
45
- * cursor to `lastMessageSeq` — a missed broadcast is never permanent.
46
- * - `resync: true` = the gap exceeds the backlog (or the client sent no cursor);
47
- * the client DROPS its local tail and re-pages history from the top.
48
- * Both are OPTIONAL/absent on a pre-contract DO (treated as `resync: true`, empty
49
- * delta — the safe re-page path) so a stale worker can't leave a client stuck.
50
- */
51
14
  export interface WireChannelSnapshot {
52
15
  lastMessageSeq: number;
53
16
  readSeq: number;
54
17
  resync?: boolean;
55
18
  delta?: WireMessageRow[];
56
19
  }
57
- /** A registry entry in the inbox snapshot (the worker runtime's registry entry). */
58
20
  export interface WireRegistryEntry {
59
21
  channelRef: string;
60
22
  kind: string;
61
23
  state: 'active' | 'tombstoned';
62
24
  registryVersion: number;
63
- /**
64
- * The conversation this entry is — the OPENABLE identity for the inbox view (the
65
- * `channelRef` is an opaque registry key). The app maps it back to its own
66
- * conversation; absent, the entry offers no open action.
67
- */
68
25
  ref?: ChatConversationRef | null;
69
- /**
70
- * Per-entry unread flag (drives the per-row dot). Optional/absent until the inbox
71
- * DO populates it; `deriveUnreadRefs` reads it as a typed field (no cast).
72
- */
73
26
  unread?: boolean;
74
- /**
75
- * Archived state — DISTINCT from `tombstoned`. An archived row is still an ACTIVE
76
- * membership (`state: 'active'`, history readable) but is grouped under the inbox
77
- * view's Archived toggle instead of the active list. The client must NOT count
78
- * archived rows in the unread roll-up (archive = done; the DO clears unread on
79
- * archive). Optional/absent on a pre-archive DO (treated as not-archived).
80
- */
81
27
  archived?: boolean;
82
28
  }
83
- /** The inbox snapshot payload pushed on connect / `resume` / live delta. */
84
29
  export interface WireInboxSnapshot {
85
30
  registry: WireRegistryEntry[];
86
31
  hasUnread: boolean;
@@ -141,14 +86,11 @@ export type ServerFrame = {
141
86
  messageRevision: number;
142
87
  };
143
88
  };
144
- /** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */
145
89
  export declare function buildFrame(type: string, payload: Record<string, unknown>): string;
146
- /** Parse an inbound frame, tolerating malformed JSON. Returns null on garbage. */
147
90
  export declare function parseFrame(raw: string): {
148
91
  v?: number;
149
92
  type?: string;
150
93
  payload?: Record<string, unknown>;
151
94
  } | null;
152
- /** A snapshot with `registry` is an inbox snapshot; one with `lastMessageSeq` is a channel snapshot. */
153
95
  export declare function isInboxSnapshot(p: WireChannelSnapshot | WireInboxSnapshot): p is WireInboxSnapshot;
154
96
  //# sourceMappingURL=wire.d.ts.map