@ttt-productions/chat-react 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +13 -11
  2. package/dist/context/ChatNameResolverContext.d.ts +3 -3
  3. package/dist/context/ChatNameResolverContext.js +3 -3
  4. package/dist/context/ChatNameResolverContext.js.map +1 -1
  5. package/dist/hooks/useChatMessages.d.ts.map +1 -1
  6. package/dist/hooks/useChatMessages.js +4 -13
  7. package/dist/hooks/useChatMessages.js.map +1 -1
  8. package/dist/index.d.ts +2 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/realtime/channel-client.d.ts +21 -4
  13. package/dist/realtime/channel-client.d.ts.map +1 -1
  14. package/dist/realtime/channel-client.js +109 -12
  15. package/dist/realtime/channel-client.js.map +1 -1
  16. package/dist/realtime/diagnostics.d.ts +7 -0
  17. package/dist/realtime/diagnostics.d.ts.map +1 -1
  18. package/dist/realtime/diagnostics.js +7 -0
  19. package/dist/realtime/diagnostics.js.map +1 -1
  20. package/dist/realtime/inbox-client.d.ts.map +1 -1
  21. package/dist/realtime/inbox-client.js +29 -8
  22. package/dist/realtime/inbox-client.js.map +1 -1
  23. package/dist/realtime/map.d.ts +3 -5
  24. package/dist/realtime/map.d.ts.map +1 -1
  25. package/dist/realtime/map.js +3 -18
  26. package/dist/realtime/map.js.map +1 -1
  27. package/dist/realtime/shared.d.ts +21 -0
  28. package/dist/realtime/shared.d.ts.map +1 -1
  29. package/dist/realtime/shared.js +22 -0
  30. package/dist/realtime/shared.js.map +1 -1
  31. package/dist/realtime/useRealtimeChatMessages.d.ts +1 -4
  32. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
  33. package/dist/realtime/useRealtimeChatMessages.js +1 -3
  34. package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
  35. package/dist/realtime/wire.d.ts +6 -5
  36. package/dist/realtime/wire.d.ts.map +1 -1
  37. package/dist/realtime/wire.js.map +1 -1
  38. package/dist/types.d.ts +1 -52
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js.map +1 -1
  41. package/dist/ui/ChatShell.d.ts +2 -2
  42. package/dist/ui/ChatShell.d.ts.map +1 -1
  43. package/dist/ui/ChatShell.js +6 -10
  44. package/dist/ui/ChatShell.js.map +1 -1
  45. package/dist/ui/Composer.d.ts +7 -6
  46. package/dist/ui/Composer.d.ts.map +1 -1
  47. package/dist/ui/Composer.js +2 -18
  48. package/dist/ui/Composer.js.map +1 -1
  49. package/dist/ui/MessageItemDefault.d.ts.map +1 -1
  50. package/dist/ui/MessageItemDefault.js +7 -12
  51. package/dist/ui/MessageItemDefault.js.map +1 -1
  52. package/dist/ui/MessageText.d.ts +19 -0
  53. package/dist/ui/MessageText.d.ts.map +1 -0
  54. package/dist/ui/MessageText.js +7 -0
  55. package/dist/ui/MessageText.js.map +1 -0
  56. package/package.json +6 -6
  57. package/src/styles/chat.css +0 -40
  58. package/dist/mentions/MentionAutocomplete.d.ts +0 -22
  59. package/dist/mentions/MentionAutocomplete.d.ts.map +0 -1
  60. package/dist/mentions/MentionAutocomplete.js +0 -21
  61. package/dist/mentions/MentionAutocomplete.js.map +0 -1
  62. package/dist/mentions/MessageText.d.ts +0 -27
  63. package/dist/mentions/MessageText.d.ts.map +0 -1
  64. package/dist/mentions/MessageText.js +0 -20
  65. package/dist/mentions/MessageText.js.map +0 -1
  66. package/dist/mentions/use-mention-autocomplete.d.ts +0 -82
  67. package/dist/mentions/use-mention-autocomplete.d.ts.map +0 -1
  68. package/dist/mentions/use-mention-autocomplete.js +0 -323
  69. package/dist/mentions/use-mention-autocomplete.js.map +0 -1
package/README.md CHANGED
@@ -1,21 +1,21 @@
1
1
  # @ttt-productions/chat-react
2
2
 
3
3
  Chat **React UI** for TTT Productions apps: chat shell, composer, message list,
4
- the generic mention autocomplete UI, the name-resolver context, and the
5
- realtime-newest-window + infinite-older hooks. Chat is TEXT-only — a
6
- conversation's files live in the consuming app's Conversation Files surface,
7
- never in the message timeline.
4
+ the name-resolver context, and the realtime-newest-window + infinite-older
5
+ hooks. Chat is PLAIN-TEXT-only — a conversation's files live in the consuming
6
+ app's Conversation Files surface, never in the message timeline, and message text
7
+ carries no token grammar (no mentions, no autocomplete, no chips).
8
8
 
9
9
  Built on the pure [`@ttt-productions/chat-core`](../chat-core) package
10
- (contracts, mention parser/serializer, grouping). A non-React consumer (a Cloud
11
- Function, a script, a future native/TV client) installs `chat-core` and pulls
12
- in none of this frontend tree.
10
+ (contracts, grouping). A non-React consumer (a Cloud Function, a script, a
11
+ future native/TV client) installs `chat-core` and pulls in none of this frontend
12
+ tree.
13
13
 
14
14
  ## Entry points
15
15
 
16
16
  - `.` — React components, hooks, the Firebase-client adapter config types
17
- (`ChatCoreConfig`, `ChatMentionConfig`), and the React render types
18
- (`MessageRenderer`, `RenderableMentionProvider`, `MentionResultRenderer`).
17
+ (`ChatCoreConfig`), and the React render types (`MessageRenderer`,
18
+ `MessageRendererRegistry`).
19
19
  - `./styles` — chat CSS. Import once in your app layout:
20
20
  `import "@ttt-productions/chat-react/styles";`
21
21
 
@@ -27,5 +27,7 @@ are optional peers — provided by the consuming app.
27
27
  ## Boundary
28
28
 
29
29
  `chat-react` does not import `ttt-core`, does not hardcode TTT origins, and does
