@ttt-productions/chat-react 0.13.16 → 0.13.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/context/ChatNameResolverContext.d.ts +0 -23
- package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
- package/dist/context/ChatNameResolverContext.js +0 -16
- package/dist/context/ChatNameResolverContext.js.map +1 -1
- package/dist/hooks/useChatMessages.d.ts +0 -4
- package/dist/hooks/useChatMessages.d.ts.map +1 -1
- package/dist/hooks/useChatMessages.js +0 -10
- package/dist/hooks/useChatMessages.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/realtime/channel-client.d.ts +0 -248
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +5 -432
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/diagnostics.d.ts +0 -62
- package/dist/realtime/diagnostics.d.ts.map +1 -1
- package/dist/realtime/diagnostics.js +4 -89
- package/dist/realtime/diagnostics.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts +0 -71
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +0 -98
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/initial-load-budget.d.ts +1 -16
- package/dist/realtime/initial-load-budget.d.ts.map +1 -1
- package/dist/realtime/initial-load-budget.js +1 -20
- package/dist/realtime/initial-load-budget.js.map +1 -1
- package/dist/realtime/map.d.ts +0 -26
- package/dist/realtime/map.d.ts.map +1 -1
- package/dist/realtime/map.js +0 -33
- package/dist/realtime/map.js.map +1 -1
- package/dist/realtime/shared.d.ts +0 -78
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +2 -76
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/socket.d.ts +0 -15
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/realtime/socket.js +0 -4
- package/dist/realtime/socket.js.map +1 -1
- package/dist/realtime/transport.d.ts +0 -23
- package/dist/realtime/transport.d.ts.map +1 -1
- package/dist/realtime/transport.js +0 -13
- package/dist/realtime/transport.js.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.d.ts +0 -17
- package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.js +0 -22
- package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
- package/dist/realtime/wire.d.ts +0 -56
- package/dist/realtime/wire.d.ts.map +1 -1
- package/dist/realtime/wire.js +0 -16
- package/dist/realtime/wire.js.map +1 -1
- package/dist/types.d.ts +0 -39
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/dist/ui/ChatShell.d.ts +0 -42
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +4 -41
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/Composer.d.ts +0 -13
- package/dist/ui/Composer.d.ts.map +1 -1
- package/dist/ui/Composer.js +0 -13
- package/dist/ui/Composer.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts +0 -5
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +2 -34
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/dist/ui/MessageList.d.ts +0 -24
- package/dist/ui/MessageList.d.ts.map +1 -1
- package/dist/ui/MessageList.js +4 -14
- package/dist/ui/MessageList.js.map +1 -1
- package/dist/ui/MessageText.d.ts +0 -13
- package/dist/ui/MessageText.d.ts.map +1 -1
- package/dist/ui/MessageText.js.map +1 -1
- package/dist/ui/SenderName.d.ts +0 -5
- package/dist/ui/SenderName.d.ts.map +1 -1
- package/dist/ui/SenderName.js +0 -5
- package/dist/ui/SenderName.js.map +1 -1
- package/dist/ui/menus.d.ts +0 -2
- package/dist/ui/menus.d.ts.map +1 -1
- package/dist/ui/menus.js +6 -10
- package/dist/ui/menus.js.map +1 -1
- package/package.json +10 -10
- package/src/styles/chat.css +2 -15
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @ttt-productions/chat-react
|
|
2
2
|
|
|
3
|
-
Chat **React UI** for
|
|
3
|
+
Chat **React UI** for consuming apps: chat shell, composer, message list,
|
|
4
4
|
the name-resolver context, and the realtime-newest-window + infinite-older
|
|
5
|
-
hooks. Chat is PLAIN-TEXT-only — a conversation
|
|
6
|
-
app's
|
|
5
|
+
hooks. Chat is PLAIN-TEXT-only — files an app attaches to a conversation are the
|
|
6
|
+
app's own surface, never in the message timeline, and message text
|
|
7
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
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { ChatNameResolution, ChatNameResolver, ChatPrewarmSenders } from '@ttt-productions/chat-core';
|
|
3
|
-
/** A sender name the app could not resolve: pending, unavailable, or failed. */
|
|
4
3
|
export type UnresolvedChatName = Exclude<ChatNameResolution, {
|
|
5
4
|
status: 'resolved';
|
|
6
5
|
}>;
|
|
@@ -12,34 +11,12 @@ type ChatNameResolverContextValue = {
|
|
|
12
11
|
export type ChatNameResolverProviderProps = {
|
|
13
12
|
resolveName: ChatNameResolver;
|
|
14
13
|
prewarm?: ChatPrewarmSenders;
|
|
15
|
-
/**
|
|
16
|
-
* Renders a sender whose name is not resolved — the app's own placeholder while the
|
|
17
|
-
* read is pending, its unavailable state when there is no person to name, and its
|
|
18
|
-
* load-error state (with `retry`) when the read failed. The chat UI never writes a
|
|
19
|
-
* stand-in name of its own; without this slot an unresolved sender renders nothing.
|
|
20
|
-
*/
|
|
21
14
|
renderUnresolvedName?: (resolution: UnresolvedChatName) => React.ReactNode;
|
|
22
15
|
children: React.ReactNode;
|
|
23
16
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Wraps any chat-react consumer (typically the whole app shell) and provides the name
|
|
26
|
-
* resolver. Required for any tree that renders <ChatShell>, <MessageList>, or
|
|
27
|
-
* <MessageItemDefault>.
|
|
28
|
-
*/
|
|
29
17
|
export declare function ChatNameResolverProvider(props: ChatNameResolverProviderProps): React.JSX.Element;
|
|
30
|
-
/**
|
|
31
|
-
* Strict hook — throws if no provider is wrapped. Used inside this package's own
|
|
32
|
-
* renderers so a missing setup surfaces on first render instead of rendering a sender
|
|
33
|
-
* with no name.
|
|
34
|
-
*/
|
|
35
18
|
export declare function useChatNameResolver(): ChatNameResolverContextValue;
|
|
36
|
-
/**
|
|
37
|
-
* Optional variant — returns null instead of throwing when no provider is wrapped.
|
|
38
|
-
* Used internally by the message hooks so headless callers (tests, data-only consumers
|
|
39
|
-
* that render no UI) can still use them.
|
|
40
|
-
*/
|
|
41
19
|
export declare function useOptionalChatNameResolver(): ChatNameResolverContextValue | null;
|
|
42
|
-
/** What the app knows about a sender's name right now. Re-renders as the app's cache settles. */
|
|
43
20
|
export declare function useSenderNameResolution(senderId: string): ChatNameResolution;
|
|
44
21
|
export {};
|
|
45
22
|
//# sourceMappingURL=ChatNameResolverContext.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatNameResolverContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatNameResolverContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,
|
|
1
|
+
{"version":3,"file":"ChatNameResolverContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatNameResolverContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,EAAE;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC,CAAC;AAErF,KAAK,4BAA4B,GAAG;IAClC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,KAAK,CAAC,SAAS,CAAC;CAC5E,CAAC;AAIF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,WAAW,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,kBAAkB,KAAK,KAAK,CAAC,SAAS,CAAC;IAC3E,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,qBAS5E;AAED,wBAAgB,mBAAmB,IAAI,4BAA4B,CAQlE;AAED,wBAAgB,2BAA2B,IAAI,4BAA4B,GAAG,IAAI,CAEjF;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAG5E"}
|
|
@@ -2,21 +2,11 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
const ChatNameResolverContext = React.createContext(null);
|
|
5
|
-
/**
|
|
6
|
-
* Wraps any chat-react consumer (typically the whole app shell) and provides the name
|
|
7
|
-
* resolver. Required for any tree that renders <ChatShell>, <MessageList>, or
|
|
8
|
-
* <MessageItemDefault>.
|
|
9
|
-
*/
|
|
10
5
|
export function ChatNameResolverProvider(props) {
|
|
11
6
|
const { resolveName, prewarm, renderUnresolvedName, children } = props;
|
|
12
7
|
const value = React.useMemo(() => ({ resolveName, prewarm, renderUnresolvedName }), [resolveName, prewarm, renderUnresolvedName]);
|
|
13
8
|
return (_jsx(ChatNameResolverContext.Provider, { value: value, children: children }));
|
|
14
9
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Strict hook — throws if no provider is wrapped. Used inside this package's own
|
|
17
|
-
* renderers so a missing setup surfaces on first render instead of rendering a sender
|
|
18
|
-
* with no name.
|
|
19
|
-
*/
|
|
20
10
|
export function useChatNameResolver() {
|
|
21
11
|
const ctx = React.useContext(ChatNameResolverContext);
|
|
22
12
|
if (!ctx) {
|
|
@@ -24,15 +14,9 @@ export function useChatNameResolver() {
|
|
|
24
14
|
}
|
|
25
15
|
return ctx;
|
|
26
16
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Optional variant — returns null instead of throwing when no provider is wrapped.
|
|
29
|
-
* Used internally by the message hooks so headless callers (tests, data-only consumers
|
|
30
|
-
* that render no UI) can still use them.
|
|
31
|
-
*/
|
|
32
17
|
export function useOptionalChatNameResolver() {
|
|
33
18
|
return React.useContext(ChatNameResolverContext);
|
|
34
19
|
}
|
|
35
|
-
/** What the app knows about a sender's name right now. Re-renders as the app's cache settles. */
|
|
36
20
|
export function useSenderNameResolution(senderId) {
|
|
37
21
|
const { resolveName } = useChatNameResolver();
|
|
38
22
|
return resolveName(senderId);
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"ChatNameResolverContext.js","sourceRoot":"","sources":["../../src/context/ChatNameResolverContext.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,MAAM,uBAAuB,GAAG,KAAK,CAAC,aAAa,CAAsC,IAAI,CAAC,CAAC;AAS/F,MAAM,UAAU,wBAAwB,CAAC,KAAoC;IAC3E,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACvE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CACzB,GAAG,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC,EACtD,CAAC,WAAW,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAC7C,CAAC;IACF,OAAO,CACL,KAAC,uBAAuB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAoC,CAC9F,CAAC;AACJ,CAAC;AAED,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,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,OAAO,KAAK,CAAC,UAAU,CAAC,uBAAuB,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,MAAM,EAAE,WAAW,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAC9C,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC","sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport type {\n ChatNameResolution,\n ChatNameResolver,\n ChatPrewarmSenders,\n} from '@ttt-productions/chat-core';\n\nexport type UnresolvedChatName = Exclude<ChatNameResolution, { status: 'resolved' }>;\n\ntype ChatNameResolverContextValue = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n renderUnresolvedName?: (resolution: UnresolvedChatName) => React.ReactNode;\n};\n\nconst ChatNameResolverContext = React.createContext<ChatNameResolverContextValue | null>(null);\n\nexport type ChatNameResolverProviderProps = {\n resolveName: ChatNameResolver;\n prewarm?: ChatPrewarmSenders;\n renderUnresolvedName?: (resolution: UnresolvedChatName) => React.ReactNode;\n children: React.ReactNode;\n};\n\nexport function ChatNameResolverProvider(props: ChatNameResolverProviderProps) {\n const { resolveName, prewarm, renderUnresolvedName, children } = props;\n const value = React.useMemo(\n () => ({ resolveName, prewarm, renderUnresolvedName }),\n [resolveName, prewarm, renderUnresolvedName],\n );\n return (\n <ChatNameResolverContext.Provider value={value}>{children}</ChatNameResolverContext.Provider>\n );\n}\n\nexport function useChatNameResolver(): ChatNameResolverContextValue {\n const ctx = React.useContext(ChatNameResolverContext);\n if (!ctx) {\n throw new Error(\n '[chat-react] ChatNameResolverProvider is required. Wrap your ChatShell tree with <ChatNameResolverProvider>.',\n );\n }\n return ctx;\n}\n\nexport function useOptionalChatNameResolver(): ChatNameResolverContextValue | null {\n return React.useContext(ChatNameResolverContext);\n}\n\nexport function useSenderNameResolution(senderId: string): ChatNameResolution {\n const { resolveName } = useChatNameResolver();\n return resolveName(senderId);\n}\n"]}
|
|
@@ -8,13 +8,9 @@ export type UseChatMessagesResult = {
|
|
|
8
8
|
fetchOlder: () => Promise<void>;
|
|
9
9
|
hasOlder: boolean;
|
|
10
10
|
isFetchingOlder: boolean;
|
|
11
|
-
/** The live listener's state: `connecting` while it (re)subscribes, `error` once it failed. */
|
|
12
11
|
sourceState: FirestoreSourceState;
|
|
13
|
-
/** The live listener's failure; the messages already shown stay in `messages`. */
|
|
14
12
|
error: Error | null;
|
|
15
|
-
/** The last older-page read failed; the list stops asking for older pages until `retry`. */
|
|
16
13
|
olderError: Error | null;
|
|
17
|
-
/** Re-run what failed: resubscribe a failed listener, re-read a failed older page. */
|
|
18
14
|
retry: () => void;
|
|
19
15
|
};
|
|
20
16
|
export declare function useChatMessages(config: ChatCoreConfig): UseChatMessagesResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatMessages.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"useChatMessages.d.ts","sourceRoot":"","sources":["../../src/hooks/useChatMessages.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMlD,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;IACzB,WAAW,EAAE,oBAAoB,CAAC;IAClC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,KAAK,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AA8BF,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,qBAAqB,CAyF7E"}
|
|
@@ -4,7 +4,6 @@ import { toMillis } from '@ttt-productions/firebase-helpers';
|
|
|
4
4
|
import { useFirestoreLiveInfinite } from '@ttt-productions/query-core/react';
|
|
5
5
|
import { messagesColPath } from '../firestore/queries.js';
|
|
6
6
|
import { useOptionalChatNameResolver } from '../context/ChatNameResolverContext.js';
|
|
7
|
-
// Safety limit to prevent memory exhaustion
|
|
8
7
|
const MAX_PAGE_SIZE = 100;
|
|
9
8
|
function mapMsg(data, threadId, messageMeta) {
|
|
10
9
|
const d = data;
|
|
@@ -12,9 +11,6 @@ function mapMsg(data, threadId, messageMeta) {
|
|
|
12
11
|
if (!createdAt) {
|
|
13
12
|
console.error('[useChatMessages] Message missing valid createdAt:', data.id, d.createdAt);
|
|
14
13
|
}
|
|
15
|
-
// A stored `replyTo` (if any legacy doc still carries one) is deliberately NOT
|
|
16
|
-
// mapped: chat has no reply-authoring affordance, so the UI has no reply
|
|
17
|
-
// renderer to feed.
|
|
18
14
|
const appMeta = messageMeta?.(data);
|
|
19
15
|
return {
|
|
20
16
|
messageId: data.id,
|
|
@@ -25,8 +21,6 @@ function mapMsg(data, threadId, messageMeta) {
|
|
|
25
21
|
type: d.type,
|
|
26
22
|
isSystemMessage: d.isSystemMessage ?? undefined,
|
|
27
23
|
referencedUids: Array.isArray(d.referencedUids) ? d.referencedUids : undefined,
|
|
28
|
-
// Moderation tombstone flag (backend-written) — carried through so renderers
|
|
29
|
-
// can tombstone the content.
|
|
30
24
|
hidden: d.hidden ?? undefined,
|
|
31
25
|
meta: appMeta ? { ...d.meta, ...appMeta } : d.meta,
|
|
32
26
|
};
|
|
@@ -37,9 +31,7 @@ export function useChatMessages(config) {
|
|
|
37
31
|
if (requestedPageSize > MAX_PAGE_SIZE) {
|
|
38
32
|
console.warn(`[useChatMessages] Requested pageSize ${requestedPageSize} exceeds maximum ${MAX_PAGE_SIZE}. Using ${MAX_PAGE_SIZE}.`);
|
|
39
33
|
}
|
|
40
|
-
// Stable key segment for the message collection path (string | string[]).
|
|
41
34
|
const pathKey = React.useMemo(() => JSON.stringify(Array.isArray(chatCollectionPath) ? chatCollectionPath : [chatCollectionPath]), [chatCollectionPath]);
|
|
42
|
-
// Slash-joined collection path for the generic hook (handles nested paths).
|
|
43
35
|
const collectionPath = React.useMemo(() => messagesColPath(chatCollectionPath, threadId, messagesSubcollection).join('/'), [chatCollectionPath, threadId, messagesSubcollection]);
|
|
44
36
|
const { items, isInitialLoading, fetchOlder, hasOlder, isFetchingOlder, sourceState, error, olderError, retry, } = useFirestoreLiveInfinite({
|
|
45
37
|
collectionPath,
|
|
@@ -59,8 +51,6 @@ export function useChatMessages(config) {
|
|
|
59
51
|
select: (data) => mapMsg(data, threadId, messageMeta),
|
|
60
52
|
});
|
|
61
53
|
const messages = React.useMemo(() => (allowed ? items : []), [allowed, items]);
|
|
62
|
-
// Pre-warm name cache for all visible senders.
|
|
63
|
-
// Optional resolver: silent no-op if no provider is wrapped.
|
|
64
54
|
const resolverCtx = useOptionalChatNameResolver();
|
|
65
55
|
const senderIds = React.useMemo(() => {
|
|
66
56
|
const set = new Set();
|
|
@@ -1 +1 @@
|
|
|
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;AAI7E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AAEpF,
|
|
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 { FirestoreSourceState } from '@ttt-productions/query-core/types';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport type { ChatCoreConfig } from '../types.js';\nimport { messagesColPath } from '../firestore/queries.js';\nimport { useOptionalChatNameResolver } from '../context/ChatNameResolverContext.js';\n\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 sourceState: FirestoreSourceState;\n error: Error | null;\n olderError: Error | null;\n retry: () => void;\n};\n\nfunction mapMsg(\n data: DocumentData & { id: string },\n threadId: string,\n messageMeta: ChatCoreConfig['messageMeta'],\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 const appMeta = messageMeta?.(data);\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 referencedUids: Array.isArray(d.referencedUids) ? d.referencedUids : undefined,\n hidden: d.hidden ?? undefined,\n meta: appMeta ? { ...d.meta, ...appMeta } : 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 allowed,\n createdAtField = 'createdAt',\n messageMeta,\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 pathKey = React.useMemo(\n () =>\n JSON.stringify(Array.isArray(chatCollectionPath) ? chatCollectionPath : [chatCollectionPath]),\n [chatCollectionPath],\n );\n\n const collectionPath = React.useMemo(\n () => messagesColPath(chatCollectionPath, threadId, messagesSubcollection).join('/'),\n [chatCollectionPath, threadId, messagesSubcollection],\n );\n\n const {\n items,\n isInitialLoading,\n fetchOlder,\n hasOlder,\n isFetchingOlder,\n sourceState,\n error,\n olderError,\n retry,\n } = useFirestoreLiveInfinite<ChatMessageV1>({\n collectionPath,\n queryKey: [\n 'chat-core',\n 'messages',\n pathKey,\n threadId,\n messagesSubcollection,\n pageSize,\n currentUserId,\n ],\n orderByField: createdAtField,\n pageSize,\n enabled: allowed,\n sort: 'asc',\n select: (data) => mapMsg(data, threadId, messageMeta),\n });\n\n const messages = React.useMemo(() => (allowed ? items : []), [allowed, items]);\n\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 for (const uid of m.referencedUids ?? []) set.add(uid);\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 sourceState,\n error: allowed ? error : null,\n olderError: allowed ? olderError : null,\n retry,\n };\n}\n"]}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,GACpC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC3F,YAAY,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAC5F,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,6BAA6B,EAAE,MAAM,uCAAuC,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3F,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC1F,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACpE,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
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
export { ChatNameResolverProvider, useChatNameResolver, useOptionalChatNameResolver, useSenderNameResolution, } from './context/ChatNameResolverContext.js';
|
|
3
3
|
export { SenderName } from './ui/SenderName.js';
|
|
4
4
|
export { useChatMessages } from './hooks/useChatMessages.js';
|
|
5
|
-
// Realtime (Cloudflare Durable Object) transport — the client half of the chat
|
|
6
|
-
// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the
|
|
7
|
-
// firestore transport stays the unchanged default. See docs/packages/chat-react.md.
|
|
8
5
|
export { createRealtimeChatClient, createInboxClient } from './realtime/transport.js';
|
|
9
6
|
export { ChannelClient } from './realtime/channel-client.js';
|
|
10
7
|
export { InboxClient } from './realtime/inbox-client.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAcb,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,mCAAmC,GACpC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAC;AAwB1E,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,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\nexport type {\n ChatCoreConfig,\n ChatTransportMode,\n ChatRealtimeTransportConfig,\n MessageRenderer,\n MessageRendererRegistry,\n} from './types.js';\n\nexport type {\n ChatNameResolverProviderProps,\n UnresolvedChatName,\n} from './context/ChatNameResolverContext.js';\nexport {\n ChatNameResolverProvider,\n useChatNameResolver,\n useOptionalChatNameResolver,\n useSenderNameResolution,\n} from './context/ChatNameResolverContext.js';\nexport { SenderName } from './ui/SenderName.js';\n\nexport type { UseChatMessagesResult } from './hooks/useChatMessages.js';\nexport { useChatMessages } from './hooks/useChatMessages.js';\n\nexport { createRealtimeChatClient, createInboxClient } from './realtime/transport.js';\nexport { ChannelClient } from './realtime/channel-client.js';\nexport { InboxClient } from './realtime/inbox-client.js';\nexport { useRealtimeChatMessages } from './realtime/useRealtimeChatMessages.js';\nexport { browserSocketFactory } from './realtime/socket.js';\nexport {\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS,\n} from './realtime/shared.js';\nexport { buildFrame, parseFrame } from './realtime/wire.js';\nexport { CHAT_CLIENT_DIAGNOSTIC_EVENTS } from './realtime/diagnostics.js';\nexport type { RealtimeChatClient, RealtimeTransportConfig } from './realtime/transport.js';\nexport type {\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n} from './realtime/diagnostics.js';\nexport type { ChannelClientState, ChannelClientConfig } from './realtime/channel-client.js';\nexport type {\n InboxClientState,\n InboxClientConfig,\n ChatMarkReadOutcome,\n} from './realtime/inbox-client.js';\nexport type { UseRealtimeChatMessagesResult } from './realtime/useRealtimeChatMessages.js';\nexport type { GrantProvider, TransportTimers, RealtimeStatus } from './realtime/shared.js';\nexport type { RealtimeSocket, SocketFactory, SocketHandlers } from './realtime/socket.js';\nexport type {\n WireMessageRow,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n} from './realtime/wire.js';\n\nexport type { ChatShellProps, ChatLoadFailure } from './ui/ChatShell.js';\nexport { ChatShell } from './ui/ChatShell.js';\nexport { MessageList } from './ui/MessageList.js';\nexport type { ChatScrollContainerProps } 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"]}
|
|
@@ -4,75 +4,29 @@ import type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js
|
|
|
4
4
|
import { type ChatClientDiagnosticsOption } from './diagnostics.js';
|
|
5
5
|
export interface ChannelClientState {
|
|
6
6
|
status: RealtimeStatus;
|
|
7
|
-
/** Ordered ascending by seq; optimistic messages (no seq yet) sort to the tail. */
|
|
8
7
|
messages: ChatMessageV1[];
|
|
9
|
-
/** uids currently typing (cleared after ~TYPING_COALESCE_MS of silence). */
|
|
10
8
|
typing: string[];
|
|
11
|
-
/** uids the DO reports online (presence). Empty until presence-subscribe. */
|
|
12
9
|
presence: string[];
|
|
13
|
-
/** True while an older-history page request is in flight. */
|
|
14
10
|
isFetchingOlder: boolean;
|
|
15
|
-
/** False once a history page returns fewer than the requested limit (reached the head). */
|
|
16
11
|
hasOlder: boolean;
|
|
17
|
-
/** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */
|
|
18
12
|
lastErrorCode: string | null;
|
|
19
|
-
/**
|
|
20
|
-
* True once the FIRST authoritative chat data has been applied — a non-resync
|
|
21
|
-
* resume snapshot, OR the first history page (INCLUDING an empty one). Drives the
|
|
22
|
-
* hook's `isInitialLoading` so the shell shows an honest "Opening chat…" state
|
|
23
|
-
* while the first data is loading, and a real empty-chat surface only after an
|
|
24
|
-
* authoritative empty result. NEVER inferred from `messages.length === 0` (an
|
|
25
|
-
* empty first history page IS a successful load). Once true it stays true — a
|
|
26
|
-
* later disconnect/reconnect keeps loaded messages mounted, not a re-loader.
|
|
27
|
-
* A grant retry / socket open before any snapshot leaves it false.
|
|
28
|
-
*/
|
|
29
13
|
hasLoadedInitialData: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* True once the first open has failed for good enough to say so: attempts have kept
|
|
32
|
-
* failing for {@link CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS} without any initial data, or
|
|
33
|
-
* reconnecting stopped (gave up, or the socket was revoked) before any. The client keeps
|
|
34
|
-
* retrying in the background; the first authoritative data, or a manual `retryNow()`,
|
|
35
|
-
* clears it. Never true once initial data has loaded — a later outage keeps the loaded
|
|
36
|
-
* messages under the reconnect banner instead.
|
|
37
|
-
*/
|
|
38
14
|
initialLoadFailed: boolean;
|
|
39
|
-
/**
|
|
40
|
-
* True while the channel is CURRENT: the socket is open, initial data has loaded, and the
|
|
41
|
-
* authoritative state has been re-applied since this socket opened — a non-resync resume
|
|
42
|
-
* snapshot, or the history page answering a resync. Cleared the moment the socket closes,
|
|
43
|
-
* reconnects, or is denied, and false again on every new socket until its resume lands, so a
|
|
44
|
-
* consumer that must act on the conversation's current terms (an agreement, an offer) waits for
|
|
45
|
-
* it. Unlike `hasLoadedInitialData` it is never sticky.
|
|
46
|
-
*/
|
|
47
15
|
isSynced: boolean;
|
|
48
16
|
}
|
|
49
17
|
export interface ChannelClientConfig {
|
|
50
|
-
/** WebSocket endpoint base, e.g. `wss://chat.example.com`. The client appends `/channel`. */
|
|
51
18
|
endpoint: string;
|
|
52
|
-
/** The logical thread id the UI keys on (used as ChatMessageV1.threadId). */
|
|
53
19
|
threadId: string;
|
|
54
|
-
/** The authenticated uid (sender identity for optimistic echoes + auth-switch guard). */
|
|
55
20
|
currentUserId: string;
|
|
56
|
-
/** Mints/refreshes a channel grant. Called on connect and once on a 4401 re-grant. */
|
|
57
21
|
grantProvider: GrantProvider;
|
|
58
22
|
socketFactory: SocketFactory;
|
|
59
23
|
timers?: TransportTimers;
|
|
60
|
-
/** Reconnect backoff tuning (forwarded to realtime-core). */
|
|
61
24
|
reconnect?: {
|
|
62
25
|
baseDelayMs?: number;
|
|
63
26
|
maxDelayMs?: number;
|
|
64
27
|
maxAttempts?: number | null;
|
|
65
28
|
random?: () => number;
|
|
66
29
|
};
|
|
67
|
-
/**
|
|
68
|
-
* OPT-IN structured client diagnostics (default OFF — absent/`false` changes
|
|
69
|
-
* nothing and costs one nullish check per decision point). `true` emits one
|
|
70
|
-
* `console.debug` line per decision with a stable `chat_client_*` event name
|
|
71
|
-
* and a compact, content-free JSON payload; a function routes those entries
|
|
72
|
-
* into the app's own logger instead. Never logs message text, captions,
|
|
73
|
-
* filenames, URLs, tokens, or previews — ids, seqs, states, counts, and codes
|
|
74
|
-
* only. See `./diagnostics.ts`.
|
|
75
|
-
*/
|
|
76
30
|
diagnostics?: ChatClientDiagnosticsOption;
|
|
77
31
|
}
|
|
78
32
|
export declare class ChannelClient {
|
|
@@ -82,280 +36,78 @@ export declare class ChannelClient {
|
|
|
82
36
|
private socket;
|
|
83
37
|
private state;
|
|
84
38
|
private readonly listeners;
|
|
85
|
-
/** Resume cursor: highest seq we've durably applied. Sent on resume after reconnect. */
|
|
86
39
|
private resumeSeq;
|
|
87
|
-
/** Last read-ack the UI requested; re-sent on reconnect so a drop during a closed
|
|
88
|
-
* socket window is recovered (M2). */
|
|
89
40
|
private lastReadAck;
|
|
90
|
-
/** True after a `4401` re-grant has been attempted for the CURRENT connect cycle (once-only). */
|
|
91
41
|
private reauthAttempted;
|
|
92
42
|
private presenceSubscribed;
|
|
93
43
|
private heartbeatTimer;
|
|
94
|
-
/**
|
|
95
|
-
* When the CURRENT socket last proved it was alive: set on open, refreshed by
|
|
96
|
-
* every successfully parsed inbound frame (`heartbeat-ack` included — that IS
|
|
97
|
-
* the DO's liveness answer). Null whenever no socket is live, so the watchdog
|
|
98
|
-
* can never judge a socket that does not exist. See {@link INBOUND_SILENCE_LIMIT_MS}.
|
|
99
|
-
*/
|
|
100
44
|
private lastInboundAt;
|
|
101
45
|
private historyRequests;
|
|
102
46
|
private typingTimers;
|
|
103
47
|
private lastTypingSentAt;
|
|
104
48
|
private reconnectTimer;
|
|
105
|
-
/** Single scheduled re-send for the membership-pending window (never stacks). */
|
|
106
49
|
private membershipRetryTimer;
|
|
107
50
|
private closedByUs;
|
|
108
|
-
/**
|
|
109
|
-
* The current lifecycle's number, advanced by every `connect()` and `close()`. A grant
|
|
110
|
-
* that resolves, or a timer that fires, for an older lifecycle is dropped — so a
|
|
111
|
-
* close → connect while an earlier grant is still minting can never open a second
|
|
112
|
-
* socket or touch the new lifecycle's state (FRONTEND-108).
|
|
113
|
-
*/
|
|
114
51
|
private lifecycle;
|
|
115
|
-
/** Reports the first open as failed once its attempts have failed for the budget. */
|
|
116
52
|
private readonly initialLoad;
|
|
117
|
-
/**
|
|
118
|
-
* Per-seq moderation overlay (the max-revision wins). A `revision` frame writes
|
|
119
|
-
* here keyed by `messageSeq` so it applies whether it arrives BEFORE its base
|
|
120
|
-
* row (re-applied when the row later renders) or AFTER (re-renders the present
|
|
121
|
-
* row in place). Merged with the row's own inline `moderationKind` so a row that
|
|
122
|
-
* arrives already-moderated from the DO AND one covered by a live frame both
|
|
123
|
-
* render the moderated state. */
|
|
124
53
|
private overlays;
|
|
125
|
-
/**
|
|
126
|
-
* The ORIGINAL (un-overlaid) mapped message per seq. A `restore` revision must
|
|
127
|
-
* revert a previously-redacted message to its original text, so re-renders start
|
|
128
|
-
* from this cache rather than the (possibly already-blanked) message in state. */
|
|
129
54
|
private originalRows;
|
|
130
|
-
/**
|
|
131
|
-
* Un-acked sends keyed by clientMessageId. A send is added here when its frame is
|
|
132
|
-
* written and removed when its `ack` (or the canonical `message` echo carrying the
|
|
133
|
-
* same clientMessageId) arrives. On every reconnect resume they are RE-SENT with
|
|
134
|
-
* the SAME clientMessageId — the DO dedups (re-acks without re-broadcast), so a
|
|
135
|
-
* send that lost its socket before the ack is delivered exactly once. A pending
|
|
136
|
-
* send that exceeds the attempt/age cap is flipped to a visible failed state
|
|
137
|
-
* (retry affordance) instead of staying a ghost. */
|
|
138
55
|
private pendingSends;
|
|
139
|
-
/**
|
|
140
|
-
* The structured diagnostics emitter, or null when diagnostics are off (the
|
|
141
|
-
* default). Every call site uses `this.diag?.(...)` so the off path constructs
|
|
142
|
-
* no payload at all.
|
|
143
|
-
*/
|
|
144
56
|
private readonly diag;
|
|
145
|
-
/** Monotonic socket-open attempt counter (diagnostics correlation only). */
|
|
146
57
|
private connectAttempt;
|
|
147
|
-
/** Why the CURRENT connect attempt is happening (diagnostics only). */
|
|
148
58
|
private reconnectCause;
|
|
149
|
-
/** The last close code observed, carried into the next attempt's cause line. */
|
|
150
59
|
private lastCloseCode;
|
|
151
60
|
constructor(config: ChannelClientConfig);
|
|
152
61
|
getState(): ChannelClientState;
|
|
153
62
|
subscribe(fn: (s: ChannelClientState) => void): () => void;
|
|
154
63
|
private setState;
|
|
155
64
|
connect(): Promise<void>;
|
|
156
|
-
/**
|
|
157
|
-
* Retry now (the first-open failure's Retry). While a reconnect is waiting out its
|
|
158
|
-
* backoff, the wait is cancelled and a socket opens at once; a lifecycle that stopped
|
|
159
|
-
* (reconnect gave up, or the socket was revoked) starts again; an attempt already in
|
|
160
|
-
* flight is left to finish. Either way the first-open failure clears, so the opening state
|
|
161
|
-
* shows while the retry runs. The spent budget stays spent: a retry that fails is reported
|
|
162
|
-
* again at once, and one that hangs a full budget later.
|
|
163
|
-
*/
|
|
164
65
|
retryNow(): void;
|
|
165
66
|
private startLifecycle;
|
|
166
|
-
/** The first open has failed for long enough to say so (see `initialLoadFailed`). */
|
|
167
67
|
private reportInitialLoadFailed;
|
|
168
|
-
/** The first authoritative data is applied; it ends any first-open failure. */
|
|
169
68
|
private initialDataLoaded;
|
|
170
69
|
private openSocket;
|
|
171
70
|
private onOpen;
|
|
172
|
-
/**
|
|
173
|
-
* On reconnect, re-send each un-acked pending send with the SAME clientMessageId
|
|
174
|
-
* (the DO dedups). A send that has exceeded the attempt or age cap is flipped to a
|
|
175
|
-
* visible failed state instead of being resent forever — the ghost becomes a
|
|
176
|
-
* retryable failure the UI can surface.
|
|
177
|
-
*/
|
|
178
71
|
private resendPendingSends;
|
|
179
|
-
/**
|
|
180
|
-
* Flip an un-acked optimistic row to a visible failed state. The pending entry is
|
|
181
|
-
* RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME
|
|
182
|
-
* clientMessageId; the row is marked `meta.sendFailed` (plus the correlated
|
|
183
|
-
* `sendFailureCode`/`sendRetryable` when a `send-rejected` frame supplied them) so
|
|
184
|
-
* the default renderer shows the right reason + retry affordance rather than an
|
|
185
|
-
* indistinguishable "sent" bubble. A late ack/echo still reconciles + clears it.
|
|
186
|
-
*
|
|
187
|
-
* `code`/`retryable` are present for a correlated rejection (terminal) or a
|
|
188
|
-
* budget-exhausted server-retryable send; absent for a blind reconnect give-up
|
|
189
|
-
* (which keeps the neutral `send-failed` code).
|
|
190
|
-
*/
|
|
191
72
|
private failPendingSend;
|
|
192
|
-
/** Clear a pending send's per-send retry timer (idempotent). */
|
|
193
73
|
private clearSendRetryTimer;
|
|
194
|
-
/**
|
|
195
|
-
* Stop tracking a send (acked / reconciled by an echo / history row): cancel its
|
|
196
|
-
* per-send retry timer first so a late reconcile never leaves a stray timer that
|
|
197
|
-
* would resend after the send already landed, then drop it from the map.
|
|
198
|
-
*/
|
|
199
74
|
private clearPendingSend;
|
|
200
|
-
/** One timer for the membership-pending re-send window; a newer frame resets it. */
|
|
201
75
|
private scheduleMembershipPendingRetry;
|
|
202
|
-
/**
|
|
203
|
-
* Handle a CORRELATED `send-rejected` frame (the target protocol): parse the
|
|
204
|
-
* untrusted payload with the shared schema, correlate to EXACTLY ONE pending send
|
|
205
|
-
* by clientMessageId, and either fail just that bubble (terminal) or schedule a
|
|
206
|
-
* single per-send resend (retryable).
|
|
207
|
-
*
|
|
208
|
-
* A frame that fails validation is NOT simply dropped when it still names one of
|
|
209
|
-
* OUR pending sends: unlike an unknown frame type, an id-correlated rejection is
|
|
210
|
-
* the server explicitly telling us this send did not land — swallowing it would
|
|
211
|
-
* strand the bubble on "Sending…" forever (no further frame, no reconnect, no
|
|
212
|
-
* budget check ever runs). A future/unknown rejection code (version skew during a
|
|
213
|
-
* rollout) therefore fails that send CLOSED with the neutral copy + Retry. A
|
|
214
|
-
* malformed frame with no recognizable pending id is dropped (forward-compat
|
|
215
|
-
* noise), never surfaced as an error.
|
|
216
|
-
*/
|
|
217
76
|
private applySendRejected;
|
|
218
|
-
/**
|
|
219
|
-
* Schedule EXACTLY ONE per-send resend of a correlated retryable rejection (never
|
|
220
|
-
* stacks — a newer rejection clears the prior timer first). When it fires it
|
|
221
|
-
* re-sends the SAME clientMessageId (the DO dedups) unless the wall-clock budget
|
|
222
|
-
* has elapsed, in which case the send fails with its recorded reason.
|
|
223
|
-
*/
|
|
224
77
|
private scheduleSendRetry;
|
|
225
|
-
/** Clamp a server `retryAfterMs` hint into a sane resend delay (defaulting when absent). */
|
|
226
78
|
private clampRetryDelay;
|
|
227
|
-
/**
|
|
228
|
-
* Flip EVERY un-acked pending send to the visible failed state. Called on the
|
|
229
|
-
* terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send
|
|
230
|
-
* in flight when the socket dies for good shows "Sending…" until unmount.
|
|
231
|
-
*/
|
|
232
79
|
private failAllPendingSends;
|
|
233
|
-
/**
|
|
234
|
-
* Retry a FAILED (or still-pending) send, RE-USING its original clientMessageId
|
|
235
|
-
* so the DO's send-idempotency dedups against any copy that actually landed.
|
|
236
|
-
* Resets the attempt/age budget, clears the row's `sendFailed` marker (back to
|
|
237
|
-
* the normal pending/optimistic look), and re-sends immediately when the socket
|
|
238
|
-
* is open — otherwise the entry rides the next reconnect's automatic resend.
|
|
239
|
-
* Returns false when the clientMessageId is not a tracked pending send (already
|
|
240
|
-
* acked/reconciled, or never sent from this client).
|
|
241
|
-
*/
|
|
242
80
|
retrySend(clientMessageId: string): boolean;
|
|
243
81
|
private onError;
|
|
244
82
|
private onClose;
|
|
245
83
|
private scheduleReconnect;
|
|
246
|
-
/**
|
|
247
|
-
* Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).
|
|
248
|
-
* A reconnect would just re-deny at the next mint, so stop for good — the same
|
|
249
|
-
* terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect
|
|
250
|
-
* controller, and flip every un-acked pending send to the visible failed state.
|
|
251
|
-
* `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does
|
|
252
|
-
* not win as the surfaced error; the React hook maps 'access-denied' to
|
|
253
|
-
* `allowed: false` so the shell renders its existing no-access surface.
|
|
254
|
-
*/
|
|
255
84
|
private denyAccessTerminally;
|
|
256
85
|
private onMessage;
|
|
257
|
-
/**
|
|
258
|
-
* Apply a live moderation `revision` frame: record the overlay keyed by seq
|
|
259
|
-
* (max-`messageRevision` wins — a stale/older frame is ignored) and re-render the
|
|
260
|
-
* matching message in place if it is already loaded. If the base row hasn't
|
|
261
|
-
* arrived yet the overlay is retained and re-applied when the row renders, so a
|
|
262
|
-
* frame that races ahead of its message never lets the original content show.
|
|
263
|
-
*/
|
|
264
86
|
private applyRevisionFrame;
|
|
265
|
-
/**
|
|
266
|
-
* The effective overlay for a seq = the max-`messageRevision` between the row's
|
|
267
|
-
* own inline overlay (from the DO history/delta merge) and any received `revision`
|
|
268
|
-
* frame stored in `this.overlays`. Either source may be ahead of the other.
|
|
269
|
-
*/
|
|
270
87
|
private effectiveOverlay;
|
|
271
|
-
/** Map a DO row to a UI message with its effective moderation overlay applied. */
|
|
272
88
|
private renderRow;
|
|
273
|
-
/**
|
|
274
|
-
* Apply the Contract C resume snapshot. The DO always returns the authoritative
|
|
275
|
-
* tail + read cursor and, per the pinned contract, EITHER a contiguous delta
|
|
276
|
-
* (`resync:false`) OR a resync directive (`resync:true`).
|
|
277
|
-
*
|
|
278
|
-
* - `resync === false`: apply the contiguous `delta` (the ≤500-message backlog
|
|
279
|
-
* after our cursor, oldest-first) through the same render/merge path as a live
|
|
280
|
-
* `message`, then advance `resumeSeq` to `lastMessageSeq`. This heals ANY gap
|
|
281
|
-
* within the backlog — including moderation `revision`s the DO baked into the
|
|
282
|
-
* delta rows — with no history fetch, so a >50-message reconnect gap is no
|
|
283
|
-
* longer a permanent hole.
|
|
284
|
-
* - `resync === true`: the gap is beyond the backlog (or we sent no cursor). Drop
|
|
285
|
-
* the local tail (messages + overlays + original-row cache) and re-page history
|
|
286
|
-
* from the top via a cursorless `requestHistory(null)`.
|
|
287
|
-
*
|
|
288
|
-
* A pre-contract DO (no `resync`/`delta`) is treated as `resync:true` ⇒ the safe
|
|
289
|
-
* re-page path, so a stale worker never strands the client. An empty delta with
|
|
290
|
-
* `resync:false` (already caught up) is a no-op beyond advancing the cursor.
|
|
291
|
-
*/
|
|
292
89
|
private applyChannelSnapshot;
|
|
293
|
-
/**
|
|
294
|
-
* Apply one server row (a live `message` frame or a resume-delta row) and record
|
|
295
|
-
* the decision the client made about it.
|
|
296
|
-
*
|
|
297
|
-
* Diagnostics bounding: a LIVE frame always emits one `chat_client_frame_applied`
|
|
298
|
-
* line; a DELTA row emits one only when it is materially interesting (carries a
|
|
299
|
-
* moderation kind, or reconciles one of our optimistic sends) — a 500-row backlog
|
|
300
|
-
* is otherwise summarized by `chat_client_resume_result` rather than spamming 500
|
|
301
|
-
* lines.
|
|
302
|
-
*/
|
|
303
90
|
private applyMessage;
|
|
304
|
-
/**
|
|
305
|
-
* The pre-merge view of a row's seq: whether we already hold that seq, and whether
|
|
306
|
-
* this row reconciles a live optimistic send.
|
|
307
|
-
* Diagnostics-only — never called when diagnostics are off.
|
|
308
|
-
*/
|
|
309
91
|
private priorRowFacts;
|
|
310
|
-
/**
|
|
311
|
-
* Merge a server message: reconcile an optimistic echo by clientMessageId (swap
|
|
312
|
-
* the `optimistic:` row for the canonical seq row), or insert/replace by seq.
|
|
313
|
-
* Keeps the list sorted ascending by seq with optimistic rows at the tail.
|
|
314
|
-
*/
|
|
315
92
|
private mergeMessage;
|
|
316
93
|
private applyAck;
|
|
317
94
|
private applyHistoryPage;
|
|
318
95
|
private applyTyping;
|
|
319
96
|
private sendFrame;
|
|
320
|
-
/**
|
|
321
|
-
* Optimistic send: render the echo immediately keyed by clientMessageId, then
|
|
322
|
-
* emit the `send` frame. The server `ack`/`message` reconciles on the seq.
|
|
323
|
-
*/
|
|
324
97
|
send(args: {
|
|
325
98
|
clientMessageId: string;
|
|
326
99
|
text: string;
|
|
327
100
|
}): boolean;
|
|
328
|
-
/**
|
|
329
|
-
* Explicit read ack (delivery != read). `focused` => the channel is open + at latest.
|
|
330
|
-
* Remembers the cursor (re-sent on reconnect) and returns whether the frame was
|
|
331
|
-
* actually sent over an open socket, so the caller only advances its local
|
|
332
|
-
* already-acked marker on a real send (M2).
|
|
333
|
-
*/
|
|
334
101
|
readAck(readSeq: number, focused: boolean): boolean;
|
|
335
|
-
/** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */
|
|
336
102
|
typing(): void;
|
|
337
103
|
presenceSubscribe(): void;
|
|
338
104
|
presenceUnsubscribe(): void;
|
|
339
|
-
/** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */
|
|
340
105
|
fetchOlder(): void;
|
|
341
106
|
private requestHistory;
|
|
342
107
|
private startHeartbeat;
|
|
343
108
|
private stopHeartbeat;
|
|
344
|
-
/** True when the live socket has produced no parseable frame for too long. */
|
|
345
109
|
private isInboundSilent;
|
|
346
|
-
/**
|
|
347
|
-
* The channel DO auto-acks every heartbeat, so silence past
|
|
348
|
-
* {@link INBOUND_SILENCE_LIMIT_MS} means this socket is dead even though no
|
|
349
|
-
* `onclose` ever fired (the half-open-connection case that otherwise strands
|
|
350
|
-
* the UI on a socket that will never deliver another message).
|
|
351
|
-
*
|
|
352
|
-
* Close it LOCALLY and deliberately WITHOUT setting `closedByUs`, so the close
|
|
353
|
-
* lands on `onClose`'s ordinary transient path — which already owns the
|
|
354
|
-
* reconnect scheduling, the backoff, and the resume-cursor recovery. Nothing
|
|
355
|
-
* about reconnect policy is duplicated here.
|
|
356
|
-
*/
|
|
357
110
|
private closeDeadSocket;
|
|
358
|
-
/** Permanently close this client (auth-user switch / unmount). Idempotent. */
|
|
359
111
|
close(): void;
|
|
360
112
|
}
|
|
361
113
|
//# sourceMappingURL=channel-client.d.ts.map
|