@ttt-productions/chat-react 0.13.6 → 0.13.8
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/context/ChatNameResolverContext.d.ts +24 -16
- package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
- package/dist/context/ChatNameResolverContext.js +15 -19
- package/dist/context/ChatNameResolverContext.js.map +1 -1
- package/dist/hooks/useChatMessages.d.ts +9 -0
- package/dist/hooks/useChatMessages.d.ts.map +1 -1
- package/dist/hooks/useChatMessages.js +10 -10
- package/dist/hooks/useChatMessages.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/realtime/channel-client.d.ts +32 -0
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +114 -15
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts +50 -6
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +133 -18
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/index.d.ts +3 -3
- 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/initial-load-budget.d.ts +31 -0
- package/dist/realtime/initial-load-budget.d.ts.map +1 -0
- package/dist/realtime/initial-load-budget.js +69 -0
- package/dist/realtime/initial-load-budget.js.map +1 -0
- package/dist/realtime/map.d.ts.map +1 -1
- package/dist/realtime/map.js +3 -2
- package/dist/realtime/map.js.map +1 -1
- package/dist/realtime/shared.d.ts +13 -0
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +18 -0
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/transport.d.ts +6 -3
- package/dist/realtime/transport.d.ts.map +1 -1
- package/dist/realtime/transport.js +1 -0
- package/dist/realtime/transport.js.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.d.ts +4 -0
- package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.js +7 -8
- package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
- package/dist/realtime/wire.d.ts +11 -6
- package/dist/realtime/wire.d.ts.map +1 -1
- package/dist/realtime/wire.js +2 -2
- package/dist/realtime/wire.js.map +1 -1
- package/dist/types.d.ts +16 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui/ChatShell.d.ts +27 -1
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +27 -7
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/Composer.d.ts.map +1 -1
- package/dist/ui/Composer.js +4 -2
- package/dist/ui/Composer.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts +1 -1
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +10 -6
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/dist/ui/MessageList.d.ts +7 -1
- package/dist/ui/MessageList.d.ts.map +1 -1
- package/dist/ui/MessageList.js +6 -5
- package/dist/ui/MessageList.js.map +1 -1
- package/dist/ui/SenderName.d.ts +9 -0
- package/dist/ui/SenderName.d.ts.map +1 -0
- package/dist/ui/SenderName.js +16 -0
- package/dist/ui/SenderName.js.map +1 -0
- package/package.json +12 -12
- package/dist/hooks/useChatThreadAccess.d.ts +0 -8
- package/dist/hooks/useChatThreadAccess.d.ts.map +0 -1
- package/dist/hooks/useChatThreadAccess.js +0 -19
- package/dist/hooks/useChatThreadAccess.js.map +0 -1
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"inbox-client.d.ts","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
// Same connection lifecycle as the channel client (subprotocol auth, reconnect,
|
|
9
9
|
// 4401 re-grant once, 4403 stop, auth-switch teardown).
|
|
10
10
|
import { createReconnectController } from '@ttt-productions/realtime-core';
|
|
11
|
+
import { ChatMarkReadResultPayloadSchema, SERVER_KINDS, } from '@ttt-productions/chat-schemas';
|
|
11
12
|
import { CLIENT_FRAME, CHAT_CLOSE_CODES, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
12
|
-
import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, TERMINAL_ERROR_CODE, } from './shared.js';
|
|
13
|
+
import { defaultTimers, isChatAccessDeniedError, isTerminalErrorCode, newCorrelationId, TERMINAL_ERROR_CODE, } from './shared.js';
|
|
14
|
+
import { InitialLoadBudget } from './initial-load-budget.js';
|
|
13
15
|
import { createDiagnosticsEmitter, safeDiagnosticLabel, CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG, } from './diagnostics.js';
|
|
14
|
-
const INITIAL = {
|
|
16
|
+
const INITIAL = {
|
|
17
|
+
status: 'idle',
|
|
18
|
+
registry: [],
|
|
19
|
+
hasUnread: false,
|
|
20
|
+
unreadChannelRefs: [],
|
|
21
|
+
lastErrorCode: null,
|
|
22
|
+
hasLoadedInitialData: false,
|
|
23
|
+
initialLoadFailed: false,
|
|
24
|
+
};
|
|
15
25
|
export class InboxClient {
|
|
16
26
|
config;
|
|
17
27
|
timers;
|
|
@@ -22,6 +32,14 @@ export class InboxClient {
|
|
|
22
32
|
reauthAttempted = false;
|
|
23
33
|
reconnectTimer = null;
|
|
24
34
|
closedByUs = false;
|
|
35
|
+
/**
|
|
36
|
+
* The current lifecycle's number, advanced by every `connect()` and `close()`; a grant,
|
|
37
|
+
* timer, or socket callback of an older lifecycle is ignored (FRONTEND-108).
|
|
38
|
+
*/
|
|
39
|
+
lifecycle = 0;
|
|
40
|
+
initialLoad;
|
|
41
|
+
/** Mark-reads sent on the current socket, awaiting their correlated result, by requestId. */
|
|
42
|
+
pendingMarkReads = new Map();
|
|
25
43
|
/** Structured diagnostics emitter, or null when off (the default, zero-overhead). */
|
|
26
44
|
diag;
|
|
27
45
|
/** Monotonic socket-open attempt counter (diagnostics correlation only). */
|
|
@@ -35,6 +53,7 @@ export class InboxClient {
|
|
|
35
53
|
this.timers = config.timers ?? defaultTimers;
|
|
36
54
|
this.controller = createReconnectController(config.reconnect ?? {});
|
|
37
55
|
this.diag = createDiagnosticsEmitter(config.diagnostics);
|
|
56
|
+
this.initialLoad = new InitialLoadBudget(this.timers, () => this.reportInitialLoadFailed());
|
|
38
57
|
}
|
|
39
58
|
getState() {
|
|
40
59
|
return this.state;
|
|
@@ -49,6 +68,34 @@ export class InboxClient {
|
|
|
49
68
|
fn(this.state);
|
|
50
69
|
}
|
|
51
70
|
async connect() {
|
|
71
|
+
return this.startLifecycle(true);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Retry now (the first-open failure's Retry) — the same rule as `ChannelClient.retryNow`:
|
|
75
|
+
* a reconnect waiting out its backoff opens at once; a stopped lifecycle starts again; an
|
|
76
|
+
* attempt in flight is left to finish; the first-open failure clears either way.
|
|
77
|
+
*/
|
|
78
|
+
retryNow() {
|
|
79
|
+
if (!this.state.hasLoadedInitialData)
|
|
80
|
+
this.initialLoad.manualRetry();
|
|
81
|
+
if (this.state.status === 'closed') {
|
|
82
|
+
void this.startLifecycle(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (this.reconnectTimer == null) {
|
|
86
|
+
if (this.state.initialLoadFailed)
|
|
87
|
+
this.setState({ initialLoadFailed: false });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.timers.clearTimeout(this.reconnectTimer);
|
|
91
|
+
this.reconnectTimer = null;
|
|
92
|
+
this.controller.reset();
|
|
93
|
+
this.controller.start();
|
|
94
|
+
this.reconnectCause = 'manual-retry';
|
|
95
|
+
this.setState({ status: 'reconnecting', initialLoadFailed: false });
|
|
96
|
+
void this.openSocket();
|
|
97
|
+
}
|
|
98
|
+
async startLifecycle(freshBudget) {
|
|
52
99
|
// Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle
|
|
53
100
|
// from a fresh (idle) or fully torn-down (closed) state. A second connect()
|
|
54
101
|
// while one is already connecting / open / reconnecting is a no-op, so a
|
|
@@ -71,12 +118,21 @@ export class InboxClient {
|
|
|
71
118
|
if (isTerminalErrorCode(this.state.lastErrorCode))
|
|
72
119
|
this.setState({ lastErrorCode: null });
|
|
73
120
|
this.closedByUs = false;
|
|
121
|
+
this.lifecycle += 1;
|
|
74
122
|
this.reconnectCause = 'initial';
|
|
123
|
+
if (!this.state.hasLoadedInitialData)
|
|
124
|
+
this.initialLoad.begin(freshBudget);
|
|
75
125
|
this.controller.start();
|
|
76
|
-
this.setState({ status: 'connecting' });
|
|
126
|
+
this.setState({ status: 'connecting', initialLoadFailed: false });
|
|
77
127
|
await this.openSocket();
|
|
78
128
|
}
|
|
129
|
+
reportInitialLoadFailed() {
|
|
130
|
+
if (this.state.hasLoadedInitialData || this.state.initialLoadFailed)
|
|
131
|
+
return;
|
|
132
|
+
this.setState({ initialLoadFailed: true });
|
|
133
|
+
}
|
|
79
134
|
async openSocket() {
|
|
135
|
+
const lifecycle = this.lifecycle;
|
|
80
136
|
this.connectAttempt += 1;
|
|
81
137
|
this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {
|
|
82
138
|
attempt: this.connectAttempt,
|
|
@@ -89,6 +145,8 @@ export class InboxClient {
|
|
|
89
145
|
grantToken = await this.config.grantProvider();
|
|
90
146
|
}
|
|
91
147
|
catch (err) {
|
|
148
|
+
if (lifecycle !== this.lifecycle)
|
|
149
|
+
return;
|
|
92
150
|
// A TERMINAL access denial (the app translated an authoritative policy "no"
|
|
93
151
|
// — e.g. Firebase `permission-denied` for a banned/suspended account — into the
|
|
94
152
|
// package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would
|
|
@@ -99,21 +157,38 @@ export class InboxClient {
|
|
|
99
157
|
if (terminal)
|
|
100
158
|
return this.denyAccessTerminally();
|
|
101
159
|
this.reconnectCause = 'grant-error';
|
|
160
|
+
if (!this.state.hasLoadedInitialData)
|
|
161
|
+
this.initialLoad.noteFailedAttempt();
|
|
102
162
|
return this.scheduleReconnect();
|
|
103
163
|
}
|
|
104
|
-
|
|
164
|
+
// A grant minted for an earlier lifecycle must never open a socket in this one.
|
|
165
|
+
if (lifecycle !== this.lifecycle || this.closedByUs)
|
|
105
166
|
return;
|
|
106
167
|
const url = `${this.config.endpoint.replace(/\/$/, '')}/inbox`;
|
|
107
|
-
|
|
168
|
+
// Every callback is fenced to THIS socket: once it has been replaced or torn down, its
|
|
169
|
+
// late open, frames, and close are ignored.
|
|
170
|
+
let socket = null;
|
|
171
|
+
const isCurrent = () => socket !== null && this.socket === socket;
|
|
172
|
+
socket = this.config.socketFactory({
|
|
108
173
|
url,
|
|
109
174
|
grantToken,
|
|
110
175
|
handlers: {
|
|
111
|
-
onOpen: () =>
|
|
112
|
-
|
|
113
|
-
|
|
176
|
+
onOpen: () => {
|
|
177
|
+
if (isCurrent())
|
|
178
|
+
this.onOpen();
|
|
179
|
+
},
|
|
180
|
+
onMessage: (data) => {
|
|
181
|
+
if (isCurrent())
|
|
182
|
+
this.onMessage(data);
|
|
183
|
+
},
|
|
184
|
+
onClose: (code, reason) => {
|
|
185
|
+
if (isCurrent())
|
|
186
|
+
this.onClose(code, reason);
|
|
187
|
+
},
|
|
114
188
|
onError: () => undefined,
|
|
115
189
|
},
|
|
116
190
|
});
|
|
191
|
+
this.socket = socket;
|
|
117
192
|
}
|
|
118
193
|
onOpen() {
|
|
119
194
|
this.controller.onOpen();
|
|
@@ -134,6 +209,8 @@ export class InboxClient {
|
|
|
134
209
|
onClose(code, _reason) {
|
|
135
210
|
this.socket = null;
|
|
136
211
|
this.lastCloseCode = code;
|
|
212
|
+
// A mark-read sent on this socket can no longer be answered.
|
|
213
|
+
this.settlePendingMarkReads({ ok: false, code: 'connection-lost' });
|
|
137
214
|
if (this.closedByUs) {
|
|
138
215
|
this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });
|
|
139
216
|
this.setState({ status: 'closed' });
|
|
@@ -152,9 +229,12 @@ export class InboxClient {
|
|
|
152
229
|
this.controller.close();
|
|
153
230
|
this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });
|
|
154
231
|
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });
|
|
232
|
+
this.reportInitialLoadFailed();
|
|
155
233
|
return;
|
|
156
234
|
}
|
|
157
235
|
this.reconnectCause = 'transient-close';
|
|
236
|
+
if (!this.state.hasLoadedInitialData)
|
|
237
|
+
this.initialLoad.noteFailedAttempt();
|
|
158
238
|
this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });
|
|
159
239
|
this.scheduleReconnect();
|
|
160
240
|
}
|
|
@@ -169,6 +249,7 @@ export class InboxClient {
|
|
|
169
249
|
denyAccessTerminally() {
|
|
170
250
|
this.closedByUs = true;
|
|
171
251
|
this.controller.close();
|
|
252
|
+
this.initialLoad.dispose();
|
|
172
253
|
this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });
|
|
173
254
|
}
|
|
174
255
|
scheduleReconnect() {
|
|
@@ -180,12 +261,14 @@ export class InboxClient {
|
|
|
180
261
|
});
|
|
181
262
|
if (delay == null) {
|
|
182
263
|
this.setState({ status: 'closed' });
|
|
264
|
+
this.reportInitialLoadFailed();
|
|
183
265
|
return;
|
|
184
266
|
}
|
|
185
267
|
this.setState({ status: 'reconnecting' });
|
|
268
|
+
const lifecycle = this.lifecycle;
|
|
186
269
|
this.reconnectTimer = this.timers.setTimeout(() => {
|
|
187
270
|
this.reconnectTimer = null;
|
|
188
|
-
if (this.closedByUs)
|
|
271
|
+
if (this.closedByUs || lifecycle !== this.lifecycle)
|
|
189
272
|
return;
|
|
190
273
|
void this.openSocket();
|
|
191
274
|
}, delay);
|
|
@@ -196,6 +279,8 @@ export class InboxClient {
|
|
|
196
279
|
this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });
|
|
197
280
|
return;
|
|
198
281
|
}
|
|
282
|
+
if (frame.type === SERVER_KINDS.MARK_READ_RESULT)
|
|
283
|
+
return this.applyMarkReadResult(frame.payload);
|
|
199
284
|
// The inbox DO pushes a full `snapshot`; a future lightweight `unread` push
|
|
200
285
|
// carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both
|
|
201
286
|
// instead of silently dropping the `unread` type (C-M2).
|
|
@@ -242,7 +327,9 @@ export class InboxClient {
|
|
|
242
327
|
const unreadChannelRefs = deriveUnreadRefs(snap);
|
|
243
328
|
const hasUnread = Boolean(snap.hasUnread);
|
|
244
329
|
const before = this.state;
|
|
245
|
-
|
|
330
|
+
if (!before.hasLoadedInitialData)
|
|
331
|
+
this.initialLoad.loaded();
|
|
332
|
+
this.setState({ registry, hasUnread, unreadChannelRefs, hasLoadedInitialData: true, initialLoadFailed: false });
|
|
246
333
|
if (!this.diag)
|
|
247
334
|
return;
|
|
248
335
|
// SIZES ONLY — a channelRef names one specific conversation and never enters a
|
|
@@ -281,31 +368,59 @@ export class InboxClient {
|
|
|
281
368
|
}
|
|
282
369
|
/**
|
|
283
370
|
* Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read
|
|
284
|
-
* controls). Sends `mark-read`
|
|
285
|
-
* member's read cursor
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
371
|
+
* controls). Sends `mark-read` with a fresh `requestId`; the inbox runtime advances the
|
|
372
|
+
* member's read cursor, pushes a fresh authoritative snapshot (which is what removes
|
|
373
|
+
* the dot — there is NO optimistic local clear), and answers with a
|
|
374
|
+
* `mark-read-result` naming the same request. The promise settles on that answer —
|
|
375
|
+
* never on a timer — or with `not-sent` when no socket is open, or `connection-lost`
|
|
376
|
+
* when the socket closes first.
|
|
289
377
|
*/
|
|
290
378
|
markRead(channelRef) {
|
|
291
|
-
const
|
|
379
|
+
const requestId = newCorrelationId();
|
|
380
|
+
const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef, requestId });
|
|
292
381
|
// The ref itself is never logged — only that a mark-read was written and what the
|
|
293
382
|
// unread projection looked like at that moment (the DO's snapshot is what clears it).
|
|
294
383
|
this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });
|
|
295
|
-
|
|
384
|
+
if (!sent)
|
|
385
|
+
return Promise.resolve({ ok: false, code: 'not-sent' });
|
|
386
|
+
return new Promise((resolve) => this.pendingMarkReads.set(requestId, resolve));
|
|
387
|
+
}
|
|
388
|
+
/** Settle the mark-read the result names; a result naming no pending request is ignored. */
|
|
389
|
+
applyMarkReadResult(payload) {
|
|
390
|
+
const parsed = ChatMarkReadResultPayloadSchema.safeParse(payload);
|
|
391
|
+
if (!parsed.success) {
|
|
392
|
+
this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: SERVER_KINDS.MARK_READ_RESULT, reason: 'malformed-payload' });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const resolve = this.pendingMarkReads.get(parsed.data.requestId);
|
|
396
|
+
if (!resolve)
|
|
397
|
+
return;
|
|
398
|
+
this.pendingMarkReads.delete(parsed.data.requestId);
|
|
399
|
+
resolve(parsed.data.ok ? { ok: true } : { ok: false, code: parsed.data.code });
|
|
400
|
+
}
|
|
401
|
+
settlePendingMarkReads(outcome) {
|
|
402
|
+
const pending = [...this.pendingMarkReads.values()];
|
|
403
|
+
this.pendingMarkReads.clear();
|
|
404
|
+
for (const resolve of pending)
|
|
405
|
+
resolve(outcome);
|
|
296
406
|
}
|
|
297
407
|
/** Permanently close (auth-user switch / unmount). Idempotent. */
|
|
298
408
|
close() {
|
|
299
409
|
this.closedByUs = true;
|
|
410
|
+
this.lifecycle += 1;
|
|
300
411
|
this.controller.close();
|
|
412
|
+
this.initialLoad.dispose();
|
|
301
413
|
if (this.reconnectTimer != null) {
|
|
302
414
|
this.timers.clearTimeout(this.reconnectTimer);
|
|
303
415
|
this.reconnectTimer = null;
|
|
304
416
|
}
|
|
305
417
|
if (this.socket) {
|
|
306
|
-
|
|
418
|
+
// Detach first: the socket's own late close is not this lifecycle's event.
|
|
419
|
+
const socket = this.socket;
|
|
307
420
|
this.socket = null;
|
|
421
|
+
socket.close(1000, 'client teardown');
|
|
308
422
|
}
|
|
423
|
+
this.settlePendingMarkReads({ ok: false, code: 'connection-lost' });
|
|
309
424
|
this.setState({ status: 'closed' });
|
|
310
425
|
}
|
|
311
426
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n isTerminalErrorCode,\n TERMINAL_ERROR_CODE,\n} from './shared.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n}\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = { status: 'idle', registry: [], hasUnread: false, unreadChannelRefs: [], lastErrorCode: null };\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle\n // from a fresh (idle) or fully torn-down (closed) state. A second connect()\n // while one is already connecting / open / reconnecting is a no-op, so a\n // double-mounted dock cannot open a SECOND inbox socket against the per-uid\n // cap. The reconnect path uses openSocket() / scheduleReconnect() directly\n // (never connect()), so this never blocks a legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n // REVIVE the controller. `controller.close()` is PERMANENT: while it is in\n // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`\n // returns null — so without this reset a close() → connect() client opens a\n // socket and works normally until its FIRST transient close, then parks\n // terminally in `closed` with reconnect disabled and no surfaced error code.\n this.controller.reset();\n // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN\n // its lifecycle; an explicit connect() is a fresh attempt the authority may\n // legitimately re-deny, so the previous lifecycle's code must not persist.\n // The registry/unread projection is deliberately KEPT — the DO's next snapshot\n // is authoritative, and blanking the dock on every restart is pure UI churn.\n if (isTerminalErrorCode(this.state.lastErrorCode)) this.setState({ lastErrorCode: null });\n this.closedByUs = false;\n this.reconnectCause = 'initial';\n this.controller.start();\n this.setState({ status: 'connecting' });\n await this.openSocket();\n }\n\n private async openSocket(): Promise<void> {\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n return this.scheduleReconnect();\n }\n if (this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\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: () => undefined,\n },\n });\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });\n return;\n }\n this.reconnectCause = 'transient-close';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\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 private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n this.setState({ registry, hasUnread, unreadChannelRefs });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\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 /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` on the inbox socket; the inbox DO advances the\n * member's read cursor to tail on the channel DO and then pushes a fresh\n * authoritative snapshot, which is what removes the row — there is NO optimistic\n * local clear, so a failed mark-read honestly leaves the dot in place.\n * Returns false when the socket is not open (the caller may surface a retry).\n */\n markRead(channelRef: string): boolean {\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n return sent;\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.controller.close();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n this.socket.close(1000, 'client teardown');\n this.socket = null;\n }\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot\n * carries a per-entry `unread` boolean on every active registry entry alongside\n * the global `hasUnread` roll-up that drives the dock dot. An entry without the\n * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot\n * still lights off `hasUnread`.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"inbox-client.js","sourceRoot":"","sources":["../../src/realtime/inbox-client.ts"],"names":[],"mappings":"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","sourcesContent":["// The Inbox realtime client: a SEPARATE WebSocket (scope `inbox`) to the user's\n// inbox DO. It maintains the channel/invite registry + unread projection that\n// drives the inbox view — DOTS ONLY, no counts (Contract C / \"Unread + inbox\").\n//\n// The inbox DO pushes a full `snapshot` ({ registry, hasUnread }) on connect, on\n// `resume`, and on every live delta (a projection apply). This client just\n// mirrors the latest snapshot into an observable store + a per-channel unread set.\n// Same connection lifecycle as the channel client (subprotocol auth, reconnect,\n// 4401 re-grant once, 4403 stop, auth-switch teardown).\n\nimport { createReconnectController, type ReconnectController } from '@ttt-productions/realtime-core';\nimport {\n ChatMarkReadResultPayloadSchema,\n SERVER_KINDS,\n type ChatMarkReadFailureCode,\n} from '@ttt-productions/chat-schemas';\nimport {\n CLIENT_FRAME,\n CHAT_CLOSE_CODES,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n type WireInboxSnapshot,\n type WireRegistryEntry,\n} from './wire.js';\nimport type { RealtimeSocket, SocketFactory } from './socket.js';\nimport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nimport {\n defaultTimers,\n isChatAccessDeniedError,\n isTerminalErrorCode,\n newCorrelationId,\n TERMINAL_ERROR_CODE,\n} from './shared.js';\nimport { InitialLoadBudget } from './initial-load-budget.js';\nimport {\n createDiagnosticsEmitter,\n safeDiagnosticLabel,\n CHAT_CLIENT_DIAGNOSTIC_EVENTS as DIAG,\n type ChatClientDiagnosticsEmitter,\n type ChatClientDiagnosticsOption,\n} from './diagnostics.js';\n\nexport interface InboxClientState {\n status: RealtimeStatus;\n /** Active (non-tombstoned) channel/invite registry entries — inbox-view visibility. */\n registry: WireRegistryEntry[];\n /** True if ANY active channel/invite has unread (drives the dock dot). */\n hasUnread: boolean;\n /** The set of channelRefs that currently carry an unread dot (per-row dots). */\n unreadChannelRefs: string[];\n /**\n * The last TERMINAL error code (mirrors `ChannelClientState.lastErrorCode`):\n * `'access-denied'` when the inbox grant provider throws `ChatAccessDeniedError`\n * (a genuine, terminal policy \"no\" — e.g. a banned/suspended account), and\n * `'revoked'` on a 4403 REVOKED close. Null while healthy and across transient\n * reconnects. The app subscribes to inbox state directly (there is no inbox React\n * hook in this package), so this stable code is the surface — it invents no new UI.\n */\n lastErrorCode: string | null;\n /**\n * True once the first authoritative snapshot has been applied — the registry and dots\n * are then the inbox runtime's answer, so an empty registry means \"nothing here\"\n * rather than \"not loaded yet\". Once true it stays true.\n */\n hasLoadedInitialData: boolean;\n /**\n * True once the first open has failed for long enough to say so — the same rule as\n * `ChannelClientState.initialLoadFailed`. Cleared by the first snapshot or `retryNow()`.\n */\n initialLoadFailed: boolean;\n}\n\n/**\n * How one `markRead` ended: `ok` — the inbox runtime advanced the read cursor (its\n * fresh snapshot clears the dot); otherwise `code` says why not — the runtime's own\n * reason, `not-sent` (no open socket, nothing was sent), or `connection-lost` (the\n * socket closed before the answer arrived, so the outcome is unknown and the dot stays).\n */\nexport type ChatMarkReadOutcome =\n | { ok: true }\n | { ok: false; code: ChatMarkReadFailureCode | 'not-sent' | 'connection-lost' };\n\nexport interface InboxClientConfig {\n endpoint: string;\n /** The authenticated uid (the inbox owner; the DO serves no one else's data). */\n currentUserId: string;\n /** Mints/refreshes an INBOX-scope grant (scope `{ kind:'inbox', uid }`). */\n grantProvider: GrantProvider;\n socketFactory: SocketFactory;\n timers?: TransportTimers;\n reconnect?: { baseDelayMs?: number; maxDelayMs?: number; maxAttempts?: number | null; random?: () => number };\n /**\n * OPT-IN structured client diagnostics — the SAME option type and emitter the\n * channel client uses (one owner, `./diagnostics.ts`). Default OFF: absent or\n * `false` changes nothing and costs one nullish check. `true` emits one\n * single-line `console.debug` entry per decision under the stable\n * `chat_client_inbox_*` names; a function routes them to the app's logger.\n * Inbox payloads are SIZES ONLY — registry/unread counts and deltas, never a\n * `channelRef` (which names one specific conversation).\n */\n diagnostics?: ChatClientDiagnosticsOption;\n}\n\nconst INITIAL: InboxClientState = {\n status: 'idle',\n registry: [],\n hasUnread: false,\n unreadChannelRefs: [],\n lastErrorCode: null,\n hasLoadedInitialData: false,\n initialLoadFailed: false,\n};\n\nexport class InboxClient {\n private readonly timers: TransportTimers;\n private readonly controller: ReconnectController;\n private socket: RealtimeSocket | null = null;\n private state: InboxClientState = INITIAL;\n private readonly listeners = new Set<(s: InboxClientState) => void>();\n private reauthAttempted = false;\n private reconnectTimer: ReturnType<TransportTimers['setTimeout']> | null = null;\n private closedByUs = false;\n /**\n * The current lifecycle's number, advanced by every `connect()` and `close()`; a grant,\n * timer, or socket callback of an older lifecycle is ignored (FRONTEND-108).\n */\n private lifecycle = 0;\n private readonly initialLoad: InitialLoadBudget;\n /** Mark-reads sent on the current socket, awaiting their correlated result, by requestId. */\n private readonly pendingMarkReads = new Map<string, (outcome: ChatMarkReadOutcome) => void>();\n /** Structured diagnostics emitter, or null when off (the default, zero-overhead). */\n private readonly diag: ChatClientDiagnosticsEmitter;\n /** Monotonic socket-open attempt counter (diagnostics correlation only). */\n private connectAttempt = 0;\n /** Why the CURRENT connect attempt is happening (diagnostics only). */\n private reconnectCause: 'initial' | 'auth-expired' | 'transient-close' | 'grant-error' | 'manual-retry' = 'initial';\n /** The last close code observed, carried into the next attempt's cause line. */\n private lastCloseCode: number | null = null;\n\n constructor(private readonly config: InboxClientConfig) {\n this.timers = config.timers ?? defaultTimers;\n this.controller = createReconnectController(config.reconnect ?? {});\n this.diag = createDiagnosticsEmitter(config.diagnostics);\n this.initialLoad = new InitialLoadBudget(this.timers, () => this.reportInitialLoadFailed());\n }\n\n getState(): InboxClientState {\n return this.state;\n }\n\n subscribe(fn: (s: InboxClientState) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private setState(patch: Partial<InboxClientState>): void {\n this.state = { ...this.state, ...patch };\n for (const fn of this.listeners) fn(this.state);\n }\n\n async connect(): Promise<void> {\n return this.startLifecycle(true);\n }\n\n /**\n * Retry now (the first-open failure's Retry) — the same rule as `ChannelClient.retryNow`:\n * a reconnect waiting out its backoff opens at once; a stopped lifecycle starts again; an\n * attempt in flight is left to finish; the first-open failure clears either way.\n */\n retryNow(): void {\n if (!this.state.hasLoadedInitialData) this.initialLoad.manualRetry();\n if (this.state.status === 'closed') {\n void this.startLifecycle(false);\n return;\n }\n if (this.reconnectTimer == null) {\n if (this.state.initialLoadFailed) this.setState({ initialLoadFailed: false });\n return;\n }\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n this.controller.reset();\n this.controller.start();\n this.reconnectCause = 'manual-retry';\n this.setState({ status: 'reconnecting', initialLoadFailed: false });\n void this.openSocket();\n }\n\n private async startLifecycle(freshBudget: boolean): Promise<void> {\n // Idempotency guard (mirrors ChannelClient): connect() only starts a lifecycle\n // from a fresh (idle) or fully torn-down (closed) state. A second connect()\n // while one is already connecting / open / reconnecting is a no-op, so a\n // double-mounted dock cannot open a SECOND inbox socket against the per-uid\n // cap. The reconnect path uses openSocket() / scheduleReconnect() directly\n // (never connect()), so this never blocks a legitimate reconnect.\n if (this.state.status !== 'idle' && this.state.status !== 'closed') return;\n // REVIVE the controller. `controller.close()` is PERMANENT: while it is in\n // state `closed`, `start()` and `onOpen()` no-op and every later `onClose()`\n // returns null — so without this reset a close() → connect() client opens a\n // socket and works normally until its FIRST transient close, then parks\n // terminally in `closed` with reconnect disabled and no surfaced error code.\n this.controller.reset();\n // A terminal verdict (4403 REVOKED / ChatAccessDeniedError) is terminal WITHIN\n // its lifecycle; an explicit connect() is a fresh attempt the authority may\n // legitimately re-deny, so the previous lifecycle's code must not persist.\n // The registry/unread projection is deliberately KEPT — the DO's next snapshot\n // is authoritative, and blanking the dock on every restart is pure UI churn.\n if (isTerminalErrorCode(this.state.lastErrorCode)) this.setState({ lastErrorCode: null });\n this.closedByUs = false;\n this.lifecycle += 1;\n this.reconnectCause = 'initial';\n if (!this.state.hasLoadedInitialData) this.initialLoad.begin(freshBudget);\n this.controller.start();\n this.setState({ status: 'connecting', initialLoadFailed: false });\n await this.openSocket();\n }\n\n private reportInitialLoadFailed(): void {\n if (this.state.hasLoadedInitialData || this.state.initialLoadFailed) return;\n this.setState({ initialLoadFailed: true });\n }\n\n private async openSocket(): Promise<void> {\n const lifecycle = this.lifecycle;\n this.connectAttempt += 1;\n this.diag?.(DIAG.INBOX_CONNECT_ATTEMPT, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n closeCode: this.lastCloseCode,\n registryCount: this.state.registry.length,\n });\n let grantToken: string;\n try {\n grantToken = await this.config.grantProvider();\n } catch (err) {\n if (lifecycle !== this.lifecycle) return;\n // A TERMINAL access denial (the app translated an authoritative policy \"no\"\n // — e.g. Firebase `permission-denied` for a banned/suspended account — into the\n // package-owned ChatAccessDeniedError) must NOT reconnect: the next mint would\n // just re-deny, reconnect-looping forever and warning every cycle. Every other\n // mint failure (network/transient) stays retryable and backs off as before.\n const terminal = isChatAccessDeniedError(err);\n this.diag?.(DIAG.INBOX_GRANT_FAILED, { attempt: this.connectAttempt, terminal });\n if (terminal) return this.denyAccessTerminally();\n this.reconnectCause = 'grant-error';\n if (!this.state.hasLoadedInitialData) this.initialLoad.noteFailedAttempt();\n return this.scheduleReconnect();\n }\n // A grant minted for an earlier lifecycle must never open a socket in this one.\n if (lifecycle !== this.lifecycle || this.closedByUs) return;\n const url = `${this.config.endpoint.replace(/\\/$/, '')}/inbox`;\n // Every callback is fenced to THIS socket: once it has been replaced or torn down, its\n // late open, frames, and close are ignored.\n let socket: RealtimeSocket | null = null;\n const isCurrent = () => socket !== null && this.socket === socket;\n socket = this.config.socketFactory({\n url,\n grantToken,\n handlers: {\n onOpen: () => {\n if (isCurrent()) this.onOpen();\n },\n onMessage: (data) => {\n if (isCurrent()) this.onMessage(data);\n },\n onClose: (code, reason) => {\n if (isCurrent()) this.onClose(code, reason);\n },\n onError: () => undefined,\n },\n });\n this.socket = socket;\n }\n\n private onOpen(): void {\n this.controller.onOpen();\n this.reauthAttempted = false;\n this.setState({ status: 'open' });\n this.diag?.(DIAG.INBOX_SOCKET_OPEN, {\n attempt: this.connectAttempt,\n cause: this.reconnectCause,\n registryCount: this.state.registry.length,\n hasUnread: this.state.hasUnread,\n });\n // The DO sends a snapshot on accept; a `resume` re-requests it after a reconnect gap.\n const sent = this.sendFrame(CLIENT_FRAME.RESUME, {});\n // The inbox resume is CURSORLESS by contract (the DO always answers with a full\n // authoritative snapshot) — there is no `afterSeq` half to record here.\n this.diag?.(DIAG.INBOX_RESUME_REQUEST, { attempt: this.connectAttempt, cursorless: true, sent });\n }\n\n private onClose(code: number, _reason: string): void {\n this.socket = null;\n this.lastCloseCode = code;\n // A mark-read sent on this socket can no longer be answered.\n this.settlePendingMarkReads({ ok: false, code: 'connection-lost' });\n if (this.closedByUs) {\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: true, outcome: 'closed' });\n this.setState({ status: 'closed' });\n return;\n }\n if (code === CHAT_CLOSE_CODES.AUTH_EXPIRED && !this.reauthAttempted) {\n this.reauthAttempted = true;\n this.reconnectCause = 'auth-expired';\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 're-grant' });\n this.setState({ status: 'reconnecting' });\n void this.openSocket();\n return;\n }\n if (code === CHAT_CLOSE_CODES.REVOKED) {\n this.closedByUs = true;\n this.controller.close();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'revoked' });\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.REVOKED });\n this.reportInitialLoadFailed();\n return;\n }\n this.reconnectCause = 'transient-close';\n if (!this.state.hasLoadedInitialData) this.initialLoad.noteFailedAttempt();\n this.diag?.(DIAG.INBOX_SOCKET_CLOSE, { code, closedByUs: false, outcome: 'reconnect' });\n this.scheduleReconnect();\n }\n\n /**\n * Terminal access denial surfaced by the inbox grant provider\n * (`ChatAccessDeniedError`). Same terminal posture as the 4403 REVOKED close: mark\n * closed-by-us, close the reconnect controller, and surface the stable\n * `access-denied` code. The inbox client tracks NO optimistic/pending sends\n * (mark-read is fire-and-forget with no local pending state), so — unlike\n * ChannelClient — there is nothing to fail here; only the lifecycle stops.\n */\n private denyAccessTerminally(): void {\n this.closedByUs = true;\n this.controller.close();\n this.initialLoad.dispose();\n this.setState({ status: 'closed', lastErrorCode: TERMINAL_ERROR_CODE.ACCESS_DENIED });\n }\n\n private scheduleReconnect(): void {\n const delay = this.controller.onClose();\n this.diag?.(DIAG.INBOX_RECONNECT_SCHEDULED, {\n delayMs: delay,\n cause: this.reconnectCause,\n attempt: this.connectAttempt,\n });\n if (delay == null) {\n this.setState({ status: 'closed' });\n this.reportInitialLoadFailed();\n return;\n }\n this.setState({ status: 'reconnecting' });\n const lifecycle = this.lifecycle;\n this.reconnectTimer = this.timers.setTimeout(() => {\n this.reconnectTimer = null;\n if (this.closedByUs || lifecycle !== this.lifecycle) return;\n void this.openSocket();\n }, delay);\n }\n\n private onMessage(data: string): void {\n const frame = parseFrame(data);\n if (!frame || !frame.payload) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: 'unparseable', reason: 'bad-envelope' });\n return;\n }\n if (frame.type === SERVER_KINDS.MARK_READ_RESULT) return this.applyMarkReadResult(frame.payload);\n // The inbox DO pushes a full `snapshot`; a future lightweight `unread` push\n // carries either a full snapshot or a `{ hasUnread }` dock-dot patch. Route both\n // instead of silently dropping the `unread` type (C-M2).\n if (frame.type !== 'snapshot' && frame.type !== 'unread') {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: safeDiagnosticLabel(frame.type), reason: 'unknown-type' });\n return;\n }\n const payload = frame.payload as unknown as WireInboxSnapshot | { hasUnread: boolean };\n if (isInboxSnapshot(payload as WireInboxSnapshot)) {\n this.applySnapshot(payload as WireInboxSnapshot, frame.type);\n } else if (frame.type === 'unread' && typeof (payload as { hasUnread?: unknown }).hasUnread === 'boolean') {\n const hasUnread = Boolean((payload as { hasUnread: boolean }).hasUnread);\n const before = this.state.hasUnread;\n this.setState({ hasUnread });\n if (before !== hasUnread) {\n this.diag?.(DIAG.INBOX_UNREAD_UPDATED, {\n source: 'unread-patch',\n hasUnreadBefore: before,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: this.state.unreadChannelRefs.length,\n unreadCountAfter: this.state.unreadChannelRefs.length,\n unreadCountDelta: 0,\n });\n }\n } else {\n // A stray CHANNEL snapshot ({ lastMessageSeq, readSeq }) on the inbox socket —\n // deliberately inert, but never silently invisible under diagnostics.\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, {\n kind: safeDiagnosticLabel(frame.type),\n reason: 'not-an-inbox-payload',\n });\n }\n }\n\n private applySnapshot(snap: WireInboxSnapshot, source: string): void {\n // Keep every non-tombstoned entry — including ARCHIVED rows — so the inbox view can\n // render both the active list and the Archived toggle. `archived` is a distinct\n // dimension from `tombstoned`; archived rows are still `state: 'active'`.\n const registry = snap.registry.filter((e) => e.state === 'active');\n // The DO's `hasUnread` is the authoritative dock dot. Per-row dots are derived from\n // the same projection but exclude ARCHIVED rows (archive = done — an archived row\n // never shows a dot). The DO clears unread on archive, so this is belt-and-braces.\n const unreadChannelRefs = deriveUnreadRefs(snap);\n const hasUnread = Boolean(snap.hasUnread);\n const before = this.state;\n if (!before.hasLoadedInitialData) this.initialLoad.loaded();\n this.setState({ registry, hasUnread, unreadChannelRefs, hasLoadedInitialData: true, initialLoadFailed: false });\n if (!this.diag) return;\n // SIZES ONLY — a channelRef names one specific conversation and never enters a\n // diagnostic line.\n this.diag(DIAG.INBOX_SNAPSHOT_APPLIED, {\n source: safeDiagnosticLabel(source),\n received: snap.registry.length,\n // `received - active` is the tombstoned count the client filtered out.\n active: registry.length,\n archived: registry.filter((e) => e.archived === true).length,\n hasUnread,\n unreadCount: unreadChannelRefs.length,\n registryDelta: registry.length - before.registry.length,\n });\n const unreadChanged =\n before.hasUnread !== hasUnread || before.unreadChannelRefs.length !== unreadChannelRefs.length;\n if (unreadChanged) {\n this.diag(DIAG.INBOX_UNREAD_UPDATED, {\n source: safeDiagnosticLabel(source),\n hasUnreadBefore: before.hasUnread,\n hasUnreadAfter: hasUnread,\n unreadCountBefore: before.unreadChannelRefs.length,\n unreadCountAfter: unreadChannelRefs.length,\n unreadCountDelta: unreadChannelRefs.length - before.unreadChannelRefs.length,\n });\n }\n }\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 /** True if a specific channel/invite currently carries an unread dot. */\n channelHasUnread(channelRef: string): boolean {\n return this.state.unreadChannelRefs.includes(channelRef);\n }\n\n /**\n * Clear a channel/invite's unread WITHOUT opening it (the inbox-view mark-read\n * controls). Sends `mark-read` with a fresh `requestId`; the inbox runtime advances the\n * member's read cursor, pushes a fresh authoritative snapshot (which is what removes\n * the dot — there is NO optimistic local clear), and answers with a\n * `mark-read-result` naming the same request. The promise settles on that answer —\n * never on a timer — or with `not-sent` when no socket is open, or `connection-lost`\n * when the socket closes first.\n */\n markRead(channelRef: string): Promise<ChatMarkReadOutcome> {\n const requestId = newCorrelationId();\n const sent = this.sendFrame(CLIENT_FRAME.MARK_READ, { channelRef, requestId });\n // The ref itself is never logged — only that a mark-read was written and what the\n // unread projection looked like at that moment (the DO's snapshot is what clears it).\n this.diag?.(DIAG.INBOX_MARK_READ, { sent, unreadCount: this.state.unreadChannelRefs.length });\n if (!sent) return Promise.resolve({ ok: false, code: 'not-sent' });\n return new Promise((resolve) => this.pendingMarkReads.set(requestId, resolve));\n }\n\n /** Settle the mark-read the result names; a result naming no pending request is ignored. */\n private applyMarkReadResult(payload: Record<string, unknown> | undefined): void {\n const parsed = ChatMarkReadResultPayloadSchema.safeParse(payload);\n if (!parsed.success) {\n this.diag?.(DIAG.INBOX_FRAME_DROPPED, { kind: SERVER_KINDS.MARK_READ_RESULT, reason: 'malformed-payload' });\n return;\n }\n const resolve = this.pendingMarkReads.get(parsed.data.requestId);\n if (!resolve) return;\n this.pendingMarkReads.delete(parsed.data.requestId);\n resolve(parsed.data.ok ? { ok: true } : { ok: false, code: parsed.data.code });\n }\n\n private settlePendingMarkReads(outcome: ChatMarkReadOutcome): void {\n const pending = [...this.pendingMarkReads.values()];\n this.pendingMarkReads.clear();\n for (const resolve of pending) resolve(outcome);\n }\n\n /** Permanently close (auth-user switch / unmount). Idempotent. */\n close(): void {\n this.closedByUs = true;\n this.lifecycle += 1;\n this.controller.close();\n this.initialLoad.dispose();\n if (this.reconnectTimer != null) {\n this.timers.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.socket) {\n // Detach first: the socket's own late close is not this lifecycle's event.\n const socket = this.socket;\n this.socket = null;\n socket.close(1000, 'client teardown');\n }\n this.settlePendingMarkReads({ ok: false, code: 'connection-lost' });\n this.setState({ status: 'closed' });\n }\n}\n\n/**\n * Derive the per-channel unread set from a snapshot. The inbox DO's snapshot\n * carries a per-entry `unread` boolean on every active registry entry alongside\n * the global `hasUnread` roll-up that drives the dock dot. An entry without the\n * field (a legacy/pre-per-entry row) simply carries no per-row dot — the dock dot\n * still lights off `hasUnread`.\n *\n * ARCHIVED rows are excluded from the unread roll-up (archive = done — the DO clears\n * unread on archive; the client also never counts them).\n */\nfunction deriveUnreadRefs(snap: WireInboxSnapshot): string[] {\n const refs: string[] = [];\n for (const e of snap.registry) {\n if (e.state !== 'active') continue;\n if (e.archived === true) continue;\n if (e.unread === true) refs.push(e.channelRef);\n }\n return refs;\n}\n"]}
|
package/dist/realtime/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
2
|
-
export type {
|
|
2
|
+
export type { ChatCloseCode, WireMessageRow, WireChannelSnapshot, WireInboxSnapshot, WireRegistryEntry, ServerFrame, RevisionKind, } from './wire.js';
|
|
3
3
|
export type { RealtimeSocket, SocketFactory, SocketHandlers } from './socket.js';
|
|
4
4
|
export { browserSocketFactory } from './socket.js';
|
|
5
5
|
export type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';
|
|
6
|
-
export { defaultTimers, ChatAccessDeniedError, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, } from './shared.js';
|
|
6
|
+
export { CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS, defaultTimers, ChatAccessDeniedError, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, } from './shared.js';
|
|
7
7
|
export { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, MODERATION_REDACTED_TEXT, } from './map.js';
|
|
8
8
|
export type { ModerationOverlay } from './map.js';
|
|
9
9
|
export { CHAT_CLIENT_DIAGNOSTIC_EVENTS } from './diagnostics.js';
|
|
@@ -11,7 +11,7 @@ export type { ChatClientDiagnosticsOption, ChatClientDiagnosticsSink, ChatClient
|
|
|
11
11
|
export { ChannelClient } from './channel-client.js';
|
|
12
12
|
export type { ChannelClientState, ChannelClientConfig } from './channel-client.js';
|
|
13
13
|
export { InboxClient } from './inbox-client.js';
|
|
14
|
-
export type { InboxClientState, InboxClientConfig } from './inbox-client.js';
|
|
14
|
+
export type { InboxClientState, InboxClientConfig, ChatMarkReadOutcome } from './inbox-client.js';
|
|
15
15
|
export { createRealtimeChatClient, createInboxClient, } from './transport.js';
|
|
16
16
|
export type { RealtimeChatClient, RealtimeTransportConfig } from './transport.js';
|
|
17
17
|
export { useRealtimeChatMessages } from './useRealtimeChatMessages.js';
|
|
@@ -1 +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,
|
|
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,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,YAAY,GACb,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,EACL,mCAAmC,EACnC,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,wBAAwB,GACzB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AACjE,YAAY,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,6BAA6B,GAC9B,MAAM,kBAAkB,CAAC;AAE1B,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,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAElG,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"}
|
package/dist/realtime/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// `createRealtimeChatClient(...)`.
|
|
6
6
|
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_FRAME, SERVER_FRAME, buildFrame, parseFrame, isInboxSnapshot, } from './wire.js';
|
|
7
7
|
export { browserSocketFactory } from './socket.js';
|
|
8
|
-
export { defaultTimers, ChatAccessDeniedError, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, } from './shared.js';
|
|
8
|
+
export { CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS, defaultTimers, ChatAccessDeniedError, isChatAccessDeniedError, HEARTBEAT_MS, TYPING_COALESCE_MS, HISTORY_PAGE_MAX, } from './shared.js';
|
|
9
9
|
export { wireRowToMessage, optimisticMessage, seqToMessageId, applyModerationOverlay, overlayFromRow, MODERATION_REDACTED_TEXT, } from './map.js';
|
|
10
10
|
export { CHAT_CLIENT_DIAGNOSTIC_EVENTS } from './diagnostics.js';
|
|
11
11
|
export { ChannelClient } from './channel-client.js';
|
|
@@ -1 +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;
|
|
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,EACL,mCAAmC,EACnC,aAAa,EACb,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,wBAAwB,GACzB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAOjE,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","sourcesContent":["// The realtime (Cloudflare Durable Object) chat transport — the client half of\n// the chat Worker wire protocol (Contract A connection + Contract C wire). The\n// FIRESTORE transport stays the unchanged default; this is opt-in via\n// `ChatCoreConfig.transport === 'realtime'` + a `realtime.client` from\n// `createRealtimeChatClient(...)`.\n\nexport {\n CHAT_SUBPROTOCOL,\n CHAT_WIRE_VERSION,\n CHAT_CLOSE_CODES,\n CLIENT_FRAME,\n SERVER_FRAME,\n buildFrame,\n parseFrame,\n isInboxSnapshot,\n} from './wire.js';\nexport type {\n ChatCloseCode,\n WireMessageRow,\n WireChannelSnapshot,\n WireInboxSnapshot,\n WireRegistryEntry,\n ServerFrame,\n RevisionKind,\n} from './wire.js';\n\nexport type { RealtimeSocket, SocketFactory, SocketHandlers } from './socket.js';\nexport { browserSocketFactory } from './socket.js';\n\nexport type { GrantProvider, TransportTimers, RealtimeStatus } from './shared.js';\nexport {\n CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS,\n defaultTimers,\n ChatAccessDeniedError,\n isChatAccessDeniedError,\n HEARTBEAT_MS,\n TYPING_COALESCE_MS,\n HISTORY_PAGE_MAX,\n} from './shared.js';\n\nexport {\n wireRowToMessage,\n optimisticMessage,\n seqToMessageId,\n applyModerationOverlay,\n overlayFromRow,\n MODERATION_REDACTED_TEXT,\n} from './map.js';\nexport type { ModerationOverlay } from './map.js';\n\nexport { CHAT_CLIENT_DIAGNOSTIC_EVENTS } from './diagnostics.js';\nexport type {\n ChatClientDiagnosticsOption,\n ChatClientDiagnosticsSink,\n ChatClientDiagnosticEventName,\n} from './diagnostics.js';\n\nexport { ChannelClient } from './channel-client.js';\nexport type { ChannelClientState, ChannelClientConfig } from './channel-client.js';\nexport { InboxClient } from './inbox-client.js';\nexport type { InboxClientState, InboxClientConfig, ChatMarkReadOutcome } from './inbox-client.js';\n\nexport {\n createRealtimeChatClient,\n createInboxClient,\n} from './transport.js';\nexport type { RealtimeChatClient, RealtimeTransportConfig } from './transport.js';\n\nexport { useRealtimeChatMessages } from './useRealtimeChatMessages.js';\nexport type { UseRealtimeChatMessagesResult } from './useRealtimeChatMessages.js';\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type TransportTimers } from './shared.js';
|
|
2
|
+
export declare class InitialLoadBudget {
|
|
3
|
+
private readonly timers;
|
|
4
|
+
/** Called when the budget runs out with still no data. */
|
|
5
|
+
private readonly onExhausted;
|
|
6
|
+
/** When the first attempt of the current no-data run started; null once data has loaded. */
|
|
7
|
+
private startedAt;
|
|
8
|
+
private timer;
|
|
9
|
+
constructor(timers: TransportTimers,
|
|
10
|
+
/** Called when the budget runs out with still no data. */
|
|
11
|
+
onExhausted: () => void);
|
|
12
|
+
/**
|
|
13
|
+
* A lifecycle is starting without data. A fresh start opens a new budget; otherwise the budget
|
|
14
|
+
* already running (or spent) is kept.
|
|
15
|
+
*/
|
|
16
|
+
begin(fresh: boolean): void;
|
|
17
|
+
/** An attempt failed before any data loaded: once the budget is spent, that is reported at once. */
|
|
18
|
+
noteFailedAttempt(): void;
|
|
19
|
+
/**
|
|
20
|
+
* The user asked to retry. The spent budget stays spent — a retry that fails is reported at
|
|
21
|
+
* once — and a retry that hangs is reported a full budget from now.
|
|
22
|
+
*/
|
|
23
|
+
manualRetry(): void;
|
|
24
|
+
/** The first data loaded — there is no first-open failure left to report. */
|
|
25
|
+
loaded(): void;
|
|
26
|
+
/** The lifecycle ended; nothing may fire for it. */
|
|
27
|
+
dispose(): void;
|
|
28
|
+
private arm;
|
|
29
|
+
private clearTimer;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=initial-load-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initial-load-budget.d.ts","sourceRoot":"","sources":["../../src/realtime/initial-load-budget.ts"],"names":[],"mappings":"AAMA,OAAO,EAAuC,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAExF,qBAAa,iBAAiB;IAM1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW;IAP9B,4FAA4F;IAC5F,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAA0D;gBAGpD,MAAM,EAAE,eAAe;IACxC,0DAA0D;IACzC,WAAW,EAAE,MAAM,IAAI;IAG1C;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAM3B,oGAAoG;IACpG,iBAAiB,IAAI,IAAI;IAKzB;;;OAGG;IACH,WAAW,IAAI,IAAI;IAKnB,6EAA6E;IAC7E,MAAM,IAAI,IAAI;IAKd,oDAAoD;IACpD,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,GAAG;IAQX,OAAO,CAAC,UAAU;CAMnB"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The first-open failure budget both realtime clients share: a client that has not loaded its
|
|
2
|
+
// first data CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS after its first attempt started reports the first
|
|
3
|
+
// open as failed — whether its attempts failed or one is still hanging (a grant mint stuck until
|
|
4
|
+
// its timeout, a socket that opened but never sent a snapshot) — while it keeps retrying in the
|
|
5
|
+
// background. One implementation for the channel and inbox sockets.
|
|
6
|
+
import { CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS } from './shared.js';
|
|
7
|
+
export class InitialLoadBudget {
|
|
8
|
+
timers;
|
|
9
|
+
onExhausted;
|
|
10
|
+
/** When the first attempt of the current no-data run started; null once data has loaded. */
|
|
11
|
+
startedAt = null;
|
|
12
|
+
timer = null;
|
|
13
|
+
constructor(timers,
|
|
14
|
+
/** Called when the budget runs out with still no data. */
|
|
15
|
+
onExhausted) {
|
|
16
|
+
this.timers = timers;
|
|
17
|
+
this.onExhausted = onExhausted;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A lifecycle is starting without data. A fresh start opens a new budget; otherwise the budget
|
|
21
|
+
* already running (or spent) is kept.
|
|
22
|
+
*/
|
|
23
|
+
begin(fresh) {
|
|
24
|
+
if (!fresh && this.startedAt != null)
|
|
25
|
+
return;
|
|
26
|
+
this.startedAt = this.timers.now();
|
|
27
|
+
this.arm(CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS);
|
|
28
|
+
}
|
|
29
|
+
/** An attempt failed before any data loaded: once the budget is spent, that is reported at once. */
|
|
30
|
+
noteFailedAttempt() {
|
|
31
|
+
if (this.startedAt == null)
|
|
32
|
+
return;
|
|
33
|
+
if (this.timers.now() - this.startedAt >= CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS)
|
|
34
|
+
this.onExhausted();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The user asked to retry. The spent budget stays spent — a retry that fails is reported at
|
|
38
|
+
* once — and a retry that hangs is reported a full budget from now.
|
|
39
|
+
*/
|
|
40
|
+
manualRetry() {
|
|
41
|
+
if (this.startedAt == null)
|
|
42
|
+
this.startedAt = this.timers.now();
|
|
43
|
+
this.arm(CHAT_INITIAL_LOAD_FAILURE_BUDGET_MS);
|
|
44
|
+
}
|
|
45
|
+
/** The first data loaded — there is no first-open failure left to report. */
|
|
46
|
+
loaded() {
|
|
47
|
+
this.clearTimer();
|
|
48
|
+
this.startedAt = null;
|
|
49
|
+
}
|
|
50
|
+
/** The lifecycle ended; nothing may fire for it. */
|
|
51
|
+
dispose() {
|
|
52
|
+
this.clearTimer();
|
|
53
|
+
}
|
|
54
|
+
arm(ms) {
|
|
55
|
+
this.clearTimer();
|
|
56
|
+
this.timer = this.timers.setTimeout(() => {
|
|
57
|
+
this.timer = null;
|
|
58
|
+
if (this.startedAt != null)
|
|
59
|
+
this.onExhausted();
|
|
60
|
+
}, ms);
|
|
61
|
+
}
|
|
62
|
+
clearTimer() {
|
|
63
|
+
if (this.timer != null) {
|
|
64
|
+
this.timers.clearTimeout(this.timer);
|
|
65
|
+
this.timer = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=initial-load-budget.js.map
|