@ttt-productions/chat-react 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/realtime/channel-client.d.ts +104 -0
  6. package/dist/realtime/channel-client.d.ts.map +1 -0
  7. package/dist/realtime/channel-client.js +356 -0
  8. package/dist/realtime/channel-client.js.map +1 -0
  9. package/dist/realtime/inbox-client.d.ts +55 -0
  10. package/dist/realtime/inbox-client.d.ts.map +1 -0
  11. package/dist/realtime/inbox-client.js +173 -0
  12. package/dist/realtime/inbox-client.js.map +1 -0
  13. package/dist/realtime/index.d.ts +16 -0
  14. package/dist/realtime/index.d.ts.map +1 -0
  15. package/dist/realtime/index.js +14 -0
  16. package/dist/realtime/index.js.map +1 -0
  17. package/dist/realtime/map.d.ts +31 -0
  18. package/dist/realtime/map.d.ts.map +1 -0
  19. package/dist/realtime/map.js +70 -0
  20. package/dist/realtime/map.js.map +1 -0
  21. package/dist/realtime/shared.d.ts +24 -0
  22. package/dist/realtime/shared.d.ts.map +1 -0
  23. package/dist/realtime/shared.js +19 -0
  24. package/dist/realtime/shared.js.map +1 -0
  25. package/dist/realtime/socket.d.ts +33 -0
  26. package/dist/realtime/socket.d.ts.map +1 -0
  27. package/dist/realtime/socket.js +43 -0
  28. package/dist/realtime/socket.js.map +1 -0
  29. package/dist/realtime/transport.d.ts +48 -0
  30. package/dist/realtime/transport.d.ts.map +1 -0
  31. package/dist/realtime/transport.js +47 -0
  32. package/dist/realtime/transport.js.map +1 -0
  33. package/dist/realtime/useRealtimeChatMessages.d.ts +23 -0
  34. package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -0
  35. package/dist/realtime/useRealtimeChatMessages.js +71 -0
  36. package/dist/realtime/useRealtimeChatMessages.js.map +1 -0
  37. package/dist/realtime/wire.d.ts +141 -0
  38. package/dist/realtime/wire.d.ts.map +1 -0
  39. package/dist/realtime/wire.js +71 -0
  40. package/dist/realtime/wire.js.map +1 -0
  41. package/dist/types.d.ts +12 -4
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/ui/ChatShell.d.ts +7 -1
  44. package/dist/ui/ChatShell.d.ts.map +1 -1
  45. package/dist/ui/ChatShell.js +51 -3
  46. package/dist/ui/ChatShell.js.map +1 -1
  47. package/package.json +3 -2
