@ttt-productions/chat-react 0.12.7 → 0.12.9
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/realtime/channel-client.d.ts +21 -0
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +35 -3
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts +18 -0
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +24 -4
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/index.d.ts +1 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/index.js +1 -1
- package/dist/realtime/index.js.map +1 -1
- package/dist/realtime/shared.d.ts +24 -0
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +32 -0
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.js +11 -5
- package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +10 -8
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +11 -2
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { ChatNameResolverProvider, useChatNameResolver, useOptionalChatNameResol
|
|
|
8
8
|
export type { UseChatMessagesResult } from "./hooks/useChatMessages.js";
|
|
9
9
|
export { useChatMessages } from "./hooks/useChatMessages.js";
|
|
10
10
|
export { canAccessThread } from "./hooks/useChatThreadAccess.js";
|
|
11
|
-
export { createRealtimeChatClient, createInboxClient, ChannelClient, InboxClient, useRealtimeChatMessages, browserSocketFactory, CHAT_SUBPROTOCOL, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, } from "./realtime/index.js";
|
|
11
|
+
export { createRealtimeChatClient, createInboxClient, ChannelClient, InboxClient, useRealtimeChatMessages, browserSocketFactory, ChatAccessDeniedError, isChatAccessDeniedError, CHAT_SUBPROTOCOL, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, } from "./realtime/index.js";
|
|
12
12
|
export type { RealtimeChatClient, RealtimeTransportConfig, ChannelClientState, ChannelClientConfig, InboxClientState, InboxClientConfig, UseRealtimeChatMessagesResult, GrantProvider, TransportTimers, RealtimeStatus, RealtimeSocket, SocketFactory, SocketHandlers, ChannelRefTuple, WireMessageRow, WireInboxSnapshot, WireRegistryEntry, ServerFrame, } from "./realtime/index.js";
|
|
13
13
|
export type { ChatShellProps } from "./ui/ChatShell.js";
|
|
14
14
|
export { ChatShell } from "./ui/ChatShell.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAM/C,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAGjD,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,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,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,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,yBAAyB,EACzB,8BAA8B,GAC/B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAM/C,YAAY,EACV,4BAA4B,EAC5B,gCAAgC,GACjC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAGjD,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,GACX,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,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"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { canAccessThread } from "./hooks/useChatThreadAccess.js";
|
|
|
7
7
|
// Realtime (Cloudflare Durable Object) transport — the client half of the chat
|
|
8
8
|
// worker wire protocol. Opt-in via `ChatCoreConfig.transport === 'realtime'`; the
|
|
9
9
|
// firestore transport stays the unchanged default. See docs/packages/chat-react.md.
|
|
10
|
-
export { createRealtimeChatClient, createInboxClient, ChannelClient, InboxClient, useRealtimeChatMessages, browserSocketFactory, CHAT_SUBPROTOCOL, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, } from "./realtime/index.js";
|
|
10
|
+
export { createRealtimeChatClient, createInboxClient, ChannelClient, InboxClient, useRealtimeChatMessages, browserSocketFactory, ChatAccessDeniedError, isChatAccessDeniedError, CHAT_SUBPROTOCOL, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, } from "./realtime/index.js";
|
|
11
11
|
export { ChatShell } from "./ui/ChatShell.js";
|
|
12
12
|
export { MessageList } from "./ui/MessageList.js";
|
|
13
13
|
export { Composer } from "./ui/Composer.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;AA2Bb,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAU/C,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAIjD,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,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,qBAAqB,CAAC;AA+B7B,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 ChatAttachmentConfig,\n ChatUploadAdapter,\n ChatMentionConfig,\n MessageRenderer,\n MessageRendererRegistry,\n RenderableMentionProvider,\n MentionResultRenderer,\n} from \"./types.js\";\n\n// Attachment-URL resolver context (render-time URL building from mediaAssetId).\nexport type {\n ChatAttachmentUrlResolver,\n ChatAttachmentUrlProviderProps,\n} from \"./context/ChatAttachmentUrlContext.js\";\nexport {\n ChatAttachmentUrlProvider,\n useChatAttachmentUrlResolver,\n} from \"./context/ChatAttachmentUrlContext.js\";\n\n// Attachment media-component injection (optional) — the app hands chat its own\n// display-path wrapper (recovery adapter, diagnostics, telemetry) so chat\n// media rides the same pipeline as every other surface; default stays the\n// package MediaViewer.\nexport type {\n ChatAttachmentMediaComponent,\n ChatAttachmentMediaProviderProps,\n} from \"./context/ChatAttachmentMediaContext.js\";\nexport {\n ChatAttachmentMediaProvider,\n useChatAttachmentMediaComponent,\n} from \"./context/ChatAttachmentMediaContext.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 CHAT_SUBPROTOCOL,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\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;AA2Bb,OAAO,EACL,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAU/C,OAAO,EACL,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,yCAAyC,CAAC;AAIjD,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,GACX,MAAM,qBAAqB,CAAC;AA+B7B,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 ChatAttachmentConfig,\n ChatUploadAdapter,\n ChatMentionConfig,\n MessageRenderer,\n MessageRendererRegistry,\n RenderableMentionProvider,\n MentionResultRenderer,\n} from \"./types.js\";\n\n// Attachment-URL resolver context (render-time URL building from mediaAssetId).\nexport type {\n ChatAttachmentUrlResolver,\n ChatAttachmentUrlProviderProps,\n} from \"./context/ChatAttachmentUrlContext.js\";\nexport {\n ChatAttachmentUrlProvider,\n useChatAttachmentUrlResolver,\n} from \"./context/ChatAttachmentUrlContext.js\";\n\n// Attachment media-component injection (optional) — the app hands chat its own\n// display-path wrapper (recovery adapter, diagnostics, telemetry) so chat\n// media rides the same pipeline as every other surface; default stays the\n// package MediaViewer.\nexport type {\n ChatAttachmentMediaComponent,\n ChatAttachmentMediaProviderProps,\n} from \"./context/ChatAttachmentMediaContext.js\";\nexport {\n ChatAttachmentMediaProvider,\n useChatAttachmentMediaComponent,\n} from \"./context/ChatAttachmentMediaContext.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} from \"./realtime/index.js\";\nexport type {\n RealtimeChatClient,\n RealtimeTransportConfig,\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"]}
|
|
@@ -15,6 +15,17 @@ export interface ChannelClientState {
|
|
|
15
15
|
hasOlder: boolean;
|
|
16
16
|
/** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */
|
|
17
17
|
lastErrorCode: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* True once the FIRST authoritative chat data has been applied — a non-resync
|
|
20
|
+
* resume snapshot, OR the first history page (INCLUDING an empty one). Drives the
|
|
21
|
+
* hook's `isInitialLoading` so the shell shows an honest "Opening chat…" state
|
|
22
|
+
* while the first data is loading, and a real empty-chat surface only after an
|
|
23
|
+
* authoritative empty result. NEVER inferred from `messages.length === 0` (an
|
|
24
|
+
* empty first history page IS a successful load). Once true it stays true — a
|
|
25
|
+
* later disconnect/reconnect keeps loaded messages mounted, not a re-loader.
|
|
26
|
+
* A grant retry / socket open before any snapshot leaves it false.
|
|
27
|
+
*/
|
|
28
|
+
hasLoadedInitialData: boolean;
|
|
18
29
|
}
|
|
19
30
|
export interface ChannelClientConfig {
|
|
20
31
|
/** WebSocket endpoint base, e.g. `wss://chat.ttt.productions`. The client appends `/channel`. */
|
|
@@ -122,6 +133,16 @@ export declare class ChannelClient {
|
|
|
122
133
|
private onError;
|
|
123
134
|
private onClose;
|
|
124
135
|
private scheduleReconnect;
|
|
136
|
+
/**
|
|
137
|
+
* Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).
|
|
138
|
+
* A reconnect would just re-deny at the next mint, so stop for good — the same
|
|
139
|
+
* terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect
|
|
140
|
+
* controller, and flip every un-acked pending send to the visible failed state.
|
|
141
|
+
* `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does
|
|
142
|
+
* not win as the surfaced error; the React hook maps 'access-denied' to
|
|
143
|
+
* `allowed: false` so the shell renders its existing no-access surface.
|
|
144
|
+
*/
|
|
145
|
+
private denyAccessTerminally;
|
|
125
146
|
private onMessage;
|
|
126
147
|
/**
|
|
127
148
|
* Apply a live moderation `revision` frame: record the overlay keyed by seq
|
|
@@ -1 +1 @@
|
|
|
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;AAWhE,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AASjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"channel-client.d.ts","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
18
18
|
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
19
19
|
import { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, } from './map.js';
|
|
20
|
-
import { defaultTimers, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, } from './shared.js';
|
|
20
|
+
import { defaultTimers, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, MAX_PENDING_SEND_ATTEMPTS, PENDING_SEND_MAX_AGE_MS, } from './shared.js';
|
|
21
21
|
const INITIAL = {
|
|
22
22
|
status: 'idle',
|
|
23
23
|
messages: [],
|
|
@@ -26,6 +26,7 @@ const INITIAL = {
|
|
|
26
26
|
isFetchingOlder: false,
|
|
27
27
|
hasOlder: true,
|
|
28
28
|
lastErrorCode: null,
|
|
29
|
+
hasLoadedInitialData: false,
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
31
32
|
* Retryable error-frame code the channel DO sends when a send arrives BEFORE the
|
|
@@ -119,8 +120,15 @@ export class ChannelClient {
|
|
|
119
120
|
try {
|
|
120
121
|
grantToken = await this.config.grantProvider();
|
|
121
122
|
}
|
|
122
|
-
catch {
|
|
123
|
-
//
|
|
123
|
+
catch (err) {
|
|
124
|
+
// A TERMINAL access denial (the app's grant provider translated an
|
|
125
|
+
// authoritative policy "no" into the package-owned ChatAccessDeniedError)
|
|
126
|
+
// must NOT reconnect — the next mint would just re-deny, stranding the shell
|
|
127
|
+
// on the opening loader forever. Every other mint failure (network, transient
|
|
128
|
+
// 'unavailable'/'internal', post-login 'unauthenticated') stays retryable and
|
|
129
|
+
// backs off exactly as before.
|
|
130
|
+
if (isChatAccessDeniedError(err))
|
|
131
|
+
return this.denyAccessTerminally();
|
|
124
132
|
return this.scheduleReconnect();
|
|
125
133
|
}
|
|
126
134
|
if (this.closedByUs)
|
|
@@ -301,6 +309,21 @@ export class ChannelClient {
|
|
|
301
309
|
void this.openSocket();
|
|
302
310
|
}, delay);
|
|
303
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).
|
|
314
|
+
* A reconnect would just re-deny at the next mint, so stop for good — the same
|
|
315
|
+
* terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect
|
|
316
|
+
* controller, and flip every un-acked pending send to the visible failed state.
|
|
317
|
+
* `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does
|
|
318
|
+
* not win as the surfaced error; the React hook maps 'access-denied' to
|
|
319
|
+
* `allowed: false` so the shell renders its existing no-access surface.
|
|
320
|
+
*/
|
|
321
|
+
denyAccessTerminally() {
|
|
322
|
+
this.closedByUs = true;
|
|
323
|
+
this.controller.close();
|
|
324
|
+
this.failAllPendingSends();
|
|
325
|
+
this.setState({ status: 'closed', lastErrorCode: 'access-denied' });
|
|
326
|
+
}
|
|
304
327
|
// ---- inbound frames ----
|
|
305
328
|
onMessage(data) {
|
|
306
329
|
const frame = parseFrame(data);
|
|
@@ -424,6 +447,11 @@ export class ChannelClient {
|
|
|
424
447
|
this.applyMessage(row);
|
|
425
448
|
if (snap.lastMessageSeq > this.resumeSeq)
|
|
426
449
|
this.resumeSeq = snap.lastMessageSeq;
|
|
450
|
+
// A non-resync snapshot IS the first authoritative data (even an empty delta =
|
|
451
|
+
// caught up) — end initial loading. A resync snapshot instead defers to the
|
|
452
|
+
// re-paged first history page below, so it must NOT end it here.
|
|
453
|
+
if (!this.state.hasLoadedInitialData)
|
|
454
|
+
this.setState({ hasLoadedInitialData: true });
|
|
427
455
|
return;
|
|
428
456
|
}
|
|
429
457
|
// Resync: drop the local tail and re-page from the top per the pinned contract.
|
|
@@ -519,6 +547,10 @@ export class ChannelClient {
|
|
|
519
547
|
messages: merged,
|
|
520
548
|
isFetchingOlder: false,
|
|
521
549
|
hasOlder: rows.length >= HISTORY_PAGE_MAX,
|
|
550
|
+
// The first history page — INCLUDING an empty one — is a successful authoritative
|
|
551
|
+
// load, so it ends initial loading. (Older-page fetches redundantly re-set true,
|
|
552
|
+
// which is a harmless no-op since the flag never flips back to false.)
|
|
553
|
+
hasLoadedInitialData: true,
|
|
522
554
|
});
|
|
523
555
|
}
|
|
524
556
|
applyTyping(uid) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-client.js","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Channel realtime client: ONE WebSocket to a single channel DO, driving the\n// Contract C wire protocol. Framework-neutral (a plain observable store the React\n// hook subscribes to) so it is unit-testable with a mock socket and no React.\n//\n// Responsibilities (Contract C):\n// - connect via subprotocol auth (grant offered by the injected grantProvider)\n// - receive versioned `message` frames; server owns `seq`\n// - optimistic send keyed by clientMessageId, reconciled on the server `ack` seq\n// - explicit read acks (delivery != read; focus signals \"read\")\n// - typing (>= 2s coalesced per the V1 limit)\n// - presence-subscribe/-unsubscribe + a 20s client heartbeat\n// - reconnect (realtime-core controller) with a resume cursor -> snapshot then deltas\n// - history pagination over the socket (page <= 50, epoch-aware via seq)\n// - error codes incl. 4401 AUTH_EXPIRED -> re-mint a grant ONCE via grantProvider\n//\n// Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireMessageRow,\n type WireChannelSnapshot,\n type RevisionKind,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport {\n wireRowToMessage,\n optimisticMessage,\n seqToMessageId,\n applyModerationOverlay,\n overlayFromRow,\n type ModerationOverlay,\n} from './map.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n HEARTBEAT_MS,\n TYPING_COALESCE_MS,\n HISTORY_PAGE_MAX,\n MAX_PENDING_SEND_ATTEMPTS,\n PENDING_SEND_MAX_AGE_MS,\n} from './shared.js';\n\n/** An un-acked send tracked for reconnect resend (clientMessageId idempotency). */\ninterface PendingSend {\n clientMessageId: string;\n text: string;\n replyTo: { messageSeq: number; preview: string } | null;\n /** When the send was first attempted (for the age cap). Reset by retrySend(). */\n sentAt: number;\n /** How many times it has been (re)sent over a socket. Reset by retrySend(). */\n attempts: number;\n /** True once the attempt/age cap flipped this send to the visible failed state.\n * A failed entry is RETAINED (not deleted) so retrySend() can re-queue it with\n * the SAME clientMessageId; it is skipped by the automatic reconnect resend. */\n failed: boolean;\n}\n\nexport interface ChannelClientState {\n status: RealtimeStatus;\n /** Ordered ascending by seq; optimistic messages (no seq yet) sort to the tail. */\n messages: ChatMessageV1[];\n /** uids currently typing (cleared after ~TYPING_COALESCE_MS of silence). */\n typing: string[];\n /** uids the DO reports online (presence). Empty until presence-subscribe. */\n presence: string[];\n /** True while an older-history page request is in flight. */\n isFetchingOlder: boolean;\n /** False once a history page returns fewer than the requested limit (reached the head). */\n hasOlder: boolean;\n /** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */\n lastErrorCode: string | null;\n}\n\nexport interface ChannelClientConfig {\n /** WebSocket endpoint base, e.g. `wss://chat.ttt.productions`. The client appends `/channel`. */\n endpoint: string;\n /** The logical thread id the UI keys on (used as ChatMessageV1.threadId). */\n threadId: string;\n /** The authenticated uid (sender identity for optimistic echoes + auth-switch guard). */\n currentUserId: string;\n /** Mints/refreshes a channel grant. Called on connect and once on a 4401 re-grant. */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n /** Reconnect backoff tuning (forwarded to realtime-core). */\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n}\n\nconst INITIAL: ChannelClientState = {\n status: 'idle',\n messages: [],\n typing: [],\n presence: [],\n isFetchingOlder: false,\n hasOlder: true,\n lastErrorCode: null,\n};\n\n/**\n * Retryable error-frame code the channel DO sends when a send arrives BEFORE the\n * member row has synced through the async backend→worker pipeline (a just-created\n * invite/channel). The socket stays open; the client re-sends after `retryAfterMs`\n * through the normal resendPendingSends machinery (same caps). The string is part\n * of the wire contract with the worker runtime's send handler.\n */\nconst MEMBERSHIP_PENDING_CODE = 'membership-pending';\n/** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */\nconst MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;\n\nexport class ChannelClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: ChannelClientState = INITIAL;\n private readonly listeners = new Set<(s: ChannelClientState) => void>();\n\n /** Resume cursor: highest seq we've durably applied. Sent on resume after reconnect. */\n private resumeSeq = 0;\n /** Last read-ack the UI requested; re-sent on reconnect so a drop during a closed\n * socket window is recovered (M2). */\n private lastReadAck: { readSeq: number; focused: boolean } | null = null;\n /** True after a `4401` re-grant has been attempted for the CURRENT connect cycle (once-only). */\n private reauthAttempted = false;\n private presenceSubscribed = false;\n private heartbeatTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private typingTimers = new Map<string, ReturnType<TransportTimers['setTimeout']>>();\n private lastTypingSentAt = Number.NEGATIVE_INFINITY;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n /** Single scheduled re-send for the membership-pending window (never stacks). */\n private membershipRetryTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /**\n * Per-seq moderation overlay (the max-revision wins). A `revision` frame writes\n * here keyed by `messageSeq` so it applies whether it arrives BEFORE its base\n * row (re-applied when the row later renders) or AFTER (re-renders the present\n * row in place). Merged with the row's own inline `moderationKind` so a row that\n * arrives already-moderated from the DO AND one covered by a live frame both\n * render the moderated state. */\n private overlays = new Map<number, ModerationOverlay>();\n /**\n * The ORIGINAL (un-overlaid) mapped message per seq. A `restore` revision must\n * revert a previously-redacted message to its original text, so re-renders start\n * from this cache rather than the (possibly already-blanked) message in state. */\n private originalRows = new Map<number, ChatMessageV1>();\n /**\n * Un-acked sends keyed by clientMessageId. A send is added here when its frame is\n * written and removed when its `ack` (or the canonical `message` echo carrying the\n * same clientMessageId) arrives. On every reconnect resume they are RE-SENT with\n * the SAME clientMessageId — the DO dedups (re-acks without re-broadcast), so a\n * send that lost its socket before the ack is delivered exactly once. A pending\n * send that exceeds the attempt/age cap is flipped to a visible failed state\n * (retry affordance) instead of staying a ghost. */\n private pendingSends = new Map<string, PendingSend>();\n\n constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n }\n\n // ---- observable store ----\n\n getState(): ChannelClientState {\n return this.state;\n }\n\n subscribe(fn: (s: ChannelClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<ChannelClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n // ---- connection lifecycle ----\n\n async connect(): Promise<void> {\n // Idempotency guard (C-B7): connect() only starts a lifecycle from a fresh\n // (idle) or fully torn-down (closed) state. A second connect() while one is\n // already connecting / open / reconnecting is a no-op, so a stray double-owner\n // can't open a second WebSocket. The reconnect path uses openSocket() /\n // scheduleReconnect() directly (never connect()), so this never blocks a\n // legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n this.closedByUs = false;\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch {\n // Grant mint failed — treat as a closed attempt and back off.\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/channel`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => this.onError(),\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n // Resume: ask the DO for the authoritative snapshot, then live deltas flow. The\n // field name MUST be `afterSeq` — the Channel DO reads `payload.afterSeq`; any other\n // name reads as cursorless and forces a full resync every reconnect.\n this.sendFrame(CLIENT_FRAME.RESUME, { afterSeq: this.resumeSeq });\n // If the UI had presence open, re-subscribe after a reconnect.\n if (this.presenceSubscribed) this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n // Re-send the last read cursor so a read-ack dropped during the disconnect window\n // (sendFrame returns false on a closed socket) is not lost until the next message.\n if (this.lastReadAck) this.sendFrame(CLIENT_FRAME.READ_ACK, { ...this.lastReadAck });\n // Re-send every un-acked pending send with its ORIGINAL clientMessageId so a send\n // that lost its socket before the ack is not silently dropped. The DO dedups\n // (re-acks, no re-broadcast, no flood-token) — this is the client half of the\n // Contract C send-idempotency the server side was built for.\n this.resendPendingSends();\n this.startHeartbeat();\n }\n\n /**\n * On reconnect, re-send each un-acked pending send with the SAME clientMessageId\n * (the DO dedups). A send that has exceeded the attempt or age cap is flipped to a\n * visible failed state instead of being resent forever — the ghost becomes a\n * retryable failure the UI can surface.\n */\n private resendPendingSends(): void {\n if (this.pendingSends.size === 0) return;\n const now = this.timers.now();\n for (const pending of [...this.pendingSends.values()]) {\n // Already flipped to the visible failed state — only an explicit retrySend()\n // re-queues it; the automatic reconnect resend leaves it alone.\n if (pending.failed) continue;\n const tooOld = now - pending.sentAt >= PENDING_SEND_MAX_AGE_MS;\n const tooManyAttempts = pending.attempts >= MAX_PENDING_SEND_ATTEMPTS;\n if (tooOld || tooManyAttempts) {\n this.failPendingSend(pending.clientMessageId);\n continue;\n }\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n if (sent) pending.attempts += 1;\n }\n }\n\n /**\n * Flip an un-acked optimistic row to a visible failed state. The pending entry is\n * RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME\n * clientMessageId; the row is marked `meta.sendFailed` so the default renderer\n * shows a failure with a retry affordance rather than an indistinguishable\n * \"sent\" bubble. A late ack/echo still reconciles + clears it.\n */\n private failPendingSend(clientMessageId: string): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending || pending.failed) return;\n pending.failed = true;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId\n ? { ...m, meta: { ...m.meta, sendFailed: true } }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: 'send-failed' });\n }\n\n /** One timer for the membership-pending re-send window; a newer frame resets it. */\n private scheduleMembershipPendingRetry(delayMs: number): void {\n if (this.membershipRetryTimer != null) this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = this.timers.setTimeout(() => {\n this.membershipRetryTimer = null;\n if (this.closedByUs) return;\n this.resendPendingSends();\n }, delayMs);\n }\n\n /**\n * Flip EVERY un-acked pending send to the visible failed state. Called on the\n * terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send\n * in flight when the socket dies for good shows \"Sending…\" until unmount.\n */\n private failAllPendingSends(): void {\n for (const clientMessageId of [...this.pendingSends.keys()]) {\n this.failPendingSend(clientMessageId);\n }\n }\n\n /**\n * Retry a FAILED (or still-pending) send, RE-USING its original clientMessageId\n * so the DO's send-idempotency dedups against any copy that actually landed.\n * Resets the attempt/age budget, clears the row's `sendFailed` marker (back to\n * the normal pending/optimistic look), and re-sends immediately when the socket\n * is open — otherwise the entry rides the next reconnect's automatic resend.\n * Returns false when the clientMessageId is not a tracked pending send (already\n * acked/reconciled, or never sent from this client).\n */\n retrySend(clientMessageId: string): boolean {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return false;\n pending.failed = false;\n pending.sentAt = this.timers.now();\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n pending.attempts = sent ? 1 : 0;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed\n ? { ...m, meta: { ...m.meta, sendFailed: false } }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n private onError(): void {\n // The close handler does the backoff; onerror is advisory in browsers.\n }\n\n private onClose(code: number, _reason: string): void {\n this.stopHeartbeat();\n this.socket = null;\n if (this.closedByUs) {\n this.setState({ status: 'closed' });\n return;\n }\n // 4401 AUTH_EXPIRED: re-mint a grant ONCE and reconnect immediately, no backoff.\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n // 4403 REVOKED: access pulled. Stop — a reconnect would just re-deny at mint.\n // Terminal: flip every un-acked pending send to the visible failed state first\n // (failPendingSend sets lastErrorCode 'send-failed'; the 'revoked' set below\n // wins as the final surfaced code).\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n if (delay == null) {\n // Reconnect give-up: terminal, same as REVOKED — pending sends must not stay\n // an eternal \"Sending…\"; flip them to the visible failed/retry state.\n this.failAllPendingSends();\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n // ---- inbound frames ----\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.type) return;\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message);\n case 'ack':\n return this.applyAck(payload as { clientMessageId?: string; seq?: number; readSeq?: number });\n case 'history-page':\n return this.applyHistoryPage((payload as { messages: WireMessageRow[] }).messages ?? []);\n case 'presence':\n return this.setState({ presence: (payload as { online: string[] }).online ?? [] });\n case 'typing':\n return this.applyTyping((payload as { uid: string }).uid);\n case 'snapshot':\n return this.applyChannelSnapshot(payload as unknown as WireChannelSnapshot);\n case 'error': {\n const code = String((payload as { code?: unknown }).code ?? 'error');\n // Retryable: membership row not yet synced to the DO. Keep the send pending\n // (no visible error — the optimistic bubble stays \"Sending…\") and re-send\n // after the server-suggested delay via the normal resend machinery, whose\n // attempt/age caps flip it to the visible failed state if the window never\n // closes. Everything else surfaces as before.\n if (code === MEMBERSHIP_PENDING_CODE) {\n const raw = (payload as { retryAfterMs?: unknown }).retryAfterMs;\n const delay = typeof raw === 'number' && raw > 0 ? raw : MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;\n return this.scheduleMembershipPendingRetry(delay);\n }\n return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n default:\n return; // unknown/optional types tolerated (forward compat)\n }\n }\n\n /**\n * Apply a live moderation `revision` frame: record the overlay keyed by seq\n * (max-`messageRevision` wins — a stale/older frame is ignored) and re-render the\n * matching message in place if it is already loaded. If the base row hasn't\n * arrived yet the overlay is retained and re-applied when the row renders, so a\n * frame that races ahead of its message never lets the original content show.\n */\n private applyRevisionFrame(payload: { messageSeq?: number; kind?: RevisionKind; messageRevision?: number }): void {\n const seq = payload.messageSeq;\n const kind = payload.kind;\n const messageRevision = payload.messageRevision;\n if (typeof seq !== 'number' || kind == null || typeof messageRevision !== 'number') return;\n const prev = this.overlays.get(seq);\n // Max-revision merge: a higher messageRevision supersedes; an older one is dropped.\n if (prev && prev.messageRevision >= messageRevision) return;\n this.overlays.set(seq, { kind, messageRevision });\n // Re-render the matching message in place if it is already present. Re-derive\n // from the ORIGINAL row so a `restore` reverts a previously-blanked text.\n const messageId = seqToMessageId(seq);\n const original = this.originalRows.get(seq);\n if (original && this.state.messages.some((m) => m.messageId === messageId)) {\n const overlaid = applyModerationOverlay(original, this.effectiveOverlay(seq, null));\n const next = this.state.messages.map((m) => (m.messageId === messageId ? overlaid : m));\n this.setState({ messages: next });\n }\n }\n\n /**\n * The effective overlay for a seq = the max-`messageRevision` between the row's\n * own inline overlay (from the DO history/delta merge) and any received `revision`\n * frame stored in `this.overlays`. Either source may be ahead of the other.\n */\n private effectiveOverlay(seq: number, rowOverlay: ModerationOverlay | null): ModerationOverlay | null {\n const fromFrame = this.overlays.get(seq) ?? null;\n if (!rowOverlay) return fromFrame;\n if (!fromFrame) return rowOverlay;\n return fromFrame.messageRevision >= rowOverlay.messageRevision ? fromFrame : rowOverlay;\n }\n\n /** Map a DO row to a UI message with its effective moderation overlay applied. */\n private renderRow(row: WireMessageRow): ChatMessageV1 {\n const mapped = wireRowToMessage(row, this.config.threadId);\n // Cache the ORIGINAL so a later `restore` frame can revert a blanked message.\n this.originalRows.set(row.seq, mapped);\n return applyModerationOverlay(mapped, this.effectiveOverlay(row.seq, overlayFromRow(row)));\n }\n\n /**\n * Apply the Contract C resume snapshot. The DO always returns the authoritative\n * tail + read cursor and, per the pinned contract, EITHER a contiguous delta\n * (`resync:false`) OR a resync directive (`resync:true`).\n *\n * - `resync === false`: apply the contiguous `delta` (the ≤500-message backlog\n * after our cursor, oldest-first) through the same render/merge path as a live\n * `message`, then advance `resumeSeq` to `lastMessageSeq`. This heals ANY gap\n * within the backlog — including moderation `revision`s the DO baked into the\n * delta rows — with no history fetch, so a >50-message reconnect gap is no\n * longer a permanent hole.\n * - `resync === true`: the gap is beyond the backlog (or we sent no cursor). Drop\n * the local tail (messages + overlays + original-row cache) and re-page history\n * from the top via a cursorless `requestHistory(null)`.\n *\n * A pre-contract DO (no `resync`/`delta`) is treated as `resync:true` ⇒ the safe\n * re-page path, so a stale worker never strands the client. An empty delta with\n * `resync:false` (already caught up) is a no-op beyond advancing the cursor.\n */\n private applyChannelSnapshot(snap: WireChannelSnapshot): void {\n // Guard: an inbox snapshot must never reach a channel client.\n if (isInboxSnapshot(snap as never)) return;\n if (typeof snap.lastMessageSeq !== 'number') return;\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\n\n if (!resync) {\n // Apply the contiguous delta oldest-first, exactly like live messages, then\n // advance the cursor to the authoritative tail so the NEXT resume tells the DO\n // we're caught up only after we actually applied the backlog.\n const delta = Array.isArray(snap.delta) ? snap.delta : [];\n for (const row of delta) this.applyMessage(row);\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\n return;\n }\n\n // Resync: drop the local tail and re-page from the top per the pinned contract.\n // Clear the derived caches too so a stale overlay/original-row can't resurrect a\n // message the re-page will reload fresh (with its baked-in moderation state).\n // KEEP un-acked optimistic rows — they aren't server history yet; their pending\n // sends were already re-sent in onOpen and reconcile when the DO echoes them.\n this.overlays.clear();\n this.originalRows.clear();\n this.resumeSeq = 0;\n const keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n private applyMessage(row: WireMessageRow): void {\n if (!row || typeof row.seq !== 'number') return;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\n }\n\n /**\n * Merge a server message: reconcile an optimistic echo by clientMessageId (swap\n * the `optimistic:` row for the canonical seq row), or insert/replace by seq.\n * Keeps the list sorted ascending by seq with optimistic rows at the tail.\n */\n private mergeMessage(mapped: ChatMessageV1, row: WireMessageRow): void {\n // If this canonical row carries a clientMessageId we still have pending (the\n // sender's own broadcast, or a resume delta that included our send), the send\n // landed — stop tracking it for reconnect resend.\n if (row.clientMessageId) this.pendingSends.delete(row.clientMessageId);\n const next = this.state.messages.filter(\n (m) =>\n m.messageId !== mapped.messageId &&\n // drop the optimistic placeholder this server row reconciles\n (m.meta?.clientMessageId !== row.clientMessageId || !m.meta?.optimistic),\n );\n next.push(mapped);\n next.sort(sortBySeq);\n this.setState({ messages: next });\n }\n\n private applyAck(payload: { clientMessageId?: string; seq?: number; readSeq?: number }): void {\n // A read-ack echo carries readSeq only — nothing to reconcile locally.\n if (payload.clientMessageId == null || typeof payload.seq !== 'number') return;\n // Reconcile the optimistic echo: stamp the server seq onto it. The canonical\n // `message` broadcast (which the sender also receives) replaces it fully; this\n // ack guarantees the seq even if the broadcast is dropped/reordered.\n const seq = payload.seq;\n const cmid = payload.clientMessageId;\n // The send is acked — stop tracking it for reconnect resend.\n this.pendingSends.delete(cmid);\n const next = this.state.messages.map((m) => {\n if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid) return m;\n // Strip any failed marker — a late ack (e.g. a retry raced the cap) means the\n // send actually landed, so the row must render as delivered, not failed.\n const { sendFailed: _sendFailed, ...restMeta } = m.meta;\n return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };\n });\n next.sort(sortBySeq);\n if (seq > this.resumeSeq) this.resumeSeq = seq;\n this.setState({ messages: next });\n }\n\n private applyHistoryPage(rows: WireMessageRow[]): void {\n const mapped = rows.map((r) => this.renderRow(r));\n // Mirror mergeMessage's optimistic reconciliation: a history row carrying a\n // clientMessageId we still hold as an optimistic placeholder IS that send (the\n // DO stored it; the ack was lost). Drop the placeholder — keyed `optimistic:{cmid}`,\n // it would otherwise survive alongside its seq-keyed server row as a duplicate\n // bubble after a resync + re-page — and stop tracking it for resend.\n const incomingCmids = new Set<string>();\n for (const r of rows) {\n if (r.clientMessageId) {\n incomingCmids.add(r.clientMessageId);\n this.pendingSends.delete(r.clientMessageId);\n }\n }\n const bySeq = new Map<string, ChatMessageV1>();\n for (const m of this.state.messages) {\n if (m.meta?.optimistic && typeof m.meta?.clientMessageId === 'string' && incomingCmids.has(m.meta.clientMessageId)) {\n continue; // reconciled by the incoming server row\n }\n bySeq.set(m.messageId, m);\n }\n for (const m of mapped) bySeq.set(m.messageId, m);\n const merged = [...bySeq.values()].sort(sortBySeq);\n for (const r of rows) if (r.seq > this.resumeSeq) this.resumeSeq = r.seq;\n this.setState({\n messages: merged,\n isFetchingOlder: false,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n });\n }\n\n private applyTyping(uid: string): void {\n if (!uid || uid === this.config.currentUserId) return;\n if (!this.state.typing.includes(uid)) this.setState({ typing: [...this.state.typing, uid] });\n const prev = this.typingTimers.get(uid);\n if (prev) this.timers.clearTimeout(prev);\n this.typingTimers.set(\n uid,\n this.timers.setTimeout(() => {\n this.typingTimers.delete(uid);\n this.setState({ typing: this.state.typing.filter((u) => u !== uid) });\n }, TYPING_COALESCE_MS * 2),\n );\n }\n\n // ---- outbound actions ----\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /**\n * Optimistic send: render the echo immediately keyed by clientMessageId, then\n * emit the `send` frame. The server `ack`/`message` reconciles on the seq.\n */\n send(args: { clientMessageId: string; text: string; replyTo?: { messageSeq: number; preview: string } | null }): boolean {\n // C-B8: write the frame FIRST and render the optimistic echo ONLY if it was\n // actually sent over an open socket. A closed socket returns false — no phantom\n // \"sent\" bubble, and the caller keeps the composer text — so a disconnect between\n // render and click can't silently swallow the message.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n replyTo: args.replyTo ?? null,\n });\n if (!sent) {\n this.setState({ lastErrorCode: 'send-failed' });\n return false;\n }\n const now = this.timers.now();\n // Track the un-acked send so a reconnect re-sends it with the same\n // clientMessageId (the DO dedups) — until the ack/echo clears it.\n this.pendingSends.set(args.clientMessageId, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n replyTo: args.replyTo ?? null,\n sentAt: now,\n attempts: 1,\n failed: false,\n });\n const echo = optimisticMessage({\n clientMessageId: args.clientMessageId,\n threadId: this.config.threadId,\n senderId: this.config.currentUserId,\n text: args.text,\n createdAt: now,\n replyTo: args.replyTo ? { messageId: seqToMessageId(args.replyTo.messageSeq), messagePreview: args.replyTo.preview } : null,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n /**\n * Explicit read ack (delivery != read). `focused` => the channel is open + at latest.\n * Remembers the cursor (re-sent on reconnect) and returns whether the frame was\n * actually sent over an open socket, so the caller only advances its local\n * already-acked marker on a real send (M2).\n */\n readAck(readSeq: number, focused: boolean): boolean {\n this.lastReadAck = { readSeq, focused };\n return this.sendFrame(CLIENT_FRAME.READ_ACK, { readSeq, focused });\n }\n\n /** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */\n typing(): void {\n const now = this.timers.now();\n if (now - this.lastTypingSentAt < TYPING_COALESCE_MS) return;\n this.lastTypingSentAt = now;\n this.sendFrame(CLIENT_FRAME.TYPING, {});\n }\n\n presenceSubscribe(): void {\n this.presenceSubscribed = true;\n this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n }\n\n presenceUnsubscribe(): void {\n this.presenceSubscribed = false;\n this.sendFrame(CLIENT_FRAME.PRESENCE_UNSUBSCRIBE, {});\n this.setState({ presence: [] });\n }\n\n /** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */\n fetchOlder(): void {\n if (this.state.isFetchingOlder || !this.state.hasOlder) return;\n const oldest = this.state.messages.find((m) => typeof m.meta?.seq === 'number');\n const beforeSeq = oldest && typeof oldest.meta?.seq === 'number' ? (oldest.meta.seq as number) : null;\n this.setState({ isFetchingOlder: true });\n this.requestHistory(beforeSeq);\n }\n\n private requestHistory(beforeSeq: number | null): void {\n this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\n }\n\n // ---- heartbeat (20s) ----\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const tick = () => {\n if (!this.socket || !this.socket.isOpen) return;\n this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n };\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer != null) {\n this.timers.clearTimeout(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n // ---- teardown ----\n\n /** Permanently close this client (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n this.stopHeartbeat();\n for (const t of this.typingTimers.values()) this.timers.clearTimeout(t);\n this.typingTimers.clear();\n this.overlays.clear();\n this.originalRows.clear();\n this.pendingSends.clear();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.membershipRetryTimer != null) {\n this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed', typing: [], presence: [] });\n }\n}\n\nfunction sortBySeq(a: ChatMessageV1, b: ChatMessageV1): number {\n const sa = typeof a.meta?.seq === 'number' ? (a.meta.seq as number) : Number.POSITIVE_INFINITY;\n const sb = typeof b.meta?.seq === 'number' ? (b.meta.seq as number) : Number.POSITIVE_INFINITY;\n if (sa !== sb) return sa - sb;\n return a.createdAt - b.createdAt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"channel-client.js","sourceRoot":"","sources":["../../src/realtime/channel-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Channel realtime client: ONE WebSocket to a single channel DO, driving the\n// Contract C wire protocol. Framework-neutral (a plain observable store the React\n// hook subscribes to) so it is unit-testable with a mock socket and no React.\n//\n// Responsibilities (Contract C):\n// - connect via subprotocol auth (grant offered by the injected grantProvider)\n// - receive versioned `message` frames; server owns `seq`\n// - optimistic send keyed by clientMessageId, reconciled on the server `ack` seq\n// - explicit read acks (delivery != read; focus signals \"read\")\n// - typing (>= 2s coalesced per the V1 limit)\n// - presence-subscribe/-unsubscribe + a 20s client heartbeat\n// - reconnect (realtime-core controller) with a resume cursor -> snapshot then deltas\n// - history pagination over the socket (page <= 50, epoch-aware via seq)\n// - error codes incl. 4401 AUTH_EXPIRED -> re-mint a grant ONCE via grantProvider\n//\n// Timers are injected (setTimeout/clearTimeout/now) so tests run deterministically.\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireMessageRow,\n type WireChannelSnapshot,\n type RevisionKind,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport {\n wireRowToMessage,\n optimisticMessage,\n seqToMessageId,\n applyModerationOverlay,\n overlayFromRow,\n type ModerationOverlay,\n} from './map.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n HEARTBEAT_MS,\n TYPING_COALESCE_MS,\n HISTORY_PAGE_MAX,\n MAX_PENDING_SEND_ATTEMPTS,\n PENDING_SEND_MAX_AGE_MS,\n} from './shared.js';\n\n/** An un-acked send tracked for reconnect resend (clientMessageId idempotency). */\ninterface PendingSend {\n clientMessageId: string;\n text: string;\n replyTo: { messageSeq: number; preview: string } | null;\n /** When the send was first attempted (for the age cap). Reset by retrySend(). */\n sentAt: number;\n /** How many times it has been (re)sent over a socket. Reset by retrySend(). */\n attempts: number;\n /** True once the attempt/age cap flipped this send to the visible failed state.\n * A failed entry is RETAINED (not deleted) so retrySend() can re-queue it with\n * the SAME clientMessageId; it is skipped by the automatic reconnect resend. */\n failed: boolean;\n}\n\nexport interface ChannelClientState {\n status: RealtimeStatus;\n /** Ordered ascending by seq; optimistic messages (no seq yet) sort to the tail. */\n messages: ChatMessageV1[];\n /** uids currently typing (cleared after ~TYPING_COALESCE_MS of silence). */\n typing: string[];\n /** uids the DO reports online (presence). Empty until presence-subscribe. */\n presence: string[];\n /** True while an older-history page request is in flight. */\n isFetchingOlder: boolean;\n /** False once a history page returns fewer than the requested limit (reached the head). */\n hasOlder: boolean;\n /** The last structured error code the DO sent (e.g. 'blocked-word', 'too-long', flood reason). */\n lastErrorCode: string | null;\n /**\n * True once the FIRST authoritative chat data has been applied — a non-resync\n * resume snapshot, OR the first history page (INCLUDING an empty one). Drives the\n * hook's `isInitialLoading` so the shell shows an honest \"Opening chat…\" state\n * while the first data is loading, and a real empty-chat surface only after an\n * authoritative empty result. NEVER inferred from `messages.length === 0` (an\n * empty first history page IS a successful load). Once true it stays true — a\n * later disconnect/reconnect keeps loaded messages mounted, not a re-loader.\n * A grant retry / socket open before any snapshot leaves it false.\n */\n hasLoadedInitialData: boolean;\n}\n\nexport interface ChannelClientConfig {\n /** WebSocket endpoint base, e.g. `wss://chat.ttt.productions`. The client appends `/channel`. */\n endpoint: string;\n /** The logical thread id the UI keys on (used as ChatMessageV1.threadId). */\n threadId: string;\n /** The authenticated uid (sender identity for optimistic echoes + auth-switch guard). */\n currentUserId: string;\n /** Mints/refreshes a channel grant. Called on connect and once on a 4401 re-grant. */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n /** Reconnect backoff tuning (forwarded to realtime-core). */\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n}\n\nconst INITIAL: ChannelClientState = {\n status: 'idle',\n messages: [],\n typing: [],\n presence: [],\n isFetchingOlder: false,\n hasOlder: true,\n lastErrorCode: null,\n hasLoadedInitialData: false,\n};\n\n/**\n * Retryable error-frame code the channel DO sends when a send arrives BEFORE the\n * member row has synced through the async backend→worker pipeline (a just-created\n * invite/channel). The socket stays open; the client re-sends after `retryAfterMs`\n * through the normal resendPendingSends machinery (same caps). The string is part\n * of the wire contract with the worker runtime's send handler.\n */\nconst MEMBERSHIP_PENDING_CODE = 'membership-pending';\n/** Fallback retry delay when the frame omits/breaks `retryAfterMs`. */\nconst MEMBERSHIP_PENDING_DEFAULT_RETRY_MS = 2000;\n\nexport class ChannelClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: ChannelClientState = INITIAL;\n private readonly listeners = new Set<(s: ChannelClientState) => void>();\n\n /** Resume cursor: highest seq we've durably applied. Sent on resume after reconnect. */\n private resumeSeq = 0;\n /** Last read-ack the UI requested; re-sent on reconnect so a drop during a closed\n * socket window is recovered (M2). */\n private lastReadAck: { readSeq: number; focused: boolean } | null = null;\n /** True after a `4401` re-grant has been attempted for the CURRENT connect cycle (once-only). */\n private reauthAttempted = false;\n private presenceSubscribed = false;\n private heartbeatTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private typingTimers = new Map<string, ReturnType<TransportTimers['setTimeout']>>();\n private lastTypingSentAt = Number.NEGATIVE_INFINITY;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n /** Single scheduled re-send for the membership-pending window (never stacks). */\n private membershipRetryTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /**\n * Per-seq moderation overlay (the max-revision wins). A `revision` frame writes\n * here keyed by `messageSeq` so it applies whether it arrives BEFORE its base\n * row (re-applied when the row later renders) or AFTER (re-renders the present\n * row in place). Merged with the row's own inline `moderationKind` so a row that\n * arrives already-moderated from the DO AND one covered by a live frame both\n * render the moderated state. */\n private overlays = new Map<number, ModerationOverlay>();\n /**\n * The ORIGINAL (un-overlaid) mapped message per seq. A `restore` revision must\n * revert a previously-redacted message to its original text, so re-renders start\n * from this cache rather than the (possibly already-blanked) message in state. */\n private originalRows = new Map<number, ChatMessageV1>();\n /**\n * Un-acked sends keyed by clientMessageId. A send is added here when its frame is\n * written and removed when its `ack` (or the canonical `message` echo carrying the\n * same clientMessageId) arrives. On every reconnect resume they are RE-SENT with\n * the SAME clientMessageId — the DO dedups (re-acks without re-broadcast), so a\n * send that lost its socket before the ack is delivered exactly once. A pending\n * send that exceeds the attempt/age cap is flipped to a visible failed state\n * (retry affordance) instead of staying a ghost. */\n private pendingSends = new Map<string, PendingSend>();\n\n constructor(private readonly config: ChannelClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n }\n\n // ---- observable store ----\n\n getState(): ChannelClientState {\n return this.state;\n }\n\n subscribe(fn: (s: ChannelClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<ChannelClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n // ---- connection lifecycle ----\n\n async connect(): Promise<void> {\n // Idempotency guard (C-B7): connect() only starts a lifecycle from a fresh\n // (idle) or fully torn-down (closed) state. A second connect() while one is\n // already connecting / open / reconnecting is a no-op, so a stray double-owner\n // can't open a second WebSocket. The reconnect path uses openSocket() /\n // scheduleReconnect() directly (never connect()), so this never blocks a\n // legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n this.closedByUs = false;\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app's grant provider translated an\n // authoritative policy \"no\" into the package-owned ChatAccessDeniedError)\n // must NOT reconnect — the next mint would just re-deny, stranding the shell\n // on the opening loader forever. Every other mint failure (network, transient\n // 'unavailable'/'internal', post-login 'unauthenticated') stays retryable and\n // backs off exactly as before.\n if (isChatAccessDeniedError(err)) return this.denyAccessTerminally();\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/channel`;\n this.socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => this.onOpen(),\n onMessage: (data) => this.onMessage(data),\n onClose: (code, reason) => this.onClose(code, reason),\n onError: () => this.onError(),\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n // Resume: ask the DO for the authoritative snapshot, then live deltas flow. The\n // field name MUST be `afterSeq` — the Channel DO reads `payload.afterSeq`; any other\n // name reads as cursorless and forces a full resync every reconnect.\n this.sendFrame(CLIENT_FRAME.RESUME, { afterSeq: this.resumeSeq });\n // If the UI had presence open, re-subscribe after a reconnect.\n if (this.presenceSubscribed) this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n // Re-send the last read cursor so a read-ack dropped during the disconnect window\n // (sendFrame returns false on a closed socket) is not lost until the next message.\n if (this.lastReadAck) this.sendFrame(CLIENT_FRAME.READ_ACK, { ...this.lastReadAck });\n // Re-send every un-acked pending send with its ORIGINAL clientMessageId so a send\n // that lost its socket before the ack is not silently dropped. The DO dedups\n // (re-acks, no re-broadcast, no flood-token) — this is the client half of the\n // Contract C send-idempotency the server side was built for.\n this.resendPendingSends();\n this.startHeartbeat();\n }\n\n /**\n * On reconnect, re-send each un-acked pending send with the SAME clientMessageId\n * (the DO dedups). A send that has exceeded the attempt or age cap is flipped to a\n * visible failed state instead of being resent forever — the ghost becomes a\n * retryable failure the UI can surface.\n */\n private resendPendingSends(): void {\n if (this.pendingSends.size === 0) return;\n const now = this.timers.now();\n for (const pending of [...this.pendingSends.values()]) {\n // Already flipped to the visible failed state — only an explicit retrySend()\n // re-queues it; the automatic reconnect resend leaves it alone.\n if (pending.failed) continue;\n const tooOld = now - pending.sentAt >= PENDING_SEND_MAX_AGE_MS;\n const tooManyAttempts = pending.attempts >= MAX_PENDING_SEND_ATTEMPTS;\n if (tooOld || tooManyAttempts) {\n this.failPendingSend(pending.clientMessageId);\n continue;\n }\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n if (sent) pending.attempts += 1;\n }\n }\n\n /**\n * Flip an un-acked optimistic row to a visible failed state. The pending entry is\n * RETAINED (marked `failed`) so retrySend() can re-queue it with the SAME\n * clientMessageId; the row is marked `meta.sendFailed` so the default renderer\n * shows a failure with a retry affordance rather than an indistinguishable\n * \"sent\" bubble. A late ack/echo still reconciles + clears it.\n */\n private failPendingSend(clientMessageId: string): void {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending || pending.failed) return;\n pending.failed = true;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId\n ? { ...m, meta: { ...m.meta, sendFailed: true } }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: 'send-failed' });\n }\n\n /** One timer for the membership-pending re-send window; a newer frame resets it. */\n private scheduleMembershipPendingRetry(delayMs: number): void {\n if (this.membershipRetryTimer != null) this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = this.timers.setTimeout(() => {\n this.membershipRetryTimer = null;\n if (this.closedByUs) return;\n this.resendPendingSends();\n }, delayMs);\n }\n\n /**\n * Flip EVERY un-acked pending send to the visible failed state. Called on the\n * terminal close paths (4403 REVOKED, reconnect give-up) — without this, a send\n * in flight when the socket dies for good shows \"Sending…\" until unmount.\n */\n private failAllPendingSends(): void {\n for (const clientMessageId of [...this.pendingSends.keys()]) {\n this.failPendingSend(clientMessageId);\n }\n }\n\n /**\n * Retry a FAILED (or still-pending) send, RE-USING its original clientMessageId\n * so the DO's send-idempotency dedups against any copy that actually landed.\n * Resets the attempt/age budget, clears the row's `sendFailed` marker (back to\n * the normal pending/optimistic look), and re-sends immediately when the socket\n * is open — otherwise the entry rides the next reconnect's automatic resend.\n * Returns false when the clientMessageId is not a tracked pending send (already\n * acked/reconciled, or never sent from this client).\n */\n retrySend(clientMessageId: string): boolean {\n const pending = this.pendingSends.get(clientMessageId);\n if (!pending) return false;\n pending.failed = false;\n pending.sentAt = this.timers.now();\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: pending.clientMessageId,\n text: pending.text,\n replyTo: pending.replyTo,\n });\n pending.attempts = sent ? 1 : 0;\n const next = this.state.messages.map((m) =>\n m.meta?.optimistic && m.meta?.clientMessageId === clientMessageId && m.meta?.sendFailed\n ? { ...m, meta: { ...m.meta, sendFailed: false } }\n : m,\n );\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n private onError(): void {\n // The close handler does the backoff; onerror is advisory in browsers.\n }\n\n private onClose(code: number, _reason: string): void {\n this.stopHeartbeat();\n this.socket = null;\n if (this.closedByUs) {\n this.setState({ status: 'closed' });\n return;\n }\n // 4401 AUTH_EXPIRED: re-mint a grant ONCE and reconnect immediately, no backoff.\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n // 4403 REVOKED: access pulled. Stop — a reconnect would just re-deny at mint.\n // Terminal: flip every un-acked pending send to the visible failed state first\n // (failPendingSend sets lastErrorCode 'send-failed'; the 'revoked' set below\n // wins as the final surfaced code).\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'revoked' });\n return;\n }\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n if (delay == null) {\n // Reconnect give-up: terminal, same as REVOKED — pending sends must not stay\n // an eternal \"Sending…\"; flip them to the visible failed/retry state.\n this.failAllPendingSends();\n this.setState({ status: 'closed' });\n return;\n }\n this.setState({ status: 'reconnecting' });\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs) return;\n void this.openSocket();\n }, delay);\n }\n\n /**\n * Terminal access denial surfaced by the grant provider (ChatAccessDeniedError).\n * A reconnect would just re-deny at the next mint, so stop for good — the same\n * terminal posture as a 4403 REVOKED close: mark closed-by-us, close the reconnect\n * controller, and flip every un-acked pending send to the visible failed state.\n * `access-denied` is set AFTER failAllPendingSends so its 'send-failed' code does\n * not win as the surfaced error; the React hook maps 'access-denied' to\n * `allowed: false` so the shell renders its existing no-access surface.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.failAllPendingSends();\n this.setState({ status: 'closed', lastErrorCode: 'access-denied' });\n }\n\n // ---- inbound frames ----\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.type) return;\n const payload = frame.payload ?? {};\n switch (frame.type) {\n case 'message':\n return this.applyMessage((payload as { message: WireMessageRow }).message);\n case 'ack':\n return this.applyAck(payload as { clientMessageId?: string; seq?: number; readSeq?: number });\n case 'history-page':\n return this.applyHistoryPage((payload as { messages: WireMessageRow[] }).messages ?? []);\n case 'presence':\n return this.setState({ presence: (payload as { online: string[] }).online ?? [] });\n case 'typing':\n return this.applyTyping((payload as { uid: string }).uid);\n case 'snapshot':\n return this.applyChannelSnapshot(payload as unknown as WireChannelSnapshot);\n case 'error': {\n const code = String((payload as { code?: unknown }).code ?? 'error');\n // Retryable: membership row not yet synced to the DO. Keep the send pending\n // (no visible error — the optimistic bubble stays \"Sending…\") and re-send\n // after the server-suggested delay via the normal resend machinery, whose\n // attempt/age caps flip it to the visible failed state if the window never\n // closes. Everything else surfaces as before.\n if (code === MEMBERSHIP_PENDING_CODE) {\n const raw = (payload as { retryAfterMs?: unknown }).retryAfterMs;\n const delay = typeof raw === 'number' && raw > 0 ? raw : MEMBERSHIP_PENDING_DEFAULT_RETRY_MS;\n return this.scheduleMembershipPendingRetry(delay);\n }\n return this.setState({ lastErrorCode: code });\n }\n case 'revision':\n return this.applyRevisionFrame(payload as { messageSeq?: number; kind?: RevisionKind; messageRevision?: number });\n default:\n return; // unknown/optional types tolerated (forward compat)\n }\n }\n\n /**\n * Apply a live moderation `revision` frame: record the overlay keyed by seq\n * (max-`messageRevision` wins — a stale/older frame is ignored) and re-render the\n * matching message in place if it is already loaded. If the base row hasn't\n * arrived yet the overlay is retained and re-applied when the row renders, so a\n * frame that races ahead of its message never lets the original content show.\n */\n private applyRevisionFrame(payload: { messageSeq?: number; kind?: RevisionKind; messageRevision?: number }): void {\n const seq = payload.messageSeq;\n const kind = payload.kind;\n const messageRevision = payload.messageRevision;\n if (typeof seq !== 'number' || kind == null || typeof messageRevision !== 'number') return;\n const prev = this.overlays.get(seq);\n // Max-revision merge: a higher messageRevision supersedes; an older one is dropped.\n if (prev && prev.messageRevision >= messageRevision) return;\n this.overlays.set(seq, { kind, messageRevision });\n // Re-render the matching message in place if it is already present. Re-derive\n // from the ORIGINAL row so a `restore` reverts a previously-blanked text.\n const messageId = seqToMessageId(seq);\n const original = this.originalRows.get(seq);\n if (original && this.state.messages.some((m) => m.messageId === messageId)) {\n const overlaid = applyModerationOverlay(original, this.effectiveOverlay(seq, null));\n const next = this.state.messages.map((m) => (m.messageId === messageId ? overlaid : m));\n this.setState({ messages: next });\n }\n }\n\n /**\n * The effective overlay for a seq = the max-`messageRevision` between the row's\n * own inline overlay (from the DO history/delta merge) and any received `revision`\n * frame stored in `this.overlays`. Either source may be ahead of the other.\n */\n private effectiveOverlay(seq: number, rowOverlay: ModerationOverlay | null): ModerationOverlay | null {\n const fromFrame = this.overlays.get(seq) ?? null;\n if (!rowOverlay) return fromFrame;\n if (!fromFrame) return rowOverlay;\n return fromFrame.messageRevision >= rowOverlay.messageRevision ? fromFrame : rowOverlay;\n }\n\n /** Map a DO row to a UI message with its effective moderation overlay applied. */\n private renderRow(row: WireMessageRow): ChatMessageV1 {\n const mapped = wireRowToMessage(row, this.config.threadId);\n // Cache the ORIGINAL so a later `restore` frame can revert a blanked message.\n this.originalRows.set(row.seq, mapped);\n return applyModerationOverlay(mapped, this.effectiveOverlay(row.seq, overlayFromRow(row)));\n }\n\n /**\n * Apply the Contract C resume snapshot. The DO always returns the authoritative\n * tail + read cursor and, per the pinned contract, EITHER a contiguous delta\n * (`resync:false`) OR a resync directive (`resync:true`).\n *\n * - `resync === false`: apply the contiguous `delta` (the ≤500-message backlog\n * after our cursor, oldest-first) through the same render/merge path as a live\n * `message`, then advance `resumeSeq` to `lastMessageSeq`. This heals ANY gap\n * within the backlog — including moderation `revision`s the DO baked into the\n * delta rows — with no history fetch, so a >50-message reconnect gap is no\n * longer a permanent hole.\n * - `resync === true`: the gap is beyond the backlog (or we sent no cursor). Drop\n * the local tail (messages + overlays + original-row cache) and re-page history\n * from the top via a cursorless `requestHistory(null)`.\n *\n * A pre-contract DO (no `resync`/`delta`) is treated as `resync:true` ⇒ the safe\n * re-page path, so a stale worker never strands the client. An empty delta with\n * `resync:false` (already caught up) is a no-op beyond advancing the cursor.\n */\n private applyChannelSnapshot(snap: WireChannelSnapshot): void {\n // Guard: an inbox snapshot must never reach a channel client.\n if (isInboxSnapshot(snap as never)) return;\n if (typeof snap.lastMessageSeq !== 'number') return;\n\n // Absent `resync` (legacy/pre-contract DO) is treated as a resync directive.\n const resync = snap.resync !== false;\n\n if (!resync) {\n // Apply the contiguous delta oldest-first, exactly like live messages, then\n // advance the cursor to the authoritative tail so the NEXT resume tells the DO\n // we're caught up only after we actually applied the backlog.\n const delta = Array.isArray(snap.delta) ? snap.delta : [];\n for (const row of delta) this.applyMessage(row);\n if (snap.lastMessageSeq > this.resumeSeq) this.resumeSeq = snap.lastMessageSeq;\n // A non-resync snapshot IS the first authoritative data (even an empty delta =\n // caught up) — end initial loading. A resync snapshot instead defers to the\n // re-paged first history page below, so it must NOT end it here.\n if (!this.state.hasLoadedInitialData) this.setState({ hasLoadedInitialData: true });\n return;\n }\n\n // Resync: drop the local tail and re-page from the top per the pinned contract.\n // Clear the derived caches too so a stale overlay/original-row can't resurrect a\n // message the re-page will reload fresh (with its baked-in moderation state).\n // KEEP un-acked optimistic rows — they aren't server history yet; their pending\n // sends were already re-sent in onOpen and reconcile when the DO echoes them.\n this.overlays.clear();\n this.originalRows.clear();\n this.resumeSeq = 0;\n const keptOptimistic = this.state.messages.filter((m) => m.meta?.optimistic);\n this.setState({ messages: keptOptimistic, hasOlder: true, isFetchingOlder: false });\n this.requestHistory(null);\n }\n\n private applyMessage(row: WireMessageRow): void {\n if (!row || typeof row.seq !== 'number') return;\n const mapped = this.renderRow(row);\n this.mergeMessage(mapped, row);\n if (row.seq > this.resumeSeq) this.resumeSeq = row.seq;\n }\n\n /**\n * Merge a server message: reconcile an optimistic echo by clientMessageId (swap\n * the `optimistic:` row for the canonical seq row), or insert/replace by seq.\n * Keeps the list sorted ascending by seq with optimistic rows at the tail.\n */\n private mergeMessage(mapped: ChatMessageV1, row: WireMessageRow): void {\n // If this canonical row carries a clientMessageId we still have pending (the\n // sender's own broadcast, or a resume delta that included our send), the send\n // landed — stop tracking it for reconnect resend.\n if (row.clientMessageId) this.pendingSends.delete(row.clientMessageId);\n const next = this.state.messages.filter(\n (m) =>\n m.messageId !== mapped.messageId &&\n // drop the optimistic placeholder this server row reconciles\n (m.meta?.clientMessageId !== row.clientMessageId || !m.meta?.optimistic),\n );\n next.push(mapped);\n next.sort(sortBySeq);\n this.setState({ messages: next });\n }\n\n private applyAck(payload: { clientMessageId?: string; seq?: number; readSeq?: number }): void {\n // A read-ack echo carries readSeq only — nothing to reconcile locally.\n if (payload.clientMessageId == null || typeof payload.seq !== 'number') return;\n // Reconcile the optimistic echo: stamp the server seq onto it. The canonical\n // `message` broadcast (which the sender also receives) replaces it fully; this\n // ack guarantees the seq even if the broadcast is dropped/reordered.\n const seq = payload.seq;\n const cmid = payload.clientMessageId;\n // The send is acked — stop tracking it for reconnect resend.\n this.pendingSends.delete(cmid);\n const next = this.state.messages.map((m) => {\n if (!m.meta?.optimistic || m.meta?.clientMessageId !== cmid) return m;\n // Strip any failed marker — a late ack (e.g. a retry raced the cap) means the\n // send actually landed, so the row must render as delivered, not failed.\n const { sendFailed: _sendFailed, ...restMeta } = m.meta;\n return { ...m, messageId: seqToMessageId(seq), meta: { ...restMeta, optimistic: false, seq } };\n });\n next.sort(sortBySeq);\n if (seq > this.resumeSeq) this.resumeSeq = seq;\n this.setState({ messages: next });\n }\n\n private applyHistoryPage(rows: WireMessageRow[]): void {\n const mapped = rows.map((r) => this.renderRow(r));\n // Mirror mergeMessage's optimistic reconciliation: a history row carrying a\n // clientMessageId we still hold as an optimistic placeholder IS that send (the\n // DO stored it; the ack was lost). Drop the placeholder — keyed `optimistic:{cmid}`,\n // it would otherwise survive alongside its seq-keyed server row as a duplicate\n // bubble after a resync + re-page — and stop tracking it for resend.\n const incomingCmids = new Set<string>();\n for (const r of rows) {\n if (r.clientMessageId) {\n incomingCmids.add(r.clientMessageId);\n this.pendingSends.delete(r.clientMessageId);\n }\n }\n const bySeq = new Map<string, ChatMessageV1>();\n for (const m of this.state.messages) {\n if (m.meta?.optimistic && typeof m.meta?.clientMessageId === 'string' && incomingCmids.has(m.meta.clientMessageId)) {\n continue; // reconciled by the incoming server row\n }\n bySeq.set(m.messageId, m);\n }\n for (const m of mapped) bySeq.set(m.messageId, m);\n const merged = [...bySeq.values()].sort(sortBySeq);\n for (const r of rows) if (r.seq > this.resumeSeq) this.resumeSeq = r.seq;\n this.setState({\n messages: merged,\n isFetchingOlder: false,\n hasOlder: rows.length >= HISTORY_PAGE_MAX,\n // The first history page — INCLUDING an empty one — is a successful authoritative\n // load, so it ends initial loading. (Older-page fetches redundantly re-set true,\n // which is a harmless no-op since the flag never flips back to false.)\n hasLoadedInitialData: true,\n });\n }\n\n private applyTyping(uid: string): void {\n if (!uid || uid === this.config.currentUserId) return;\n if (!this.state.typing.includes(uid)) this.setState({ typing: [...this.state.typing, uid] });\n const prev = this.typingTimers.get(uid);\n if (prev) this.timers.clearTimeout(prev);\n this.typingTimers.set(\n uid,\n this.timers.setTimeout(() => {\n this.typingTimers.delete(uid);\n this.setState({ typing: this.state.typing.filter((u) => u !== uid) });\n }, TYPING_COALESCE_MS * 2),\n );\n }\n\n // ---- outbound actions ----\n\n private sendFrame(type: string, payload: Record<string, unknown>): boolean {\n if (!this.socket || !this.socket.isOpen) return false;\n this.socket.send(buildFrame(type, payload));\n return true;\n }\n\n /**\n * Optimistic send: render the echo immediately keyed by clientMessageId, then\n * emit the `send` frame. The server `ack`/`message` reconciles on the seq.\n */\n send(args: { clientMessageId: string; text: string; replyTo?: { messageSeq: number; preview: string } | null }): boolean {\n // C-B8: write the frame FIRST and render the optimistic echo ONLY if it was\n // actually sent over an open socket. A closed socket returns false — no phantom\n // \"sent\" bubble, and the caller keeps the composer text — so a disconnect between\n // render and click can't silently swallow the message.\n const sent = this.sendFrame(CLIENT_FRAME.SEND, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n replyTo: args.replyTo ?? null,\n });\n if (!sent) {\n this.setState({ lastErrorCode: 'send-failed' });\n return false;\n }\n const now = this.timers.now();\n // Track the un-acked send so a reconnect re-sends it with the same\n // clientMessageId (the DO dedups) — until the ack/echo clears it.\n this.pendingSends.set(args.clientMessageId, {\n clientMessageId: args.clientMessageId,\n text: args.text,\n replyTo: args.replyTo ?? null,\n sentAt: now,\n attempts: 1,\n failed: false,\n });\n const echo = optimisticMessage({\n clientMessageId: args.clientMessageId,\n threadId: this.config.threadId,\n senderId: this.config.currentUserId,\n text: args.text,\n createdAt: now,\n replyTo: args.replyTo ? { messageId: seqToMessageId(args.replyTo.messageSeq), messagePreview: args.replyTo.preview } : null,\n });\n const next = [...this.state.messages, echo].sort(sortBySeq);\n this.setState({ messages: next, lastErrorCode: null });\n return true;\n }\n\n /**\n * Explicit read ack (delivery != read). `focused` => the channel is open + at latest.\n * Remembers the cursor (re-sent on reconnect) and returns whether the frame was\n * actually sent over an open socket, so the caller only advances its local\n * already-acked marker on a real send (M2).\n */\n readAck(readSeq: number, focused: boolean): boolean {\n this.lastReadAck = { readSeq, focused };\n return this.sendFrame(CLIENT_FRAME.READ_ACK, { readSeq, focused });\n }\n\n /** Coalesced typing signal: at most one frame per TYPING_COALESCE_MS. */\n typing(): void {\n const now = this.timers.now();\n if (now - this.lastTypingSentAt < TYPING_COALESCE_MS) return;\n this.lastTypingSentAt = now;\n this.sendFrame(CLIENT_FRAME.TYPING, {});\n }\n\n presenceSubscribe(): void {\n this.presenceSubscribed = true;\n this.sendFrame(CLIENT_FRAME.PRESENCE_SUBSCRIBE, {});\n }\n\n presenceUnsubscribe(): void {\n this.presenceSubscribed = false;\n this.sendFrame(CLIENT_FRAME.PRESENCE_UNSUBSCRIBE, {});\n this.setState({ presence: [] });\n }\n\n /** Fetch the page of history older than the oldest loaded message (epoch-aware by seq). */\n fetchOlder(): void {\n if (this.state.isFetchingOlder || !this.state.hasOlder) return;\n const oldest = this.state.messages.find((m) => typeof m.meta?.seq === 'number');\n const beforeSeq = oldest && typeof oldest.meta?.seq === 'number' ? (oldest.meta.seq as number) : null;\n this.setState({ isFetchingOlder: true });\n this.requestHistory(beforeSeq);\n }\n\n private requestHistory(beforeSeq: number | null): void {\n this.sendFrame(CLIENT_FRAME.HISTORY, { beforeSeq, limit: HISTORY_PAGE_MAX });\n }\n\n // ---- heartbeat (20s) ----\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const tick = () => {\n if (!this.socket || !this.socket.isOpen) return;\n this.sendFrame(CLIENT_FRAME.HEARTBEAT, {});\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n };\n this.heartbeatTimer = this.timers.setTimeout(tick, HEARTBEAT_MS);\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer != null) {\n this.timers.clearTimeout(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n // ---- teardown ----\n\n /** Permanently close this client (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n this.stopHeartbeat();\n for (const t of this.typingTimers.values()) this.timers.clearTimeout(t);\n this.typingTimers.clear();\n this.overlays.clear();\n this.originalRows.clear();\n this.pendingSends.clear();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.membershipRetryTimer != null) {\n this.timers.clearTimeout(this.membershipRetryTimer);\n this.membershipRetryTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed', typing: [], presence: [] });\n }\n}\n\nfunction sortBySeq(a: ChatMessageV1, b: ChatMessageV1): number {\n const sa = typeof a.meta?.seq === 'number' ? (a.meta.seq as number) : Number.POSITIVE_INFINITY;\n const sb = typeof b.meta?.seq === 'number' ? (b.meta.seq as number) : Number.POSITIVE_INFINITY;\n if (sa !== sb) return sa - sb;\n return a.createdAt - b.createdAt;\n}\n"]}
|
|
@@ -9,6 +9,15 @@ export interface InboxClientState {
|
|
|
9
9
|
hasUnread: boolean;
|
|
10
10
|
/** The set of channelRefs that currently carry an unread dot (per-row dots). */
|
|
11
11
|
unreadChannelRefs: string[];
|
|
12
|
+
/**
|
|
13
|
+
* The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):
|
|
14
|
+
* `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`
|
|
15
|
+
* (a genuine, terminal policy "no" — e.g. a banned/suspended account), and
|
|
16
|
+
* `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient
|
|
17
|
+
* reconnects. The app subscribes to inbox state directly (there is no inbox React
|
|
18
|
+
* hook in this package), so this stable code is the surface — it invents no new UI.
|
|
19
|
+
*/
|
|
20
|
+
lastErrorCode: string | null;
|
|
12
21
|
}
|
|
13
22
|
export interface InboxClientConfig {
|
|
14
23
|
endpoint: string;
|
|
@@ -43,6 +52,15 @@ export declare class InboxClient {
|
|
|
43
52
|
private openSocket;
|
|
44
53
|
private onOpen;
|
|
45
54
|
private onClose;
|
|
55
|
+
/**
|
|
56
|
+
* Terminal access denial surfaced by the inbox grant provider
|
|
57
|
+
* (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark
|
|
58
|
+
* closed-by-us, close the reconnect controller, and surface the stable
|
|
59
|
+
* `access-denied` code. The inbox client tracks NO optimistic/pending sends
|
|
60
|
+
* (mark-read is fire-and-forget with no local pending state), so — unlike
|
|
61
|
+
* ChannelClient — there is nothing to fail here; only the lifecycle stops.
|
|
62
|
+
*/
|
|
63
|
+
private denyAccessTerminally;
|
|
46
64
|
private scheduleReconnect;
|
|
47
65
|
private onMessage;
|
|
48
66
|
private applySnapshot;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,uFAAuF;IACvF,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,iBAAiB,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"AAWA,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,uFAAuF;IACvF,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,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;CAC/G;AAID,qBAAa,WAAW;IAUV,OAAO,CAAC,QAAQ,CAAC,MAAM;IATnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAsB;IACjD,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAA0D;IAChF,OAAO,CAAC,UAAU,CAAS;gBAEE,MAAM,EAAE,iBAAiB;IAKtD,QAAQ,IAAI,gBAAgB;IAI5B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD,OAAO,CAAC,QAAQ;IAKV,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAOhB,UAAU;IA2BxB,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,OAAO;IAqBf;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,SAAS;IAejB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,SAAS;IAMjB,yEAAyE;IACzE,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI7C;;;;;;;OAOG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIrC,kEAAkE;IAClE,KAAK,IAAI,IAAI;CAad"}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// 4401 re-grant once, 4403 stop, auth-switch teardown).
|
|
10
10
|
import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
11
11
|
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
12
|
-
import { defaultTimers } from './shared.js';
|
|
13
|
-
const INITIAL = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [] };
|
|
12
|
+
import { defaultTimers, isChatAccessDeniedError } from './shared.js';
|
|
13
|
+
const INITIAL = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };
|
|
14
14
|
export class InboxClient {
|
|
15
15
|
config;
|
|
16
16
|
timers;
|
|
@@ -49,7 +49,14 @@ export class InboxClient {
|
|
|
49
49
|
try {
|
|
50
50
|
grantToken = await this.config.grantProvider();
|
|
51
51
|
}
|
|
52
|
-
catch {
|
|
52
|
+
catch (err) {
|
|
53
|
+
// A TERMINAL access denial (the app translated an authoritative policy "no"
|
|
54
|
+
// — e.g. Firebase `permission-denied` for a banned/suspended account — into the
|
|
55
|
+
// package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would
|
|
56
|
+
// just re-deny, reconnect-looping forever and warning every cycle. Every other
|
|
57
|
+
// mint failure (network/transient) stays retryable and backs off as before.
|
|
58
|
+
if (isChatAccessDeniedError(err))
|
|
59
|
+
return this.denyAccessTerminally();
|
|
53
60
|
return this.scheduleReconnect();
|
|
54
61
|
}
|
|
55
62
|
if (this.closedByUs)
|
|
@@ -88,11 +95,24 @@ export class InboxClient {
|
|
|
88
95
|
if (code === CHAT_CLOSE_CODES.REVOKED) {
|
|
89
96
|
this.closedByUs = true;
|
|
90
97
|
this.controller.close();
|
|
91
|
-
this.setState({ status: 'closed' });
|
|
98
|
+
this.setState({ status: 'closed', lastErrorCode: 'revoked' });
|
|
92
99
|
return;
|
|
93
100
|
}
|
|
94
101
|
this.scheduleReconnect();
|
|
95
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Terminal access denial surfaced by the inbox grant provider
|
|
105
|
+
* (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark
|
|
106
|
+
* closed-by-us, close the reconnect controller, and surface the stable
|
|
107
|
+
* `access-denied` code. The inbox client tracks NO optimistic/pending sends
|
|
108
|
+
* (mark-read is fire-and-forget with no local pending state), so — unlike
|
|
109
|
+
* ChannelClient — there is nothing to fail here; only the lifecycle stops.
|
|
110
|
+
*/
|
|
111
|
+
denyAccessTerminally() {
|
|
112
|
+
this.closedByUs = true;
|
|
113
|
+
this.controller.close();
|
|
114
|
+
this.setState({ status: 'closed', lastErrorCode: 'access-denied' });
|
|
115
|
+
}
|
|
96
116
|
scheduleReconnect() {
|
|
97
117
|
const delay = this.controller.onClose();
|
|
98
118
|
if (delay == null) {
|