30
- not build TTT storage paths — it runs no upload path at all. TTT mention kinds,
31
- permissions, provider lists, routing, and search live in TTT code.
30
+ not build TTT storage paths — it runs no upload path at all. It owns no mention
31
+ machinery either: mentions belong to the app's Square-posts surface, not to chat.
32
+ And it owns no reply-to machinery: every send path takes text alone, because the
33
+ product has no affordance for replying to a specific message.
@@ -16,9 +16,9 @@ export type ChatNameResolverProviderProps = {
16
16
  */
17
17
  export declare function ChatNameResolverProvider(props: ChatNameResolverProviderProps): React.JSX.Element;
18
18
  /**
19
- * Strict hook — throws if no provider is wrapped. Used inside chat-core's
20
- * own renderers (MessageItemDefault, ReplyQuote) so a missing setup surfaces
21
- * immediately on first render rather than silently rendering "User".
19
+ * Strict hook — throws if no provider is wrapped. Used inside this package's
20
+ * own renderers (MessageItemDefault) so a missing setup surfaces immediately on
21
+ * first render rather than silently rendering "User".
22
22
  */
23
23
  export declare function useChatNameResolver(): ChatNameResolverContextValue;
24
24
  /**
@@ -13,9 +13,9 @@ export function ChatNameResolverProvider(props) {
13
13
  return (_jsx(ChatNameResolverContext.Provider, { value: value, children: children }));
14
14
  }
15
15
  /**
16
- * Strict hook — throws if no provider is wrapped. Used inside chat-core's
17
- * own renderers (MessageItemDefault, ReplyQuote) so a missing setup surfaces
18
- * immediately on first render rather than silently rendering "User".
16
+ * Strict hook — throws if no provider is wrapped. Used inside this package's
17
+ * own renderers (MessageItemDefault) so a missing setup surfaces immediately on
18
+ * first render rather than silently rendering "User".
19
19
  */
20
20
  export function useChatNameResolver() {
21
21
  const ctx = React.useContext(ChatNameResolverContext);
@@ -1 +1 @@
1
- {"version":3,"file":"ChatNameResolverContext.js","sourceRoot":"","sources":["../../src/context/ChatNameResolverContext.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,MAAM,uBAAuB,GAAG,KAAK,CAAC,aAAa,CAAsC,IAAI,CAAC,CAAC;AAQ/F;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAoC;IAC3E,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IACtF,OAAO,CACL,KAAC,uBAAuB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAC3C,QAAQ,GACwB,CACpC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,6GAA6G,CAC9G,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,MAAM,EAAE,WAAW,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAC9C,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC;AACzC,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { ChatNameResolver, ChatPrewarmSenders } from \"@ttt-productions/chat-core\";\n\ntype ChatNameResolverContextValue = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n};\n\nconst ChatNameResolverContext = React.createContext<ChatNameResolverContextValue | null>(null);\n\nexport type ChatNameResolverProviderProps = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n children: React.ReactNode;\n};\n\n/**\n * Wraps any chat-core consumer (typically the whole app shell) and provides\n * a name resolver. Required for any tree that renders <ChatShell>, <MessageList>,\n * or <MessageItemDefault>.\n */\nexport function ChatNameResolverProvider(props: ChatNameResolverProviderProps) {\n const { resolveName, prewarm, children } = props;\n const value = React.useMemo(() => ({ resolveName, prewarm }), [resolveName, prewarm]);\n return (\n <ChatNameResolverContext.Provider value={value}>\n {children}\n </ChatNameResolverContext.Provider>\n );\n}\n\n/**\n * Strict hook — throws if no provider is wrapped. Used inside chat-core's\n * own renderers (MessageItemDefault, ReplyQuote) so a missing setup surfaces\n * immediately on first render rather than silently rendering \"User\".\n */\nexport function useChatNameResolver(): ChatNameResolverContextValue {\n const ctx = React.useContext(ChatNameResolverContext);\n if (!ctx) {\n throw new Error(\n \"[chat-core] ChatNameResolverProvider is required. Wrap your ChatShell tree with <ChatNameResolverProvider>.\"\n );\n }\n return ctx;\n}\n\n/**\n * Optional variant — returns null instead of throwing when no provider is\n * wrapped. Used internally by useChatMessages so headless callers (tests,\n * data-only consumers that don't render UI) can still use the hook.\n */\nexport function useOptionalChatNameResolver(): ChatNameResolverContextValue | null {\n return React.useContext(ChatNameResolverContext);\n}\n\n/**\n * Convenience hook for renderers. Returns a resolved string — never null —\n * by falling back to \"User\" when the cache hasn't filled yet. Re-renders\n * automatically once the upstream cache settles.\n */\nexport function useResolvedSenderName(senderId: string): string {\n const { resolveName } = useChatNameResolver();\n return resolveName(senderId) ?? \"User\";\n}\n"]}
1
+ {"version":3,"file":"ChatNameResolverContext.js","sourceRoot":"","sources":["../../src/context/ChatNameResolverContext.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,MAAM,uBAAuB,GAAG,KAAK,CAAC,aAAa,CAAsC,IAAI,CAAC,CAAC;AAQ/F;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAoC;IAC3E,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;IACtF,OAAO,CACL,KAAC,uBAAuB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAC3C,QAAQ,GACwB,CACpC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;IACtD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,6GAA6G,CAC9G,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,MAAM,EAAE,WAAW,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAC9C,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC;AACzC,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { ChatNameResolver, ChatPrewarmSenders } from \"@ttt-productions/chat-core\";\n\ntype ChatNameResolverContextValue = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n};\n\nconst ChatNameResolverContext = React.createContext<ChatNameResolverContextValue | null>(null);\n\nexport type ChatNameResolverProviderProps = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n children: React.ReactNode;\n};\n\n/**\n * Wraps any chat-core consumer (typically the whole app shell) and provides\n * a name resolver. Required for any tree that renders <ChatShell>, <MessageList>,\n * or <MessageItemDefault>.\n */\nexport function ChatNameResolverProvider(props: ChatNameResolverProviderProps) {\n const { resolveName, prewarm, children } = props;\n const value = React.useMemo(() => ({ resolveName, prewarm }), [resolveName, prewarm]);\n return (\n <ChatNameResolverContext.Provider value={value}>\n {children}\n </ChatNameResolverContext.Provider>\n );\n}\n\n/**\n * Strict hook — throws if no provider is wrapped. Used inside this package's\n * own renderers (MessageItemDefault) so a missing setup surfaces immediately on\n * first render rather than silently rendering \"User\".\n */\nexport function useChatNameResolver(): ChatNameResolverContextValue {\n const ctx = React.useContext(ChatNameResolverContext);\n if (!ctx) {\n throw new Error(\n \"[chat-core] ChatNameResolverProvider is required. Wrap your ChatShell tree with <ChatNameResolverProvider>.\"\n );\n }\n return ctx;\n}\n\n/**\n * Optional variant — returns null instead of throwing when no provider is\n * wrapped. Used internally by useChatMessages so headless callers (tests,\n * data-only consumers that don't render UI) can still use the hook.\n */\nexport function useOptionalChatNameResolver(): ChatNameResolverContextValue | null {\n return React.useContext(ChatNameResolverContext);\n}\n\n/**\n * Convenience hook for renderers. Returns a resolved string — never null —\n * by falling back to \"User\" when the cache hasn't filled yet. Re-renders\n * automatically once the upstream cache settles.\n */\nexport function useResolvedSenderName(senderId: string): string {\n const { resolveName } = useChatNameResolver();\n return resolveName(senderId) ?? \"User\";\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useChatMessages.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,MAAM,MAAM,qBAAqB,GAAG;IAClC,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;CAC1B,CAAC;AAwCF,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,qBAAqB,CAuF7E"}
1
+ {"version":3,"file":"useChatMessages.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,MAAM,MAAM,qBAAqB,GAAG;IAClC,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;CAC1B,CAAC;AAgCF,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,qBAAqB,CAsF7E"}
@@ -13,15 +13,9 @@ function mapMsg(data, threadId) {
13
13
  if (!createdAt) {
14
14
  console.error('[useChatMessages] Message missing valid createdAt:', data.id, d.createdAt);
15
15
  }
16
- // replyTo: the new shape uses senderId, not senderUsername. Tolerate older
17
- // docs that still have senderUsername — drop the name and keep what we can.
18
- const replyTo = d.replyTo
19
- ? {
20
- messageId: d.replyTo.messageId,
21
- senderId: d.replyTo.senderId ?? "",
22
- messagePreview: d.replyTo.messagePreview,
23
- }
24
- : undefined;
16
+ // A stored `replyTo` (if any legacy doc still carries one) is deliberately NOT
17
+ // mapped: chat has no reply-authoring affordance, so the UI has no reply
18
+ // renderer to feed (DJ ruling 2026-07-29).
25
19
  return {
26
20
  messageId: data.id,
27
21
  threadId,
@@ -29,7 +23,6 @@ function mapMsg(data, threadId) {
29
23
  senderId: d.senderId,
30
24
  text: d.text ?? "",
31
25
  type: d.type,
32
- replyTo,
33
26
  isSystemMessage: d.isSystemMessage ?? undefined,
34
27
  // Moderation tombstone flag (backend-written) — carried through so renderers
35
28
  // can tombstone the content.
@@ -64,15 +57,13 @@ export function useChatMessages(config) {
64
57
  getSortValue: (data) => toMillis(data.createdAt) ?? 0,
65
58
  });
66
59
  const messages = React.useMemo(() => (allowed ? items : []), [allowed, items]);
67
- // Pre-warm name cache for all visible senders (and reply-to senders).
60
+ // Pre-warm name cache for all visible senders.
68
61
  // Optional resolver: silent no-op if no provider is wrapped.
69
62
  const resolverCtx = useOptionalChatNameResolver();
70
63
  const senderIds = React.useMemo(() => {
71
64
  const set = new Set();
72
65
  for (const m of messages) {
73
66
  set.add(m.senderId);
74
- if (m.replyTo?.senderId)
75
- set.add(m.replyTo.senderId);
76
67
  }
77
68
  return Array.from(set);
78
69
  }, [messages]);
@@ -1 +1 @@
1
- {"version":3,"file":"useChatMessages.js","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { DocumentData } from \"firebase/firestore\";\nimport { toMillis } from \"@ttt-productions/firebase-helpers\";\nimport { useFirestoreLiveInfinite } from \"@ttt-productions/query-core/react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { ChatCoreConfig } from \"../types.js\";\nimport { canAccessThread } from \"./useChatThreadAccess.js\";\nimport { messagesColPath } from \"../firestore/queries.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\n// Safety limit to prevent memory exhaustion\nconst MAX_PAGE_SIZE = 100;\n\nexport type UseChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n};\n\nfunction mapMsg(\n data: DocumentData & { id: string },\n threadId: string\n): ChatMessageV1 {\n const d = data as any;\n\n const createdAt = toMillis(d.createdAt);\n\n if (!createdAt) {\n console.error('[useChatMessages] Message missing valid createdAt:', data.id, d.createdAt);\n }\n\n // replyTo: the new shape uses senderId, not senderUsername. Tolerate older\n // docs that still have senderUsername — drop the name and keep what we can.\n const replyTo = d.replyTo\n ? {\n messageId: d.replyTo.messageId,\n senderId: d.replyTo.senderId ?? \"\",\n messagePreview: d.replyTo.messagePreview,\n }\n : undefined;\n\n return {\n messageId: data.id,\n threadId,\n createdAt: createdAt || 0,\n senderId: d.senderId,\n text: d.text ?? \"\",\n type: d.type,\n replyTo,\n isSystemMessage: d.isSystemMessage ?? undefined,\n // Moderation tombstone flag (backend-written) — carried through so renderers\n // can tombstone the content.\n hidden: d.hidden ?? undefined,\n meta: d.meta,\n };\n}\n\nexport function useChatMessages(config: ChatCoreConfig): UseChatMessagesResult {\n const {\n chatCollectionPath,\n messagesSubcollection = \"messages\",\n threadId,\n pageSize: requestedPageSize = 20,\n currentUserId,\n isAdmin,\n accessMode,\n threadAllowedUserIds,\n createdAtField = \"createdAt\",\n } = config;\n\n const pageSize = Math.min(requestedPageSize, MAX_PAGE_SIZE);\n\n if (requestedPageSize > MAX_PAGE_SIZE) {\n console.warn(\n `[useChatMessages] Requested pageSize ${requestedPageSize} exceeds maximum ${MAX_PAGE_SIZE}. Using ${MAX_PAGE_SIZE}.`\n );\n }\n\n const allowed = React.useMemo(\n () =>\n canAccessThread({\n accessMode,\n isAdmin,\n currentUserId,\n allowedUserIds: threadAllowedUserIds,\n }),\n [accessMode, isAdmin, currentUserId, threadAllowedUserIds]\n );\n\n // Stable key segment for the message collection path (string | string[]).\n const pathKey = React.useMemo(\n () =>\n JSON.stringify(\n Array.isArray(chatCollectionPath) ? chatCollectionPath : [chatCollectionPath]\n ),\n [chatCollectionPath]\n );\n\n // Slash-joined collection path for the generic hook (handles nested paths).\n const collectionPath = React.useMemo(\n () => messagesColPath(chatCollectionPath, threadId, messagesSubcollection).join(\"/\"),\n [chatCollectionPath, threadId, messagesSubcollection]\n );\n\n const { items, isInitialLoading, fetchOlder, hasOlder, isFetchingOlder } =\n useFirestoreLiveInfinite<ChatMessageV1>({\n collectionPath,\n queryKey: [\"chat-core\", \"messages\", pathKey, threadId, messagesSubcollection, pageSize],\n orderByField: createdAtField,\n pageSize,\n enabled: allowed,\n sort: \"asc\",\n select: (data) => mapMsg(data, threadId),\n getSortValue: (data) => toMillis(data.createdAt) ?? 0,\n });\n\n const messages = React.useMemo(() => (allowed ? items : []), [allowed, items]);\n\n // Pre-warm name cache for all visible senders (and reply-to senders).\n // Optional resolver: silent no-op if no provider is wrapped.\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of messages) {\n set.add(m.senderId);\n if (m.replyTo?.senderId) set.add(m.replyTo.senderId);\n }\n return Array.from(set);\n }, [messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) {\n resolverCtx.prewarm(senderIds);\n }\n }, [resolverCtx, senderIds]);\n\n return {\n allowed,\n isInitialLoading: allowed ? isInitialLoading : false,\n messages,\n fetchOlder,\n hasOlder,\n isFetchingOlder,\n };\n}"]}
1
+ {"version":3,"file":"useChatMessages.js","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAG7E,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AAEpF,4CAA4C;AAC5C,MAAM,aAAa,GAAG,GAAG,CAAC;AAW1B,SAAS,MAAM,CACb,IAAmC,EACnC,QAAgB;IAEhB,MAAM,CAAC,GAAG,IAAW,CAAC;IAEtB,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAExC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,oDAAoD,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5F,CAAC;IAED,+EAA+E;IAC/E,yEAAyE;IACzE,2CAA2C;IAC3C,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,EAAE;QAClB,QAAQ;QACR,SAAS,EAAE,SAAS,IAAI,CAAC;QACzB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,SAAS;QAC/C,6EAA6E;QAC7E,6BAA6B;QAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;QAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,MAAM,EACJ,kBAAkB,EAClB,qBAAqB,GAAG,UAAU,EAClC,QAAQ,EACR,QAAQ,EAAE,iBAAiB,GAAG,EAAE,EAChC,aAAa,EACb,OAAO,EACP,UAAU,EACV,oBAAoB,EACpB,cAAc,GAAG,WAAW,GAC7B,GAAG,MAAM,CAAC;IAEX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;IAE5D,IAAI,iBAAiB,GAAG,aAAa,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CACV,wCAAwC,iBAAiB,oBAAoB,aAAa,WAAW,aAAa,GAAG,CACtH,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAC3B,GAAG,EAAE,CACH,eAAe,CAAC;QACd,UAAU;QACV,OAAO;QACP,aAAa;QACb,cAAc,EAAE,oBAAoB;KACrC,CAAC,EACJ,CAAC,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,oBAAoB,CAAC,CAC3D,CAAC;IAEF,0EAA0E;IAC1E,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAC3B,GAAG,EAAE,CACH,IAAI,CAAC,SAAS,CACZ,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAC9E,EACH,CAAC,kBAAkB,CAAC,CACrB,CAAC;IAEF,4EAA4E;IAC5E,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAClC,GAAG,EAAE,CAAC,eAAe,CAAC,kBAAkB,EAAE,QAAQ,EAAE,qBAAqB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EACpF,CAAC,kBAAkB,EAAE,QAAQ,EAAE,qBAAqB,CAAC,CACtD,CAAC;IAEF,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,GACtE,wBAAwB,CAAgB;QACtC,cAAc;QACd,QAAQ,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,QAAQ,CAAC;QACvF,YAAY,EAAE,cAAc;QAC5B,QAAQ;QACR,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC;QACxC,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KACtD,CAAC,CAAC;IAEL,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IAE/E,+CAA+C;IAC/C,6DAA6D;IAC7D,MAAM,WAAW,GAAG,2BAA2B,EAAE,CAAC;IAClD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;QAC9B,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtB,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,IAAI,WAAW,EAAE,OAAO,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjD,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;IAE7B,OAAO;QACL,OAAO;QACP,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK;QACpD,QAAQ;QACR,UAAU;QACV,QAAQ;QACR,eAAe;KAChB,CAAC;AACJ,CAAC","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { DocumentData } from \"firebase/firestore\";\nimport { toMillis } from \"@ttt-productions/firebase-helpers\";\nimport { useFirestoreLiveInfinite } from \"@ttt-productions/query-core/react\";\nimport type { ChatMessageV1 } from \"@ttt-productions/chat-core\";\nimport type { ChatCoreConfig } from \"../types.js\";\nimport { canAccessThread } from \"./useChatThreadAccess.js\";\nimport { messagesColPath } from \"../firestore/queries.js\";\nimport { useOptionalChatNameResolver } from \"../context/ChatNameResolverContext.js\";\n\n// Safety limit to prevent memory exhaustion\nconst MAX_PAGE_SIZE = 100;\n\nexport type UseChatMessagesResult = {\n allowed: boolean;\n isInitialLoading: boolean;\n messages: ChatMessageV1[];\n fetchOlder: () => Promise<void>;\n hasOlder: boolean;\n isFetchingOlder: boolean;\n};\n\nfunction mapMsg(\n data: DocumentData & { id: string },\n threadId: string\n): ChatMessageV1 {\n const d = data as any;\n\n const createdAt = toMillis(d.createdAt);\n\n if (!createdAt) {\n console.error('[useChatMessages] Message missing valid createdAt:', data.id, d.createdAt);\n }\n\n // A stored `replyTo` (if any legacy doc still carries one) is deliberately NOT\n // mapped: chat has no reply-authoring affordance, so the UI has no reply\n // renderer to feed (DJ ruling 2026-07-29).\n return {\n messageId: data.id,\n threadId,\n createdAt: createdAt || 0,\n senderId: d.senderId,\n text: d.text ?? \"\",\n type: d.type,\n isSystemMessage: d.isSystemMessage ?? undefined,\n // Moderation tombstone flag (backend-written) — carried through so renderers\n // can tombstone the content.\n hidden: d.hidden ?? undefined,\n meta: d.meta,\n };\n}\n\nexport function useChatMessages(config: ChatCoreConfig): UseChatMessagesResult {\n const {\n chatCollectionPath,\n messagesSubcollection = \"messages\",\n threadId,\n pageSize: requestedPageSize = 20,\n currentUserId,\n isAdmin,\n accessMode,\n threadAllowedUserIds,\n createdAtField = \"createdAt\",\n } = config;\n\n const pageSize = Math.min(requestedPageSize, MAX_PAGE_SIZE);\n\n if (requestedPageSize > MAX_PAGE_SIZE) {\n console.warn(\n `[useChatMessages] Requested pageSize ${requestedPageSize} exceeds maximum ${MAX_PAGE_SIZE}. Using ${MAX_PAGE_SIZE}.`\n );\n }\n\n const allowed = React.useMemo(\n () =>\n canAccessThread({\n accessMode,\n isAdmin,\n currentUserId,\n allowedUserIds: threadAllowedUserIds,\n }),\n [accessMode, isAdmin, currentUserId, threadAllowedUserIds]\n );\n\n // Stable key segment for the message collection path (string | string[]).\n const pathKey = React.useMemo(\n () =>\n JSON.stringify(\n Array.isArray(chatCollectionPath) ? chatCollectionPath : [chatCollectionPath]\n ),\n [chatCollectionPath]\n );\n\n // Slash-joined collection path for the generic hook (handles nested paths).\n const collectionPath = React.useMemo(\n () => messagesColPath(chatCollectionPath, threadId, messagesSubcollection).join(\"/\"),\n [chatCollectionPath, threadId, messagesSubcollection]\n );\n\n const { items, isInitialLoading, fetchOlder, hasOlder, isFetchingOlder } =\n useFirestoreLiveInfinite<ChatMessageV1>({\n collectionPath,\n queryKey: [\"chat-core\", \"messages\", pathKey, threadId, messagesSubcollection, pageSize],\n orderByField: createdAtField,\n pageSize,\n enabled: allowed,\n sort: \"asc\",\n select: (data) => mapMsg(data, threadId),\n getSortValue: (data) => toMillis(data.createdAt) ?? 0,\n });\n\n const messages = React.useMemo(() => (allowed ? items : []), [allowed, items]);\n\n // Pre-warm name cache for all visible senders.\n // Optional resolver: silent no-op if no provider is wrapped.\n const resolverCtx = useOptionalChatNameResolver();\n const senderIds = React.useMemo(() => {\n const set = new Set<string>();\n for (const m of messages) {\n set.add(m.senderId);\n }\n return Array.from(set);\n }, [messages]);\n\n React.useEffect(() => {\n if (resolverCtx?.prewarm && senderIds.length > 0) {\n resolverCtx.prewarm(senderIds);\n }\n }, [resolverCtx, senderIds]);\n\n return {\n allowed,\n isInitialLoading: allowed ? isInitialLoading : false,\n messages,\n fetchOlder,\n hasOlder,\n isFetchingOlder,\n };\n}"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { ChatCoreConfig, ChatTransportMode, ChatRealtimeTransportConfig, ChatMentionConfig, MessageRenderer, MessageRendererRegistry, RenderableMentionProvider, MentionResultRenderer, } from "./types.js";
1
+ export type { ChatCoreConfig, ChatTransportMode, ChatRealtimeTransportConfig, MessageRenderer, MessageRendererRegistry, } from "./types.js";
2
2
  export type { ChatNameResolverProviderProps } from "./context/ChatNameResolverContext.js";
3
3
  export { ChatNameResolverProvider, useChatNameResolver, useOptionalChatNameResolver, useResolvedSenderName, } from "./context/ChatNameResolverContext.js";
4
4
  export type { UseChatMessagesResult } from "./hooks/useChatMessages.js";
@@ -14,7 +14,5 @@ export { Composer } from "./ui/Composer.js";
14
14
  export type { MessageItemDefaultProps } from "./ui/MessageItemDefault.js";
15
15
  export { MessageItemDefault } from "./ui/MessageItemDefault.js";
16
16
  export { MessageActions, ThreadActions } from "./ui/menus.js";
17
- export { MessageText, type MessageTextProps } from "./mentions/MessageText.js";
18
- export { MentionAutocomplete, type MentionAutocompleteProps, } from "./mentions/MentionAutocomplete.js";
19
- export { useMentionAutocomplete, type UseMentionAutocompleteArgs, type UseMentionAutocompleteResult, type AutocompleteState, type AutocompleteResultGroup, } from "./mentions/use-mention-autocomplete.js";
17
+ export { MessageText, type MessageTextProps } from "./ui/MessageText.js";
20
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAE9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAKjE,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,YAAY,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,GAC9B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,4BAA4B,EACjC,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,wCAAwC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,6BAA6B,EAAE,MAAM,sCAAsC,CAAC;AAC1F,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAE9C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAKjE,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,6BAA6B,EAC7B,aAAa,EACb,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAU7B,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,YAAY,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -11,7 +11,5 @@ export { MessageList } from "./ui/MessageList.js";
11
11
  export { Composer } from "./ui/Composer.js";
12
12
  export { MessageItemDefault } from "./ui/MessageItemDefault.js";
13
13
  export { MessageActions, ThreadActions } from "./ui/menus.js";
14
- export { MessageText } from "./mentions/MessageText.js";
15
- export { MentionAutocomplete, } from "./mentions/MentionAutocomplete.js";
16
- export { useMentionAutocomplete, } from "./mentions/use-mention-autocomplete.js";
14
+ export { MessageText } from "./ui/MessageText.js";
17
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAsBb,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,+EAA+E;AAC/E,kFAAkF;AAClF,oFAAoF;AACpF,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EACL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EACL,sBAAsB,GAKvB,MAAM,wCAAwC,CAAC","sourcesContent":["\"use client\";\n\n// @ttt-productions/chat-react — chat React UI, hooks, the Firebase-client\n// adapter config types, and React render types. Depends on the pure\n// @ttt-productions/chat-core for contracts, parser, and grouping.\n//\n// Apps import chat CSS via \"@ttt-productions/chat-react/styles\".\n\n// React-coupled config + render types (previously on chat-core root).\nexport type {\n ChatCoreConfig,\n ChatTransportMode,\n ChatRealtimeTransportConfig,\n ChatMentionConfig,\n MessageRenderer,\n MessageRendererRegistry,\n RenderableMentionProvider,\n MentionResultRenderer,\n} from \"./types.js\";\n\n// Name-resolver context.\nexport type { ChatNameResolverProviderProps } from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useResolvedSenderName,\n} from \"./context/ChatNameResolverContext.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\nexport { canAccessThread } from \"./hooks/useChatThreadAccess.js\";\n\n// Realtime (Cloudflare Durable Object) transport — the client half of the chat\n// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the\n// firestore transport stays the unchanged default. See docs/packages/chat-react.md.\nexport {\n createRealtimeChatClient,\n createInboxClient,\n ChannelClient,\n InboxClient,\n useRealtimeChatMessages,\n browserSocketFactory,\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n CHAT_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n ChannelRefTuple,\n WireMessageRow,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n} from \"./realtime/index.js\";\n\n// TODO (re-export when a consumer needs them): the moderation-overlay helpers\n// `RevisionKind`, `ModerationOverlay`, `applyModerationOverlay`, `overlayFromRow`,\n// and `MODERATION_REDACTED_TEXT` live in `./realtime` but are intentionally NOT\n// re-exported from this package root yet — the consuming app's renderer reads moderation\n// state off `message.meta` (the realtime client applies the overlay internally), so\n// nothing imports them from here today. Add them to the block above when a consumer\n// genuinely needs to apply the overlay directly. (Comment-only; rides the next publish.)\n\nexport type { ChatShellProps } from \"./ui/ChatShell.js\";\nexport { ChatShell } from \"./ui/ChatShell.js\";\nexport { MessageList } from \"./ui/MessageList.js\";\nexport type { ComposerProps } from \"./ui/Composer.js\";\nexport { Composer } from \"./ui/Composer.js\";\nexport type { MessageItemDefaultProps } from \"./ui/MessageItemDefault.js\";\nexport { MessageItemDefault } from \"./ui/MessageItemDefault.js\";\nexport { MessageActions, ThreadActions } from \"./ui/menus.js\";\nexport { MessageText, type MessageTextProps } from \"./mentions/MessageText.js\";\nexport {\n MentionAutocomplete,\n type MentionAutocompleteProps,\n} from \"./mentions/MentionAutocomplete.js\";\nexport {\n useMentionAutocomplete,\n type UseMentionAutocompleteArgs,\n type UseMentionAutocompleteResult,\n type AutocompleteState,\n type AutocompleteResultGroup,\n} from \"./mentions/use-mention-autocomplete.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAmBb,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,+EAA+E;AAC/E,kFAAkF;AAClF,oFAAoF;AACpF,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,6BAA6B,GAC9B,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,qBAAqB,CAAC","sourcesContent":["\"use client\";\n\n// @ttt-productions/chat-react — chat React UI, hooks, the Firebase-client\n// adapter config types, and React render types. Depends on the pure\n// @ttt-productions/chat-core for contracts, parser, and grouping.\n//\n// Apps import chat CSS via \"@ttt-productions/chat-react/styles\".\n\n// React-coupled config + render types (previously on chat-core root).\nexport type {\n ChatCoreConfig,\n ChatTransportMode,\n ChatRealtimeTransportConfig,\n MessageRenderer,\n MessageRendererRegistry,\n} from \"./types.js\";\n\n// Name-resolver context.\nexport type { ChatNameResolverProviderProps } from \"./context/ChatNameResolverContext.js\";\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useResolvedSenderName,\n} from \"./context/ChatNameResolverContext.js\";\n\nexport type { UseChatMessagesResult } from \"./hooks/useChatMessages.js\";\nexport { useChatMessages } from \"./hooks/useChatMessages.js\";\nexport { canAccessThread } from \"./hooks/useChatThreadAccess.js\";\n\n// Realtime (Cloudflare Durable Object) transport — the client half of the chat\n// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the\n// firestore transport stays the unchanged default. See docs/packages/chat-react.md.\nexport {\n createRealtimeChatClient,\n createInboxClient,\n ChannelClient,\n InboxClient,\n useRealtimeChatMessages,\n browserSocketFactory,\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n CHAT_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n ChannelClientState,\n ChannelClientConfig,\n InboxClientState,\n InboxClientConfig,\n UseRealtimeChatMessagesResult,\n GrantProvider,\n TransportTimers,\n RealtimeStatus,\n RealtimeSocket,\n SocketFactory,\n SocketHandlers,\n ChannelRefTuple,\n WireMessageRow,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n} from \"./realtime/index.js\";\n\n// TODO (re-export when a consumer needs them): the moderation-overlay helpers\n// `RevisionKind`, `ModerationOverlay`, `applyModerationOverlay`, `overlayFromRow`,\n// and `MODERATION_REDACTED_TEXT` live in `./realtime` but are intentionally NOT\n// re-exported from this package root yet — the consuming app's renderer reads moderation\n// state off `message.meta` (the realtime client applies the overlay internally), so\n// nothing imports them from here today. Add them to the block above when a consumer\n// genuinely needs to apply the overlay directly. (Comment-only; rides the next publish.)\n\nexport type { ChatShellProps } from \"./ui/ChatShell.js\";\nexport { ChatShell } from \"./ui/ChatShell.js\";\nexport { MessageList } from \"./ui/MessageList.js\";\nexport type { ComposerProps } from \"./ui/Composer.js\";\nexport { Composer } from \"./ui/Composer.js\";\nexport type { MessageItemDefaultProps } from \"./ui/MessageItemDefault.js\";\nexport { MessageItemDefault } from \"./ui/MessageItemDefault.js\";\nexport { MessageActions, ThreadActions } from \"./ui/menus.js\";\nexport { MessageText, type MessageTextProps } from \"./ui/MessageText.js\";\n"]}
@@ -73,6 +73,13 @@ export declare class ChannelClient {
73
73
  private reauthAttempted;
74
74
  private presenceSubscribed;
75
75
  private heartbeatTimer;
76
+ /**
77
+ * When the CURRENT socket last proved it was alive: set on open, refreshed by
78
+ * every successfully parsed inbound frame (`heartbeat-ack` included — that IS
79
+ * the DO's liveness answer). Null whenever no socket is live, so the watchdog
80
+ * can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.
81
+ */
82
+ private lastInboundAt;
76
83
  private typingTimers;
77
84
  private lastTypingSentAt;
78
85
  private reconnectTimer;
@@ -275,10 +282,6 @@ export declare class ChannelClient {
275
282
  send(args: {
276
283
  clientMessageId: string;
277
284
  text: string;
278
- replyTo?: {
279
- messageSeq: number;
280
- preview: string;
281
- } | null;
282
285
  }): boolean;
283
286
  /**
284
287
  * Explicit read ack (delivery != read). `focused` => the channel is open + at latest.
@@ -296,6 +299,20 @@ export declare class ChannelClient {
296
299
  private requestHistory;
297
300
  private startHeartbeat;
298
301
  private stopHeartbeat;
302
+ /** True when the live socket has produced no parseable frame for too long. */
303
+ private isInboundSilent;
304
+ /**
305
+ * The channel DO auto-acks every heartbeat, so silence past
306
+ * {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no
307
+ * `onclose` ever fired (the half-open-connection case that otherwise strands
308
+ * the UI on a socket that will never deliver another message).
309
+ *
310
+ * Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close
311
+ * lands on `onClose`'s ordinary transient path — which already owns the
312
+ * reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing
313
+ * about reconnect policy is duplicated here.
314
+ */
315
+ private closeDeadSocket;
299
316
  /** Permanently close this client (auth-user switch / unmount). Idempotent. */
300
317
  close(): void;
301
318
  }
@@ -1 +1 @@
1
- {"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAahE,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AASjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAKL,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AA8C1B,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,6DAA6D;IAC7D,eAAe,EAAE,OAAO,CAAC;IACzB,2FAA2F;IAC3F,QAAQ,EAAE,OAAO,CAAC;IAClB,kGAAkG;IAClG,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;;;;;;OASG;IACH,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,aAAa,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,6DAA6D;IAC7D,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE,CAAC;IAC9G;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,2BAA2B,CAAC;CAC3C;AAyDD,qBAAa,aAAa;IAgEZ,OAAO,CAAC,QAAQ,CAAC,MAAM;IA/DnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8C;IAExE,wFAAwF;IACxF,OAAO,CAAC,SAAS,CAAK;IACtB;2CACuC;IACvC,OAAO,CAAC,WAAW,CAAsD;IACzE,iGAAiG;IACjG,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,cAAc,CAA0D;IAChF;;;;;OAKG;IACH,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,YAAY,CAAgE;IACpF,OAAO,CAAC,gBAAgB,CAA4B;IACpD,OAAO,CAAC,cAAc,CAA0D;IAChF,iFAAiF;IACjF,OAAO,CAAC,oBAAoB,CAA0D;IACtF,OAAO,CAAC,UAAU,CAAS;IAC3B;;;;;;qCAMiC;IACjC,OAAO,CAAC,QAAQ,CAAwC;IACxD;;;sFAGkF;IAClF,OAAO,CAAC,YAAY,CAAoC;IACxD;;;;;;;wDAOoD;IACpD,OAAO,CAAC,YAAY,CAAkC;IACtD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,4EAA4E;IAC5E,OAAO,CAAC,cAAc,CAAK;IAC3B,uEAAuE;IACvE,OAAO,CAAC,cAAc,CAA6E;IACnG,gFAAgF;IAChF,OAAO,CAAC,aAAa,CAAuB;gBAEf,MAAM,EAAE,mBAAmB;IAQxD,QAAQ,IAAI,kBAAkB;IAI9B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI;IAK1D,OAAO,CAAC,QAAQ;IAOV,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YA+BhB,UAAU;IA0CxB,OAAO,CAAC,MAAM;IAgCd;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA8C1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,eAAe;IAkCvB,gEAAgE;IAChE,OAAO,CAAC,mBAAmB;IAO3B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAMxB,oFAAoF;IACpF,OAAO,CAAC,8BAA8B;IAStC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAqDzB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IA8CzB,4FAA4F;IAC5F,OAAO,CAAC,eAAe;IAQvB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAM3B;;;;;;;;OAQG;IACH,SAAS,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO;IAwC3C,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,OAAO;IAoCf,OAAO,CAAC,iBAAiB;IAuBzB;;;;;;;;OAQG;IACH,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,SAAS;IA4EjB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAwC1B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAOxB,kFAAkF;IAClF,OAAO,CAAC,SAAS;IAOjB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,oBAAoB;IAgE5B;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IA2BpB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAerB;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,QAAQ;IAoChB,OAAO,CAAC,gBAAgB;IAgDxB,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,SAAS;IAMjB;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE;QAAE,eAAe,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAgD9D;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;IAKnD,yEAAyE;IACzE,MAAM,IAAI,IAAI;IAOd,iBAAiB,IAAI,IAAI;IAKzB,mBAAmB,IAAI,IAAI;IAM3B,2FAA2F;IAC3F,UAAU,IAAI,IAAI;IAQlB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,aAAa;IAOrB,8EAA8E;IAC9E,OAAO,CAAC,eAAe;IAKvB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,eAAe;IAevB,8EAA8E;IAC9E,KAAK,IAAI,IAAI;CA4Bd"}
@@ -18,7 +18,7 @@ import { createReconnectController } from '@ttt-productions/realtime-core';
18
18
  import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, ChatSendRejectedPayloadSchema, } from './wire.js';
19
19
  import { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, } from './map.js';
20
20
  import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
21
- import { defaultTimers, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
21
+ import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, SERVER_RETRYABLE_MAX_AGE_MS, SEND_RETRY_MIN_DELAY_MS, SEND_RETRY_MAX_DELAY_MS, } from './shared.js';
22
22
  const INITIAL = {
23
23
  status: 'idle',
24
24
  messages: [],
@@ -39,6 +39,37 @@ const INITIAL = {
39
39
  const MEMBERSHIP_PENDING_CODE = 'membership-pending';
40
40
  /** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */
41
41
  const MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;
42
+ /**
43
+ * Dead-socket watchdog: how long inbound silence may last before this client
44
+ * treats the socket as dead and closes it locally.
45
+ *
46
+ * CHANNEL-ONLY, and this is why it is sound here: the channel DO answers every
47
+ * 20 s client `heartbeat` through the hibernation AUTO-RESPONSE API, so a healthy
48
+ * channel socket is NEVER silent for a full heartbeat window. Inbound silence past
49
+ * this threshold is therefore provable death (a half-open TCP connection whose
50
+ * `onclose` never fires), not idleness. The INBOX socket has no such auto-response
51
+ * and a healthy idle inbox is legitimately silent for hours — applying this rule
52
+ * there would endlessly reconnect healthy sockets, so it is deliberately absent
53
+ * from `inbox-client.ts`.
54
+ *
55
+ * PRIVATE CLIENT POLICY, not a wire constant: nothing on the worker side agrees to
56
+ * it, so it does not belong in `@ttt-productions/chat-schemas` or in this package's
57
+ * exported surface. The check rides the existing 20 s heartbeat tick (no second
58
+ * timer), so detection lands on the NEXT tick after the deadline rather than at
59
+ * exactly 50 s — intended: 2.5 missed windows is unambiguous, and it costs no
60
+ * extra timer.
61
+ */
62
+ const INBOUND_SILENCE_LIMIT_MS = 50_000;
63
+ /**
64
+ * Close code used for the watchdog's LOCAL close. A browser `WebSocket.close()`
65
+ * accepts only 1000 or 3000–4999, and the private 4xxx range is the chat wire's
66
+ * (`CHAT_CLOSE_CODES`) — a client-invented code there could collide with a future
67
+ * server code. 1000 keeps the close frame valid and, being neither 4401 nor 4403,
68
+ * routes through `onClose`'s ordinary transient path, which owns reconnect,
69
+ * backoff, and resume. The reason string + the liveness diagnostic are what
70
+ * identify it.
71
+ */
72
+ const WATCHDOG_CLOSE_CODE = 1000;
42
73
  export class ChannelClient {
43
74
  config;
44
75
  timers;
@@ -55,6 +86,13 @@ export class ChannelClient {
55
86
  reauthAttempted = false;
56
87
  presenceSubscribed = false;
57
88
  heartbeatTimer = null;
89
+ /**
90
+ * When the CURRENT socket last proved it was alive: set on open, refreshed by
91
+ * every successfully parsed inbound frame (`heartbeat-ack` included — that IS
92
+ * the DO's liveness answer). Null whenever no socket is live, so the watchdog
93
+ * can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.
94
+ */
95
+ lastInboundAt = null;
58
96
  typingTimers = new Map();
59
97
  lastTypingSentAt = Number.NEGATIVE_INFINITY;
60
98
  reconnectTimer = null;
@@ -124,6 +162,23 @@ export class ChannelClient {
124
162
  // legitimate reconnect.
125
163
  if (this.state.status !== 'idle' && this.state.status !== 'closed')
126
164
  return;
165
+ // REVIVE the controller. `controller.close()` is PERMANENT: while it is in
166
+ // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`
167
+ // returns null. So without this reset a close() → connect() client opens a
168
+ // socket and works normally until its FIRST transient close, at which point
169
+ // `scheduleReconnect()` gets null and parks it terminally in `closed` with
170
+ // reconnect disabled and no surfaced error code. The idempotency guard above
171
+ // is what keeps this from disturbing a live lifecycle: only an `idle` or
172
+ // fully torn-down (`closed`) client ever reaches here.
173
+ this.controller.reset();
174
+ // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN
175
+ // its lifecycle; an explicit connect() is a fresh attempt the authority may
176
+ // legitimately re-deny, so the previous lifecycle's code must not persist into
177
+ // this one (the hook maps `access-denied` to `allowed: false`). Loaded
178
+ // messages and the initial-load flag are deliberately KEPT — a lifecycle
179
+ // restart must not churn the UI back to an empty opening state.
180
+ if (isTerminalErrorCode(this.state.lastErrorCode))
181
+ this.setState({ lastErrorCode: null });
127
182
  this.closedByUs = false;
128
183
  this.reconnectCause = 'initial';
129
184
  this.controller.start();
@@ -159,6 +214,9 @@ export class ChannelClient {
159
214
  }
160
215
  if (this.closedByUs)
161
216
  return;
217
+ // Socket REPLACEMENT: the previous socket's liveness record dies with it, so
218
+ // the watchdog never judges a new socket against an old deadline.
219
+ this.lastInboundAt = null;
162
220
  const url = `${this.config.endpoint.replace(/\/$/, '')}/channel`;
163
221
  this.socket = this.config.socketFactory({
164
222
  url,
@@ -174,6 +232,9 @@ export class ChannelClient {
174
232
  onOpen() {
175
233
  this.controller.onOpen();
176
234
  this.reauthAttempted = false;
235
+ // Start the liveness clock at open — the first heartbeat window is measured
236
+ // from here, not from a stale previous socket.
237
+ this.lastInboundAt = this.timers.now();
177
238
  this.setState({ status: 'open' });
178
239
  this.diag?.(DIAG.SOCKET_OPEN, {
179
240
  attempt: this.connectAttempt,
@@ -243,7 +304,6 @@ export class ChannelClient {
243
304
  const sent = this.sendFrame(CLIENT_FRAME.SEND, {
244
305
  clientMessageId: pending.clientMessageId,
245
306
  text: pending.text,
246
- replyTo: pending.replyTo,
247
307
  });
248
308
  if (sent)
249
309
  pending.attempts += 1;
@@ -418,7 +478,6 @@ export class ChannelClient {
418
478
  const sent = this.sendFrame(CLIENT_FRAME.SEND, {
419
479
  clientMessageId: current.clientMessageId,
420
480
  text: current.text,
421
- replyTo: current.replyTo,
422
481
  });
423
482
  this.diag?.(DIAG.SEND_RETRY, {
424
483
  clientMessageId: current.clientMessageId,
@@ -487,7 +546,6 @@ export class ChannelClient {
487
546
  const sent = this.sendFrame(CLIENT_FRAME.SEND, {
488
547
  clientMessageId: pending.clientMessageId,
489
548
  text: pending.text,
490
- replyTo: pending.replyTo,
491
549
  });
492
550
  pending.attempts = sent ? 1 : 0;
493
551
  this.diag?.(DIAG.SEND_RETRY, {
@@ -515,6 +573,7 @@ export class ChannelClient {
515
573
  onClose(code, _reason) {
516
574
  this.stopHeartbeat();
517
575
  this.socket = null;
576
+ this.lastInboundAt = null;
518
577
  this.lastCloseCode = code;
519
578
  if (this.closedByUs) {
520
579
  this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed', resumeSeq: this.resumeSeq });
@@ -539,7 +598,7 @@ export class ChannelClient {
539
598
  this.controller.close();
540
599
  this.diag?.(DIAG.SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked', resumeSeq: this.resumeSeq });
541
600
  this.failAllPendingSends();
542
- this.setState({ status: 'closed', lastErrorCode: 'revoked' });
601
+ this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
543
602
  return;
544
603
  }
545
604
  this.reconnectCause = 'transient-close';
@@ -582,15 +641,21 @@ export class ChannelClient {
582
641
  this.closedByUs = true;
583
642
  this.controller.close();
584
643
  this.failAllPendingSends();
585
- this.setState({ status: 'closed', lastErrorCode: 'access-denied' });
644
+ this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });
586
645
  }
587
646
  // ---- inbound frames ----
588
647
  onMessage(data) {
589
648
  const frame = parseFrame(data);
590
649
  if (!frame || !frame.type) {
650
+ // Unparseable: bytes arrived, but nothing proves they came from a DO
651
+ // speaking this protocol, so this must NOT refresh the liveness deadline.
591
652
  this.diag?.(DIAG.FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });
592
653
  return;
593
654
  }
655
+ // A well-formed envelope IS the liveness proof — including the `heartbeat-ack`
656
+ // the DO auto-responds with, and a forward-compat type this build does not
657
+ // handle yet (a newer DO's frames must never be read as a dead socket).
658
+ this.lastInboundAt = this.timers.now();
594
659
  const payload = frame.payload ?? {};
595
660
  switch (frame.type) {
596
661
  case 'message':
@@ -988,7 +1053,6 @@ export class ChannelClient {
988
1053
  const sent = this.sendFrame(CLIENT_FRAME.SEND, {
989
1054
  clientMessageId: args.clientMessageId,
990
1055
  text: args.text,
991
- replyTo: args.replyTo ?? null,
992
1056
  });
993
1057
  if (!sent) {
994
1058
  this.diag?.(DIAG.SEND_FAILED, {
@@ -1007,7 +1071,6 @@ export class ChannelClient {
1007
1071
  this.pendingSends.set(args.clientMessageId, {
1008
1072
  clientMessageId: args.clientMessageId,
1009
1073
  text: args.text,
1010
- replyTo: args.replyTo ?? null,
1011
1074
  sentAt: now,
1012
1075
  attempts: 1,
1013
1076
  failed: false,
@@ -1020,14 +1083,11 @@ export class ChannelClient {
1020
1083
  senderId: this.config.currentUserId,
1021
1084
  text: args.text,
1022
1085
  createdAt: now,
1023
- replyTo: args.replyTo ? { messageId: seqToMessageId(args.replyTo.messageSeq), messagePreview: args.replyTo.preview } : null,
1024
1086
  });
1025
1087
  const next = [...this.state.messages, echo].sort(sortBySeq);
1026
1088
  this.setState({ messages: next, lastErrorCode: null });
1027
1089
  this.diag?.(DIAG.SEND_OPTIMISTIC, {
1028
1090
  clientMessageId: args.clientMessageId,
1029
- hasReplyTo: Boolean(args.replyTo),
1030
- replyToSeq: args.replyTo?.messageSeq ?? null,
1031
1091
  pendingSends: this.pendingSends.size,
1032
1092
  });
1033
1093
  return true;
@@ -1072,12 +1132,17 @@ export class ChannelClient {
1072
1132
  const sent = this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });
1073
1133
  this.diag?.(DIAG.HISTORY_REQUEST, { beforeSeq, limit: HISTORY_PAGE_MAX, sent });
1074
1134
  }
1075
- // ---- heartbeat (20s) ----
1135
+ // ---- heartbeat (20s) + dead-socket watchdog ----
1076
1136
  startHeartbeat() {
1077
1137
  this.stopHeartbeat();
1078
1138
  const tick = () => {
1079
1139
  if (!this.socket || !this.socket.isOpen)
1080
1140
  return;
1141
+ // Liveness is checked BEFORE the write: a socket already proven dead must
1142
+ // not be handed another frame. Closing here does not reschedule — onClose
1143
+ // stops the heartbeat and owns what happens next.
1144
+ if (this.isInboundSilent())
1145
+ return this.closeDeadSocket();
1081
1146
  this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});
1082
1147
  this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);
1083
1148
  };
@@ -1089,12 +1154,44 @@ export class ChannelClient {
1089
1154
  this.heartbeatTimer = null;
1090
1155
  }
1091
1156
  }
1157
+ /** True when the live socket has produced no parseable frame for too long. */
1158
+ isInboundSilent() {
1159
+ if (this.lastInboundAt == null)
1160
+ return false;
1161
+ return this.timers.now() - this.lastInboundAt >= INBOUND_SILENCE_LIMIT_MS;
1162
+ }
1163
+ /**
1164
+ * The channel DO auto-acks every heartbeat, so silence past
1165
+ * {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no
1166
+ * `onclose` ever fired (the half-open-connection case that otherwise strands
1167
+ * the UI on a socket that will never deliver another message).
1168
+ *
1169
+ * Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close
1170
+ * lands on `onClose`'s ordinary transient path — which already owns the
1171
+ * reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing
1172
+ * about reconnect policy is duplicated here.
1173
+ */
1174
+ closeDeadSocket() {
1175
+ const socket = this.socket;
1176
+ this.diag?.(DIAG.SOCKET_LIVENESS_TIMEOUT, {
1177
+ silentMs: this.lastInboundAt == null ? null : this.timers.now() - this.lastInboundAt,
1178
+ thresholdMs: INBOUND_SILENCE_LIMIT_MS,
1179
+ attempt: this.connectAttempt,
1180
+ resumeSeq: this.resumeSeq,
1181
+ pendingSends: this.pendingSends.size,
1182
+ });
1183
+ this.lastInboundAt = null;
1184
+ socket?.close(WATCHDOG_CLOSE_CODE, 'client liveness timeout');
1185
+ }
1092
1186
  // ---- teardown ----
1093
1187
  /** Permanently close this client (auth-user switch / unmount). Idempotent. */
1094
1188
  close() {
1095
1189
  this.closedByUs = true;
1096
1190
  this.controller.close();
1097
1191
  this.stopHeartbeat();
1192
+ // The heartbeat timer IS the watchdog's timer — stopping it above ends both.
1193
+ // Clearing the record too means a later connect() starts from a clean deadline.
1194
+ this.lastInboundAt = null;
1098
1195
  for (const t of this.typingTimers.values())
1099
1196
  this.timers.clearTimeout(t);
1100
1197
  this.typingTimers.clear();