@@ -0,0 +1,173 @@
1
+ // The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's
2
+ // inbox DO. It maintains the channel/invite registry + unread projection that
3
+ // drives the Chats view — DOTS ONLY, no counts (Contract C / "Unread + inbox").
4
+ //
5
+ // The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on
6
+ // `resume`, and on every live delta (a projection apply). This client just
7
+ // mirrors the latest snapshot into an observable store + a per-channel unread set.
8
+ // Same connection lifecycle as the channel client (subprotocol auth, reconnect,
9
+ // 4401 re-grant once, 4403 stop, auth-switch teardown).
10
+ import { createReconnectController } from '@ttt-productions/realtime-core';
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: [] };
14
+ export class InboxClient {
15
+ config;
16
+ timers;
17
+ controller;
18
+ socket = null;
19
+ state = INITIAL;
20
+ listeners = new Set();
21
+ reauthAttempted = false;
22
+ reconnectTimer = null;
23
+ closedByUs = false;
24
+ constructor(config) {
25
+ this.config = config;
26
+ this.timers = config.timers ?? defaultTimers;
27
+ this.controller = createReconnectController(config.reconnect ?? {});
28
+ }
29
+ getState() {
30
+ return this.state;
31
+ }
32
+ subscribe(fn) {
33
+ this.listeners.add(fn);
34
+ return () => this.listeners.delete(fn);
35
+ }
36
+ setState(patch) {
37
+ this.state = { ...this.state, ...patch };
38
+ for (const fn of this.listeners)
39
+ fn(this.state);
40
+ }
41
+ async connect() {
42
+ this.closedByUs = false;
43
+ this.controller.start();
44
+ this.setState({ status: 'connecting' });
45
+ await this.openSocket();
46
+ }
47
+ async openSocket() {
48
+ let grantToken;
49
+ try {
50
+ grantToken = await this.config.grantProvider();
51
+ }
52
+ catch {
53
+ return this.scheduleReconnect();
54
+ }
55
+ if (this.closedByUs)
56
+ return;
57
+ const url = `${this.config.endpoint.replace(/\/$/, '')}/inbox`;
58
+ this.socket = this.config.socketFactory({
59
+ url,
60
+ grantToken,
61
+ handlers: {
62
+ onOpen: () => this.onOpen(),
63
+ onMessage: (data) => this.onMessage(data),
64
+ onClose: (code, reason) => this.onClose(code, reason),
65
+ onError: () => undefined,
66
+ },
67
+ });
68
+ }
69
+ onOpen() {
70
+ this.controller.onOpen();
71
+ this.reauthAttempted = false;
72
+ this.setState({ status: 'open' });
73
+ // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.
74
+ this.sendFrame(CLIENT_FRAME.RESUME, {});
75
+ }
76
+ onClose(code, _reason) {
77
+ this.socket = null;
78
+ if (this.closedByUs) {
79
+ this.setState({ status: 'closed' });
80
+ return;
81
+ }
82
+ if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {
83
+ this.reauthAttempted = true;
84
+ this.setState({ status: 'reconnecting' });
85
+ void this.openSocket();
86
+ return;
87
+ }
88
+ if (code === CHAT_CLOSE_CODES.REVOKED) {
89
+ this.closedByUs = true;
90
+ this.controller.close();
91
+ this.setState({ status: 'closed' });
92
+ return;
93
+ }
94
+ this.scheduleReconnect();
95
+ }
96
+ scheduleReconnect() {
97
+ const delay = this.controller.onClose();
98
+ if (delay == null) {
99
+ this.setState({ status: 'closed' });
100
+ return;
101
+ }
102
+ this.setState({ status: 'reconnecting' });
103
+ this.reconnectTimer = this.timers.setTimeout(() => {
104
+ this.reconnectTimer = null;
105
+ if (this.closedByUs)
106
+ return;
107
+ void this.openSocket();
108
+ }, delay);
109
+ }
110
+ onMessage(data) {
111
+ const frame = parseFrame(data);
112
+ if (!frame || frame.type !== 'snapshot' || !frame.payload)
113
+ return;
114
+ const payload = frame.payload;
115
+ if (!isInboxSnapshot(payload))
116
+ return; // ignore a stray channel snapshot
117
+ this.applySnapshot(payload);
118
+ }
119
+ applySnapshot(snap) {
120
+ const registry = snap.registry.filter((e) => e.state === 'active');
121
+ // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived
122
+ // from the same projection: the DO only keeps unread for ACTIVE entries, and a
123
+ // future snapshot field can carry per-channel unread; until then the dock dot
124
+ // is authoritative and per-row unread is surfaced via the projected set below.
125
+ this.setState({
126
+ registry,
127
+ hasUnread: Boolean(snap.hasUnread),
128
+ unreadChannelRefs: deriveUnreadRefs(snap),
129
+ });
130
+ }
131
+ sendFrame(type, payload) {
132
+ if (!this.socket || !this.socket.isOpen)
133
+ return false;
134
+ this.socket.send(buildFrame(type, payload));
135
+ return true;
136
+ }
137
+ /** True if a specific channel/invite currently carries an unread dot. */
138
+ channelHasUnread(channelRef) {
139
+ return this.state.unreadChannelRefs.includes(channelRef);
140
+ }
141
+ /** Permanently close (auth-user switch / unmount). Idempotent. */
142
+ close() {
143
+ this.closedByUs = true;
144
+ this.controller.close();
145
+ if (this.reconnectTimer != null) {
146
+ this.timers.clearTimeout(this.reconnectTimer);
147
+ this.reconnectTimer = null;
148
+ }
149
+ if (this.socket) {
150
+ this.socket.close(1000, 'client teardown');
151
+ this.socket = null;
152
+ }
153
+ this.setState({ status: 'closed' });
154
+ }
155
+ }
156
+ /**
157
+ * Derive the per-channel unread set from a snapshot. The current inbox DO snapshot
158
+ * exposes a single `hasUnread` boolean (dock dot) + the active registry; it does
159
+ * NOT yet enumerate which entries are unread. We surface a per-entry `unread`
160
+ * field when the DO provides it (forward-compatible), else fall back to empty
161
+ * per-row dots (the dock dot still lights). See the ASSUMPTIONS note in the docs.
162
+ */
163
+ function deriveUnreadRefs(snap) {
164
+ const refs = [];
165
+ for (const e of snap.registry) {
166
+ if (e.state !== 'active')
167
+ continue;
168
+ if (e.unread === true)
169
+ refs.push(e.channelRef);
170
+ }
171
+ return refs;
172
+ }
173
+ //# sourceMappingURL=inbox-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
2
+ export type { ChannelRefTuple, ChatCloseCode, WireMessageRow, WireChannelSnapshot, WireInboxSnapshot, WireRegistryEntry, ServerFrame, } from './wire.js';
3
+ export type { RealtimeSocket, SocketFactory, SocketHandlers } from './socket.js';
4
+ export { browserSocketFactory } from './socket.js';
5
+ export type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';
6
+ export { defaultTimers, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from './shared.js';
7
+ export { wireRowToMessage, optimisticMessage, seqToMessageId } from './map.js';
8
+ export { ChannelClient } from './channel-client.js';
9
+ export type { ChannelClientState, ChannelClientConfig } from './channel-client.js';
10
+ export { InboxClient } from './inbox-client.js';
11
+ export type { InboxClientState, InboxClientConfig } from './inbox-client.js';
12
+ export { createRealtimeChatClient, createInboxClient, } from './transport.js';
13
+ export type { RealtimeChatClient, RealtimeTransportConfig } from './transport.js';
14
+ export { useRealtimeChatMessages } from './useRealtimeChatMessages.js';
15
+ export type { UseRealtimeChatMessagesResult } from './useRealtimeChatMessages.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,eAAe,EACf,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,GACZ,MAAM,WAAW,CAAC;AAEnB,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEhG,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EACL,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAElF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,YAAY,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,14 @@
1
+ // The realtime (Cloudflare Durable Object) chat transport — the client half of
2
+ // the chat-worker wire protocol (Contract A connection + Contract C wire). The
3
+ // FIRESTORE transport stays the unchanged default; this is opt-in via
4
+ // `ChatCoreConfig.transport === 'realtime'` + a `realtime.client` from
5
+ // `createRealtimeChatClient(...)`.
6
+ export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
7
+ export { browserSocketFactory } from './socket.js';
8
+ export { defaultTimers, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX } from './shared.js';
9
+ export { wireRowToMessage, optimisticMessage, seqToMessageId } from './map.js';
10
+ export { ChannelClient } from './channel-client.js';
11
+ export { InboxClient } from './inbox-client.js';
12
+ export { createRealtimeChatClient, createInboxClient, } from './transport.js';
13
+ export { useRealtimeChatMessages } from './useRealtimeChatMessages.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,sEAAsE;AACtE,uEAAuE;AACvE,mCAAmC;AAEnC,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAC;AAYnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEhG,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ChatMessageV1 } from '@ttt-productions/chat-core';
2
+ import type { WireMessageRow } from './wire.js';
3
+ /** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */
4
+ export declare function seqToMessageId(seq: number): string;
5
+ /**
6
+ * Convert a DO message row to the UI message. `threadId` is the caller's logical
7
+ * thread id (the UI's thread key); the DO row carries no threadId of its own.
8
+ * - `messageId` = the server seq (string) — stable + dedup key.
9
+ * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
10
+ * - The DO row carries only the attachment LIFECYCLE (`attachmentState` +
11
+ * `attachmentMeta.mediaAssetId`), not the full `ChatAttachment` (id/name/size/
12
+ * storagePath). The full attachment-saga projection is the next chain's work
13
+ * (P5/P6); until then the placeholder lifecycle rides in `meta.attachment*` so
14
+ * the renderer can show the pending/ready/failed bubble without a stored URL —
15
+ * the render-time URL comes from `mediaAssetId` via the attachment-URL resolver
16
+ * context (identity/media invariant: no URLs are ever stored).
17
+ */
18
+ export declare function wireRowToMessage(row: WireMessageRow, threadId: string): ChatMessageV1;
19
+ /** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */
20
+ export declare function optimisticMessage(args: {
21
+ clientMessageId: string;
22
+ threadId: string;
23
+ senderId: string;
24
+ text: string;
25
+ createdAt: number;
26
+ replyTo?: {
27
+ messageId: string;
28
+ messagePreview?: string;
29
+ } | null;
30
+ }): ChatMessageV1;
31
+ //# sourceMappingURL=map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAqBrF;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjE,GAAG,aAAa,CAYhB"}
@@ -0,0 +1,70 @@
1
+ // Map the DO wire row (`WireMessageRow`) into the UI `ChatMessageV1` shape so the
2
+ // UI stays transport-agnostic — the MessageList renders the same shape whether
3
+ // the bytes came from Firestore or the DO socket.
4
+ //
5
+ // Identity invariant: the DO stores uid references only (`senderUid`), never a
6
+ // display name — names resolve at render time through the ChatNameResolver
7
+ // context, identical to the Firestore path.
8
+ /** The DO `seq` is the stable per-channel message id (monotonic, epoch-spanning). */
9
+ export function seqToMessageId(seq) {
10
+ return String(seq);
11
+ }
12
+ function parseJson(raw) {
13
+ if (raw == null)
14
+ return null;
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /**
23
+ * Convert a DO message row to the UI message. `threadId` is the caller's logical
24
+ * thread id (the UI's thread key); the DO row carries no threadId of its own.
25
+ * - `messageId` = the server seq (string) — stable + dedup key.
26
+ * - `replyTo` JSON `{ messageSeq, preview }` → UI `{ messageId, senderId:'', messagePreview }`.
27
+ * - The DO row carries only the attachment LIFECYCLE (`attachmentState` +
28
+ * `attachmentMeta.mediaAssetId`), not the full `ChatAttachment` (id/name/size/
29
+ * storagePath). The full attachment-saga projection is the next chain's work
30
+ * (P5/P6); until then the placeholder lifecycle rides in `meta.attachment*` so
31
+ * the renderer can show the pending/ready/failed bubble without a stored URL —
32
+ * the render-time URL comes from `mediaAssetId` via the attachment-URL resolver
33
+ * context (identity/media invariant: no URLs are ever stored).
34
+ */
35
+ export function wireRowToMessage(row, threadId) {
36
+ const reply = parseJson(row.replyTo);
37
+ const meta = parseJson(row.attachmentMeta);
38
+ return {
39
+ messageId: seqToMessageId(row.seq),
40
+ threadId,
41
+ createdAt: row.createdAt,
42
+ senderId: row.senderUid,
43
+ text: row.text,
44
+ replyTo: reply
45
+ ? { messageId: seqToMessageId(reply.messageSeq), senderId: '', messagePreview: reply.preview }
46
+ : undefined,
47
+ isSystemMessage: row.senderUid === 'system' || undefined,
48
+ meta: {
49
+ seq: row.seq,
50
+ epoch: row.epoch,
51
+ ...(row.attachmentState ? { attachmentState: row.attachmentState } : {}),
52
+ ...(meta ? { attachmentMeta: meta } : {}),
53
+ },
54
+ };
55
+ }
56
+ /** The optimistic local echo a `send` produces before the server `ack` reconciles the seq. */
57
+ export function optimisticMessage(args) {
58
+ return {
59
+ messageId: `optimistic:${args.clientMessageId}`,
60
+ threadId: args.threadId,
61
+ createdAt: args.createdAt,
62
+ senderId: args.senderId,
63
+ text: args.text,
64
+ replyTo: args.replyTo
65
+ ? { messageId: args.replyTo.messageId, senderId: '', messagePreview: args.replyTo.messagePreview ?? '' }
66
+ : undefined,
67
+ meta: { clientMessageId: args.clientMessageId, optimistic: true },
68
+ };
69
+ }
70
+ //# sourceMappingURL=map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/realtime/map.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,+EAA+E;AAC/E,kDAAkD;AAClD,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,4CAA4C;AAK5C,qFAAqF;AACrF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAcD,SAAS,SAAS,CAAI,GAAkB;IACtC,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAM,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAmB,EAAE,QAAgB;IACpE,MAAM,KAAK,GAAG,SAAS,CAAc,GAAG,CAAC,OAAO,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,SAAS,CAAqB,GAAG,CAAC,cAAc,CAAC,CAAC;IAE/D,OAAO;QACL,SAAS,EAAE,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAClC,QAAQ;QACR,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,KAAK;YACZ,CAAC,CAAC,EAAE,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,EAAE;YAC9F,CAAC,CAAC,SAAS;QACb,eAAe,EAAE,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,SAAS;QACxD,IAAI,EAAE;YACJ,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1C;KACF,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,iBAAiB,CAAC,IAOjC;IACC,OAAO;QACL,SAAS,EAAE,cAAc,IAAI,CAAC,eAAe,EAAE;QAC/C,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACnB,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,EAAE,EAAE;YACxG,CAAC,CAAC,SAAS;QACb,IAAI,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,EAAE,IAAI,EAAE;KAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Mints a short-lived chat grant token for the socket's scope. The app wires this
3
+ * to its `mintChatGrant` callable (channel grant for a ChannelClient, inbox grant
4
+ * for an InboxClient). Called on every connect AND once on a 4401 re-grant. The
5
+ * app owns the Firestore permission check, the scope shape, caching to a React
6
+ * Query staleTime < grant exp, and returning a FRESH token when re-invoked.
7
+ */
8
+ export type GrantProvider = () => Promise<string>;
9
+ /** Injectable timers so the transport runs deterministically under fake timers in tests. */
10
+ export interface TransportTimers {
11
+ setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
12
+ clearTimeout: (h: ReturnType<typeof setTimeout>) => void;
13
+ now: () => number;
14
+ }
15
+ export declare const defaultTimers: TransportTimers;
16
+ /** Connection status surfaced to the UI. */
17
+ export type RealtimeStatus = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed';
18
+ /** Client heartbeat cadence (worker `CLIENT_HEARTBEAT_MS`). */
19
+ export declare const HEARTBEAT_MS = 20000;
20
+ /** Typing coalescing minimum interval (worker `TYPING_COALESCE_MS`). */
21
+ export declare const TYPING_COALESCE_MS = 2000;
22
+ /** History page cap (worker `HISTORY_PAGE_MAX`). */
23
+ export declare const HISTORY_PAGE_MAX = 50;
24
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;AAElD,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC1E,YAAY,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,eAI3B,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;AAExF,+DAA+D;AAC/D,eAAO,MAAM,YAAY,QAAS,CAAC;AACnC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,oDAAoD;AACpD,eAAO,MAAM,gBAAgB,KAAK,CAAC"}
@@ -0,0 +1,19 @@
1
+ // Shared injected contracts + tunable constants for the realtime transport.
2
+ //
3
+ // The constants mirror the worker's V1 limits (`ttt-master-app/chat-worker/src/
4
+ // limits.ts`) for the values the CLIENT must agree on. They are duplicated (not
5
+ // imported) on purpose — this package is generic and never imports the worker;
6
+ // the worker is the authority and rejects anything out of bounds, so a drift is
7
+ // caught server-side, but the client uses these to coalesce/paginate politely.
8
+ export const defaultTimers = {
9
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
10
+ clearTimeout: (h) => clearTimeout(h),
11
+ now: () => Date.now(),
12
+ };
13
+ /** Client heartbeat cadence (worker `CLIENT_HEARTBEAT_MS`). */
14
+ export const HEARTBEAT_MS = 20_000;
15
+ /** Typing coalescing minimum interval (worker `TYPING_COALESCE_MS`). */
16
+ export const TYPING_COALESCE_MS = 2_000;
17
+ /** History page cap (worker `HISTORY_PAGE_MAX`). */
18
+ export const HISTORY_PAGE_MAX = 50;
19
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/realtime/shared.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAkB/E,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC5C,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IACpC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CACtB,CAAC;AAKF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AACnC,wEAAwE;AACxE,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AACxC,oDAAoD;AACpD,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,33 @@
1
+ /** Lifecycle events a socket surfaces to its owning client. */
2
+ export interface SocketHandlers {
3
+ onOpen: () => void;
4
+ onMessage: (data: string) => void;
5
+ /** code/reason from the close frame (4401/4403/4408/4413/4429/1013 or transport codes). */
6
+ onClose: (code: number, reason: string) => void;
7
+ onError: () => void;
8
+ }
9
+ /** The transport-neutral socket the clients use. A mock implements the same shape in tests. */
10
+ export interface RealtimeSocket {
11
+ /** Queue/send a text frame. No-op if not open. */
12
+ send(data: string): void;
13
+ /** Close the socket with an optional app code + reason. Idempotent. */
14
+ close(code?: number, reason?: string): void;
15
+ /** True once the underlying transport is OPEN (ready to send). */
16
+ readonly isOpen: boolean;
17
+ }
18
+ /**
19
+ * A factory that opens a socket to `url`, offering the chat subprotocol + the
20
+ * grant token, and wires the handlers. Injected into the transport so tests
21
+ * substitute a mock. The default factory uses the global `WebSocket`.
22
+ */
23
+ export type SocketFactory = (args: {
24
+ url: string;
25
+ grantToken: string;
26
+ handlers: SocketHandlers;
27
+ }) => RealtimeSocket;
28
+ /**
29
+ * The default browser socket factory. Offers `['ttt.chat.v1', grantToken]` as the
30
+ * subprotocol list so the grant rides in `Sec-WebSocket-Protocol`, never the URL.
31
+ */
32
+ export declare const browserSocketFactory: SocketFactory;
33
+ //# sourceMappingURL=socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAYA,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2FAA2F;IAC3F,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uEAAuE;IACvE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;CAC1B,KAAK,cAAc,CAAC;AAiBrB;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAuBlC,CAAC"}
@@ -0,0 +1,43 @@
1
+ // The minimal socket abstraction the realtime clients drive, plus the default
2
+ // browser-WebSocket factory. Injectable so unit tests pass a MOCK socket and no
3
+ // real network is touched.
4
+ //
5
+ // Contract A transport: the grant token is offered as the SECOND WebSocket
6
+ // subprotocol — `Sec-WebSocket-Protocol: ttt.chat.v1, <grant-token>` — never in
7
+ // the URL (tokens never appear in URLs; Contract A). The browser sets that header
8
+ // from `new WebSocket(url, ['ttt.chat.v1', grantToken])`. The worker validates
9
+ // cookie + grant + Origin BEFORE accept and echoes back only `ttt.chat.v1`.
10
+ import { CHAT_SUBPROTOCOL } from './wire.js';
11
+ const WS_OPEN = 1;
12
+ /**
13
+ * The default browser socket factory. Offers `['ttt.chat.v1', grantToken]` as the
14
+ * subprotocol list so the grant rides in `Sec-WebSocket-Protocol`, never the URL.
15
+ */
16
+ export const browserSocketFactory = ({ url, grantToken, handlers }) => {
17
+ const Ctor = globalThis.WebSocket;
18
+ if (!Ctor)
19
+ throw new Error('[chat-realtime] no global WebSocket available; inject a SocketFactory');
20
+ const ws = new Ctor(url, [CHAT_SUBPROTOCOL, grantToken]);
21
+ ws.onopen = () => handlers.onOpen();
22
+ ws.onmessage = (ev) => handlers.onMessage(typeof ev.data === 'string' ? ev.data : String(ev.data));
23
+ ws.onclose = (ev) => handlers.onClose(ev.code, ev.reason);
24
+ ws.onerror = () => handlers.onError();
25
+ return {
26
+ send(data) {
27
+ if (ws.readyState === WS_OPEN)
28
+ ws.send(data);
29
+ },
30
+ close(code, reason) {
31
+ try {
32
+ ws.close(code, reason);
33
+ }
34
+ catch {
35
+ /* already closing/closed */
36
+ }
37
+ },
38
+ get isOpen() {
39
+ return ws.readyState === WS_OPEN;
40
+ },
41
+ };
42
+ };
43
+ //# sourceMappingURL=socket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../../src/realtime/socket.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,2BAA2B;AAC3B,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,kFAAkF;AAClF,+EAA+E;AAC/E,4EAA4E;AAE5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AA6C7C,MAAM,OAAO,GAAG,CAAC,CAAC;AAElB;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE;IACnF,MAAM,IAAI,GAAI,UAA4C,CAAC,SAAS,CAAC;IACrE,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACpG,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC,CAAC;IACzD,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACpC,EAAE,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACnG,EAAE,CAAC,OAAO,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC1D,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IACtC,OAAO;QACL,IAAI,CAAC,IAAY;YACf,IAAI,EAAE,CAAC,UAAU,KAAK,OAAO;gBAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,IAAa,EAAE,MAAe;YAClC,IAAI,CAAC;gBACH,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,4BAA4B;YAC9B,CAAC;QACH,CAAC;QACD,IAAI,MAAM;YACR,OAAO,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { ChannelClient, type ChannelClientConfig, type ChannelClientState } from './channel-client.js';
2
+ import { InboxClient, type InboxClientConfig, type InboxClientState } from './inbox-client.js';
3
+ import { type SocketFactory } from './socket.js';
4
+ import type { GrantProvider, TransportTimers } from './shared.js';
5
+ import type { ChannelRefTuple } from './wire.js';
6
+ /**
7
+ * Everything the channel transport needs. The app supplies `grantProvider`
8
+ * (its `mintChatGrant` callable, channel scope) + `endpoint`; `socketFactory`
9
+ * defaults to the real browser WebSocket and is overridden in tests.
10
+ */
11
+ export interface RealtimeTransportConfig {
12
+ endpoint: string;
13
+ channelRef: ChannelRefTuple;
14
+ threadId: string;
15
+ currentUserId: string;
16
+ grantProvider: GrantProvider;
17
+ socketFactory?: SocketFactory;
18
+ timers?: TransportTimers;
19
+ reconnect?: ChannelClientConfig['reconnect'];
20
+ }
21
+ /**
22
+ * The opaque realtime client handle stored on `ChatRealtimeTransportConfig.client`.
23
+ * The UI hook (`useRealtimeChatMessages`) drives it; the UI never reaches into the
24
+ * socket directly.
25
+ */
26
+ export interface RealtimeChatClient {
27
+ readonly channel: ChannelClient;
28
+ readonly channelRef: ChannelRefTuple;
29
+ readonly currentUserId: string;
30
+ connect(): Promise<void>;
31
+ /** Tear down every socket (auth-user switch / unmount). Idempotent. */
32
+ close(): void;
33
+ getState(): ChannelClientState;
34
+ subscribe(fn: (s: ChannelClientState) => void): () => void;
35
+ }
36
+ /** Create the per-thread realtime client (the value placed on `config.realtime.client`). */
37
+ export declare function createRealtimeChatClient(config: RealtimeTransportConfig): RealtimeChatClient;
38
+ /** Create the per-USER inbox client (mount once at the dock; not per-thread). */
39
+ export declare function createInboxClient(config: {
40
+ endpoint: string;
41
+ currentUserId: string;
42
+ grantProvider: GrantProvider;
43
+ socketFactory?: SocketFactory;
44
+ timers?: TransportTimers;
45
+ reconnect?: InboxClientConfig['reconnect'];
46
+ }): InboxClient;
47
+ export type { ChannelClientState, InboxClientState };
48
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,aAAa,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,IAAI,kBAAkB,CAAC;IAC/B,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5D;AAED,4FAA4F;AAC5F,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,kBAAkB,CAoB5F;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;CAC5C,GAAG,WAAW,CASd;AAED,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,47 @@
1
+ // The realtime transport HANDLE the chat-react config carries on the `realtime`
2
+ // transport (`ChatRealtimeTransportConfig.client`). It owns:
3
+ // - a ChannelClient for the active thread (send/read/typing/presence/history)
4
+ // - the grant-mint wiring (channel scope) via the injected grantProvider
5
+ // - auth-user-switch teardown (close every socket before connecting as a new uid)
6
+ //
7
+ // The INBOX socket is created SEPARATELY (one per user, app-wide) via
8
+ // `createInboxClient` — it is not tied to a single thread, so the app owns its
9
+ // lifetime (mount it once at the dock, not per ChatShell). This keeps the channel
10
+ // transport (per-thread) and the inbox transport (per-user) cleanly split, exactly
11
+ // as the worker splits Channel DOs from the Inbox DO.
12
+ import { ChannelClient } from './channel-client.js';
13
+ import { InboxClient } from './inbox-client.js';
14
+ import { browserSocketFactory } from './socket.js';
15
+ /** Create the per-thread realtime client (the value placed on `config.realtime.client`). */
16
+ export function createRealtimeChatClient(config) {
17
+ const channel = new ChannelClient({
18
+ endpoint: config.endpoint,
19
+ threadId: config.threadId,
20
+ currentUserId: config.currentUserId,
21
+ grantProvider: config.grantProvider,
22
+ socketFactory: config.socketFactory ?? browserSocketFactory,
23
+ timers: config.timers,
24
+ reconnect: config.reconnect,
25
+ });
26
+ return {
27
+ channel,
28
+ channelRef: config.channelRef,
29
+ currentUserId: config.currentUserId,
30
+ connect: () => channel.connect(),
31
+ close: () => channel.close(),
32
+ getState: () => channel.getState(),
33
+ subscribe: (fn) => channel.subscribe(fn),
34
+ };
35
+ }
36
+ /** Create the per-USER inbox client (mount once at the dock; not per-thread). */
37
+ export function createInboxClient(config) {
38
+ return new InboxClient({
39
+ endpoint: config.endpoint,
40
+ currentUserId: config.currentUserId,
41
+ grantProvider: config.grantProvider,
42
+ socketFactory: config.socketFactory ?? browserSocketFactory,
43
+ timers: config.timers,
44
+ reconnect: config.reconnect,
45
+ });
46
+ }
47
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/realtime/transport.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAC7D,gFAAgF;AAChF,2EAA2E;AAC3E,oFAAoF;AACpF,EAAE;AACF,sEAAsE;AACtE,+EAA+E;AAC/E,kFAAkF;AAClF,mFAAmF;AACnF,sDAAsD;AAEtD,OAAO,EAAE,aAAa,EAAqD,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAiD,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,oBAAoB,EAAsB,MAAM,aAAa,CAAC;AAoCvE,4FAA4F;AAC5F,MAAM,UAAU,wBAAwB,CAAC,MAA+B;IACtE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QAChC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QAC5B,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;KACzC,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,MAOjC;IACC,OAAO,IAAI,WAAW,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,oBAAoB;QAC3D,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { ChatMessageV1 } from "@ttt-productions/chat-core";
2
+ import type { RealtimeChatClient } from "./transport.js";
3
+ import type { ChannelClientState } from "./channel-client.js";
4
+ export type UseRealtimeChatMessagesResult = {
5
+ allowed: boolean;
6
+ isInitialLoading: boolean;
7
+ messages: ChatMessageV1[];
8
+ fetchOlder: () => Promise<void>;
9
+ hasOlder: boolean;
10
+ isFetchingOlder: boolean;
11
+ /** Realtime extras (not present on the firestore result) — optional for the UI. */
12
+ status: ChannelClientState["status"];
13
+ typing: string[];
14
+ presence: string[];
15
+ send: (text: string, replyTo?: {
16
+ messageSeq: number;
17
+ preview: string;
18
+ } | null) => void;
19
+ readAck: (readSeq: number, focused: boolean) => void;
20
+ signalTyping: () => void;
21
+ };
22
+ export declare function useRealtimeChatMessages(client: RealtimeChatClient): UseRealtimeChatMessagesResult;
23
+ //# sourceMappingURL=useRealtimeChatMessages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRealtimeChatMessages.d.ts","sourceRoot":"","sources":["../../src/realtime/useRealtimeChatMessages.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,mFAAmF;IACnF,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACvF,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,6BAA6B,CAgEjG"}