@pmate/chat-sdk 0.12.0-next.6 → 0.12.0-next.60

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 (64) hide show
  1. package/README.md +98 -3
  2. package/lib/runtime/batchedReadIndex.d.ts +16 -3
  3. package/lib/runtime/batchedReadIndex.js +50 -4
  4. package/lib/runtime/cachedMessage.d.ts +12 -0
  5. package/lib/runtime/cachedMessage.js +18 -0
  6. package/lib/runtime/chatClient.d.ts +1 -0
  7. package/lib/runtime/chatClient.js +1 -1
  8. package/lib/runtime/chatSession.d.ts +109 -12
  9. package/lib/runtime/chatSession.js +624 -40
  10. package/lib/runtime/connectivity.d.ts +4 -0
  11. package/lib/runtime/connectivity.js +8 -0
  12. package/lib/runtime/conversationRead.d.ts +13 -0
  13. package/lib/runtime/conversationRead.js +34 -0
  14. package/lib/runtime/conversationReading.d.ts +28 -0
  15. package/lib/runtime/conversationReading.js +47 -0
  16. package/lib/runtime/createChat.d.ts +5 -5
  17. package/lib/runtime/createChat.js +30 -17
  18. package/lib/runtime/durableSender.d.ts +97 -14
  19. package/lib/runtime/durableSender.js +786 -71
  20. package/lib/runtime/groupClient.d.ts +76 -0
  21. package/lib/runtime/groupClient.js +325 -0
  22. package/lib/runtime/index.d.ts +6 -0
  23. package/lib/runtime/index.js +5 -0
  24. package/lib/runtime/messageClient.d.ts +30 -8
  25. package/lib/runtime/messageClient.js +332 -66
  26. package/lib/runtime/messageControls.d.ts +19 -0
  27. package/lib/runtime/messageControls.js +44 -0
  28. package/lib/runtime/messageLaunch.d.ts +42 -0
  29. package/lib/runtime/messageLaunch.js +133 -0
  30. package/lib/runtime/messageQuotes.d.ts +3 -0
  31. package/lib/runtime/messageQuotes.js +17 -0
  32. package/lib/runtime/messageReaders.d.ts +12 -1
  33. package/lib/runtime/messageReaders.js +67 -3
  34. package/lib/runtime/messageStore.d.ts +38 -2
  35. package/lib/runtime/messageStore.js +215 -11
  36. package/lib/runtime/messageTimeline.d.ts +50 -0
  37. package/lib/runtime/messageTimeline.js +120 -0
  38. package/lib/runtime/outboxSchema.d.ts +5 -0
  39. package/lib/runtime/outboxSchema.js +59 -0
  40. package/lib/runtime/outgoingContent.d.ts +85 -0
  41. package/lib/runtime/outgoingContent.js +106 -0
  42. package/lib/runtime/readBatchJournal.d.ts +35 -0
  43. package/lib/runtime/readBatchJournal.js +210 -0
  44. package/lib/runtime/readCounts.d.ts +15 -1
  45. package/lib/runtime/readCounts.js +96 -8
  46. package/lib/runtime/readIndex.d.ts +68 -7
  47. package/lib/runtime/readIndex.js +460 -22
  48. package/lib/runtime/readSchema.d.ts +2 -0
  49. package/lib/runtime/readSchema.js +58 -0
  50. package/lib/runtime/readSync.d.ts +1 -0
  51. package/lib/runtime/readSync.js +25 -8
  52. package/lib/runtime/readingClient.d.ts +19 -0
  53. package/lib/runtime/readingClient.js +131 -19
  54. package/lib/runtime/readingContent.d.ts +5 -0
  55. package/lib/runtime/readingContent.js +23 -0
  56. package/lib/runtime/recordCache.d.ts +9 -0
  57. package/lib/runtime/recordCache.js +62 -5
  58. package/lib/runtime/responseCache.d.ts +57 -0
  59. package/lib/runtime/responseCache.js +230 -0
  60. package/lib/runtime/socialReadClient.d.ts +3 -1
  61. package/lib/runtime/socialReadClient.js +8 -4
  62. package/lib/runtime/voiceUpload.d.ts +2 -0
  63. package/lib/runtime/voiceUpload.js +27 -0
  64. package/package.json +6 -1
@@ -1,3 +1,10 @@
1
+ import { groupThreadId } from "./thread.js";
2
+ import { GroupClient } from "./groupClient.js";
3
+ import { isReadingControl } from "./readingContent.js";
4
+ import { outgoingContent } from "./outgoingContent.js";
5
+ import { projectMessageTimeline } from "./messageTimeline.js";
6
+ import { projectConversationReading } from "./conversationReading.js";
7
+ import { ReadCountsClient } from "./readCounts.js";
1
8
  import { WindowedTasks } from "./coalescer.js";
2
9
  import { MessageReadersClient } from "./messageReaders.js";
3
10
  import { MessageClient } from "./messageClient.js";
@@ -12,17 +19,43 @@ export class ChatSession {
12
19
  connection;
13
20
  reading;
14
21
  readers;
22
+ groups;
23
+ counts;
24
+ countsReceiver;
25
+ countsThread;
26
+ countSnapshots = new Map();
27
+ countClosings = new Set();
28
+ countTasks;
29
+ controlTails = new Map();
30
+ acceptedControls = new Set();
31
+ controlCount = 0;
32
+ controlBytes = 0;
15
33
  sender;
16
34
  listeners = new Map();
17
35
  snapshots = new Map();
36
+ timelineDecoded = new WeakMap();
37
+ decodeTimeline = (message) => { if (!this.timelineDecoded.has(message))
38
+ this.timelineDecoded.set(message, this.options.decode(message)); return this.timelineDecoded.get(message); };
39
+ historyEdges = new Map();
40
+ earlierReads = new Map();
18
41
  off = [];
19
42
  globalListeners = new Set();
20
43
  globalSnapshot;
44
+ closedThread = { messages: [], timeline: [], outgoing: [], hasEarlier: false, resetVersion: 0, complete: false, unread: undefined, messageReading: {} };
45
+ closedSummary = { threads: [], unreadMessageCount: undefined, badgeCount: undefined, connection: "closed" };
21
46
  connectionState = "closed";
22
47
  catchUp;
48
+ readingAllowed = false;
49
+ readingListeners = new Set();
50
+ acceptedReading = new Set();
51
+ readingAccess;
23
52
  readingStarted = false;
53
+ readingStarting;
54
+ withdrawn = false;
55
+ revoking;
24
56
  closed = false;
25
57
  starting;
58
+ initializing;
26
59
  closing;
27
60
  constructor(options) {
28
61
  this.options = options;
@@ -35,19 +68,77 @@ export class ChatSession {
35
68
  throw new Error("CHAT_SESSION_SCOPE_MISMATCH");
36
69
  }
37
70
  this.messages = new MessageClient(options.messages);
38
- this.connection = options.sharedConnection ?? new ChatClient(options.connection);
39
- this.reading = new ReadingClient({ ...options.reading, storageScope: JSON.stringify([scope.accountId, scope.teamId]) });
40
- this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
41
- this.sender = new DurableChatSender(this.connection, scope, options.onError);
71
+ this.connection = options.sharedConnection ?? new ChatClient({ ...options.connection, onReach: async (event) => {
72
+ const handled = await this.handleReach(event);
73
+ if (handled)
74
+ return handled;
75
+ if (options.connection.onReach)
76
+ return options.connection.onReach(event);
77
+ throw new Error("REACH_HANDLER_UNAVAILABLE");
78
+ } });
79
+ this.reading = new ReadingClient({ ...options.reading, teamId: scope.teamId, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true, metadataOnly: true });
80
+ this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, access: () => this.messages.scopeAccess(), onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
81
+ if (options.loadGroup) {
82
+ this.groups = new GroupClient({ scope, access: () => this.messages.scopeAccess(), load: options.loadGroup, onError: options.onError, onDenied: () => { void this.revokeAccess().catch(error => options.onError(error)); } });
83
+ this.off.push(this.groups.subscribe(thread => this.publish(thread)));
84
+ }
85
+ this.sender = new DurableChatSender(this.connection, scope, options.onError, { access: () => this.messages.scopeAccess(), uploadVoice: options.uploadVoice, reconcile: async (record, signal) => {
86
+ const cached = await this.messages.cachedMessage(record.threadId, record.message.hash);
87
+ if (cached && cached.from === scope.profileId && cached.to === record.message.to)
88
+ return true;
89
+ if (!options.reconcileOutgoing)
90
+ throw new Error("OUTBOX_RECONCILIATION_UNAVAILABLE");
91
+ try {
92
+ return await options.reconcileOutgoing(record, signal);
93
+ }
94
+ catch (error) {
95
+ if ([401, 403].includes(error?.status ?? 0))
96
+ await this.messages.revoke(record.threadId);
97
+ throw error;
98
+ }
99
+ } });
42
100
  this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
43
101
  options.onError(error); });
102
+ this.countTasks = new WindowedTasks(50, error => { if (!this.closed)
103
+ options.onError(error); });
104
+ if (typeof window !== "undefined") {
105
+ const wake = () => {
106
+ if (typeof document !== "undefined" && document.hidden)
107
+ return;
108
+ this.wakeCounts();
109
+ this.groups?.wake();
110
+ this.scheduleCatchUp();
111
+ };
112
+ for (const type of ["online", "focus", "pageshow"])
113
+ window.addEventListener(type, wake);
114
+ if (typeof document !== "undefined")
115
+ document.addEventListener("visibilitychange", wake);
116
+ this.off.push(() => {
117
+ for (const type of ["online", "focus", "pageshow"])
118
+ window.removeEventListener(type, wake);
119
+ if (typeof document !== "undefined")
120
+ document.removeEventListener("visibilitychange", wake);
121
+ });
122
+ }
123
+ this.off.push(this.messages.subscribeAccessRevoked(() => { if (!this.withdrawn && !this.closed)
124
+ void this.revokeAccess().catch(error => options.onError(error)); }));
44
125
  this.off.push(this.messages.subscribeAll(thread => this.publish(thread)));
45
- this.off.push(this.sender.subscribe(() => this.publishAll()), this.reading.subscribe(() => this.publishAll()));
126
+ this.off.push(this.sender.subscribe(() => this.publishAll()), this.reading.subscribe(() => { this.publishAll(); if (this.readingAllowed)
127
+ this.readingListeners.forEach(listener => listener()); }));
46
128
  this.off.push(this.connection.watchMessages(message => {
47
129
  try {
130
+ const body = message.body;
131
+ if (body?.kind === "group" && body.payload?.appId === scope.appId && body.payload.teamId === scope.teamId && body.payload.groupId === message.to) {
132
+ this.groups?.hint({ id: groupThreadId(scope.appId, message.to, scope.teamId), kind: "group", targetId: message.to, teamId: scope.teamId }, Number.isSafeInteger(body.payload.versionNumber) ? body.payload.versionNumber : undefined);
133
+ }
48
134
  const decoded = options.decode(message);
49
135
  if (decoded)
50
- void this.messages.receive(decoded.threadId, [decoded]).then(() => this.sender.confirm([message.hash])).catch(error => { if (String(error).includes("MESSAGE_PROJECTION_CHANGED"))
136
+ void this.messages.receive(decoded.threadId, [decoded]).then(async () => {
137
+ if (!this.closed && this.readingStarted)
138
+ this.reading.refresh();
139
+ if (message.from === scope.profileId)
140
+ await this.sender.confirm([message.hash]);
141
+ }).catch(error => { if (String(error).includes("MESSAGE_PROJECTION_CHANGED"))
51
142
  this.messages.invalidate(decoded.threadId);
52
143
  else if (!this.closed)
53
144
  options.onError(error); });
@@ -60,27 +151,132 @@ export class ChatSession {
60
151
  this.connectionState = state;
61
152
  this.publishGlobal();
62
153
  if (state === "connected") {
63
- const since = Date.now();
64
- this.catchUp.schedule("scope", () => this.messages.restore(since));
154
+ this.scheduleCatchUp();
65
155
  if (this.readingStarted)
66
156
  this.reading.refresh();
157
+ this.wakeCounts();
158
+ this.groups?.wake();
67
159
  }
68
160
  }));
69
161
  }
162
+ scheduleCatchUp() {
163
+ if (this.connectionState !== "connected" || this.closed || this.withdrawn)
164
+ return;
165
+ const since = Date.now();
166
+ this.catchUp.schedule("scope", async () => {
167
+ if (this.connectionState === "connected" && !this.closed && !this.withdrawn)
168
+ await this.messages.restore(since, this.options.activeThreadId);
169
+ });
170
+ }
70
171
  start() {
71
172
  this.assertOpen();
72
173
  return this.starting ??= (async () => {
73
- await this.sender.initialize();
74
- this.readingStarted = true;
75
- await this.reading.start();
174
+ await this.initialize();
76
175
  this.assertOpen();
77
176
  await this.connection.connect();
78
177
  })().catch(error => { this.starting = undefined; throw error; });
79
178
  }
80
- assertOpen() { if (this.closed)
179
+ /** Initializes local state and background reading sync without opening a
180
+ * second transport when the host already owns calls/presence. */
181
+ initialize() {
182
+ this.assertOpen();
183
+ return this.initializing ??= (async () => {
184
+ await this.sender.initialize();
185
+ this.assertOpen();
186
+ await this.startReading();
187
+ this.assertOpen();
188
+ })().catch(error => { this.initializing = undefined; throw error; });
189
+ }
190
+ startReading() {
191
+ if (this.readingStarted)
192
+ return Promise.resolve();
193
+ return this.readingStarting ??= (async () => {
194
+ const access = await this.messages.scopeAccess();
195
+ if (!access.allowed)
196
+ return;
197
+ this.assertOpen();
198
+ await this.reading.prepareTeam(this.options.messages.scope.teamId, access.version);
199
+ this.setReadingAllowed(true);
200
+ await this.reading.start();
201
+ this.assertOpen();
202
+ this.readingStarted = true;
203
+ })().finally(() => { this.readingStarting = undefined; });
204
+ }
205
+ /** One-time migration only: never establishes trusted history coverage. */
206
+ importLegacyMessages(threadId, messages) { this.assertOpen(); return this.messages.importLegacy(threadId, messages); }
207
+ getReadingSnapshot() { return this.closed || this.withdrawn || !this.readingAllowed ? undefined : this.reading.getSnapshot(); }
208
+ subscribeReading(listener) { if (this.closed || this.withdrawn)
209
+ return () => { }; this.readingListeners.add(listener); return () => { this.readingListeners.delete(listener); }; }
210
+ setReadingAllowed(allowed) {
211
+ if (this.readingAllowed === allowed)
212
+ return;
213
+ const previous = this.getReadingSnapshot();
214
+ this.readingAllowed = allowed;
215
+ if (previous !== this.getReadingSnapshot())
216
+ this.readingListeners.forEach(listener => listener());
217
+ this.publishAll();
218
+ }
219
+ readingAuthority() {
220
+ return this.readingAccess ??= this.messages.scopeAccess().then(access => { if (!access.allowed)
221
+ this.setReadingAllowed(false); return access; }).finally(() => { this.readingAccess = undefined; });
222
+ }
223
+ async authorizeReading() {
224
+ const access = await this.readingAuthority();
225
+ if (this.withdrawn || !access.allowed)
226
+ throw new Error("CHAT_ACCESS_REVOKED");
227
+ await this.reading.prepareTeam(this.options.messages.scope.teamId, access.version);
228
+ this.setReadingAllowed(true);
229
+ }
230
+ readingOperation(run, durable = true) {
231
+ if (this.closed || this.withdrawn)
232
+ return Promise.reject(new Error(this.withdrawn ? "CHAT_ACCESS_REVOKED" : "CHAT_SESSION_CLOSED"));
233
+ const task = (async () => {
234
+ await this.authorizeReading();
235
+ if (this.withdrawn)
236
+ throw new Error("CHAT_ACCESS_REVOKED");
237
+ if (!durable)
238
+ this.assertOpen();
239
+ const next = await run();
240
+ if (this.withdrawn || !(await this.readingAuthority()).allowed)
241
+ throw new Error("CHAT_ACCESS_REVOKED");
242
+ return next;
243
+ })();
244
+ if (durable) {
245
+ this.acceptedReading.add(task);
246
+ void task.finally(() => this.acceptedReading.delete(task)).catch(() => { });
247
+ }
248
+ return task;
249
+ }
250
+ refreshReading() {
251
+ if (this.closed || this.withdrawn)
252
+ return;
253
+ void this.authorizeReading().then(() => { if (!this.closed && !this.withdrawn)
254
+ this.reading.refresh(); }).catch(error => { if (!this.closed && !this.withdrawn)
255
+ this.options.onError(error); });
256
+ }
257
+ synchronizeReading(cursor, profileId) { return this.readingOperation(() => this.reading.synchronize(cursor, profileId), false); }
258
+ /** Explicit text-history acknowledgement; unheard audio stays unread. */
259
+ async markRead(threadId, messages) {
260
+ this.assertOpen();
261
+ const cached = messages ?? this.getSnapshot(threadId).timeline.flatMap(item => {
262
+ if (item.recalled || !item.message)
263
+ return [];
264
+ const candidate = this.options.readingCandidate?.(item.message);
265
+ return candidate ? [{ id: item.id, threadId, from: item.from, mine: item.mine, recalled: item.recalled, readPolicy: candidate.readPolicy }] : [];
266
+ });
267
+ return this.readingOperation(() => this.reading.markRead(threadId, cached));
268
+ }
269
+ updateReading(transform) { return this.readingOperation(() => this.reading.update(transform)); }
270
+ assertOpen() { if (this.withdrawn)
271
+ throw new Error("CHAT_ACCESS_REVOKED"); if (this.closed)
81
272
  throw new Error("CHAT_SESSION_CLOSED"); }
82
273
  async getMessages(threadId, options = {}) {
83
274
  this.assertOpen();
275
+ if (options.earlier) {
276
+ if (options.before || options.all || options.limit !== undefined || options.refresh)
277
+ throw new Error("INVALID_MESSAGE_QUERY");
278
+ return this.readEarlier(threadId);
279
+ }
84
280
  if (options.all && (options.before || options.limit !== undefined))
85
281
  throw new Error("INVALID_MESSAGE_QUERY");
86
282
  // Explicit authoritative changes (for example history assignment) can invalidate
@@ -91,9 +287,53 @@ export class ChatSession {
91
287
  return this.readAll(threadId);
92
288
  }
93
289
  const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
94
- await this.sender.confirm(page.messages.map(message => message.id));
290
+ this.assertOpen();
291
+ await this.confirmMessages(page.messages);
292
+ if (this.initializing && !this.readingStarted && page.complete)
293
+ await this.startReading();
294
+ this.assertOpen();
295
+ const edge = this.historyEdges.get(threadId);
296
+ if (!options.before && page.complete && (!edge || edge.version !== page.resetVersion))
297
+ this.rememberHistoryEdge(threadId, page);
95
298
  return page;
96
299
  }
300
+ async confirmMessages(messages) {
301
+ await this.sender.initialize();
302
+ // Only this profile can create its outgoing wire hashes. Keep the legacy
303
+ // fallback when a generic host has not supplied an author on stored rows.
304
+ const hashes = messages.filter(message => {
305
+ const from = message.from;
306
+ return typeof from !== "string" || !from || from === this.options.messages.scope.profileId;
307
+ }).map(message => message.id);
308
+ return hashes.length ? this.sender.confirm(hashes) : Promise.resolve();
309
+ }
310
+ rememberHistoryEdge(threadId, page) {
311
+ const first = page.messages[0];
312
+ this.historyEdges.set(threadId, { before: first ? { timestamp: first.timestamp, id: first.id } : undefined, version: page.resetVersion, hasEarlier: page.hasEarlier });
313
+ }
314
+ readEarlier(threadId) {
315
+ const running = this.earlierReads.get(threadId);
316
+ if (running)
317
+ return running;
318
+ const task = (async () => {
319
+ const recent = await this.getMessages(threadId);
320
+ if (!recent.complete)
321
+ throw new Error("MESSAGE_WINDOW_INCOMPLETE");
322
+ const edge = this.historyEdges.get(threadId);
323
+ if (!edge.hasEarlier)
324
+ return { ...recent, messages: [], hasEarlier: false };
325
+ if (!edge.before)
326
+ throw new Error("MESSAGE_SYNC_NO_PROGRESS");
327
+ const page = await this.messages.earlier(threadId, edge.before, 100, true);
328
+ await this.confirmMessages(page.messages);
329
+ this.assertOpen();
330
+ this.rememberHistoryEdge(threadId, page);
331
+ return page;
332
+ })().finally(() => { if (this.earlierReads.get(threadId) === task)
333
+ this.earlierReads.delete(threadId); });
334
+ this.earlierReads.set(threadId, task);
335
+ return task;
336
+ }
97
337
  async readAll(threadId) {
98
338
  for (let attempt = 0; attempt < 3; attempt++) {
99
339
  const pages = [];
@@ -115,12 +355,18 @@ export class ChatSession {
115
355
  break;
116
356
  }
117
357
  pages.push(page.messages);
358
+ if (!page.complete)
359
+ return { messages: pages.reverse().flat(), hasEarlier: true, complete: false, resetVersion: version };
118
360
  }
119
361
  if (restart)
120
362
  continue;
121
- return { messages: pages.reverse().flat(), hasEarlier: false, complete: true, resetVersion: version };
363
+ const result = { messages: pages.reverse().flat(), hasEarlier: false, complete: true, resetVersion: version };
364
+ await this.messages.expandWindow(threadId, result);
365
+ this.rememberHistoryEdge(threadId, result);
366
+ return result;
122
367
  }
123
368
  catch (error) {
369
+ this.assertOpen();
124
370
  if ([401, 403].includes(error?.status ?? 0))
125
371
  throw error;
126
372
  if (String(error).includes("MESSAGE_PROJECTION_CHANGED"))
@@ -132,20 +378,239 @@ export class ChatSession {
132
378
  }
133
379
  throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
134
380
  }
135
- sendMessage(threadId, input) {
381
+ /** Reads trusted local group metadata immediately; SDK refreshes it in the background. */
382
+ getGroup(thread, options = {}) {
136
383
  this.assertOpen();
137
- return this.sender.enqueue(threadId, input);
384
+ if (!this.groups)
385
+ throw new Error("GROUP_METADATA_UNAVAILABLE");
386
+ return this.groups.get(thread, options);
138
387
  }
139
- getReaders(threadId, messageId, page = {}, signal) {
388
+ async createGroup(thread, content) {
140
389
  this.assertOpen();
141
- return this.readers.getReaders({ ...page, threadId, messageId }, signal);
390
+ if (!this.groups)
391
+ throw new Error("GROUP_METADATA_UNAVAILABLE");
392
+ await this.sendMessage(thread, { groupCreate: content }, { waitFor: "delivered" });
393
+ return this.groups.after(thread, 1);
394
+ }
395
+ async updateGroup(thread, changes) {
396
+ const previous = await this.getGroup(thread);
397
+ if (previous.versionNumber >= Number.MAX_SAFE_INTEGER)
398
+ throw new Error("INVALID_GROUP_VERSION");
399
+ await this.sendMessage(thread, { groupUpdate: { ...changes, expectedRevision: changes.expectedRevision ?? previous.revision } }, { waitFor: "delivered" });
400
+ return this.groups.after(thread, previous.versionNumber + 1);
401
+ }
402
+ sendMessage(thread, input, options = {}) {
403
+ this.assertOpen();
404
+ if (options.waitFor !== undefined && !["accepted", "delivered"].includes(options.waitFor))
405
+ throw new Error("INVALID_MESSAGE_WAIT");
406
+ const threadId = typeof thread === "string" ? thread : thread.id;
407
+ const wire = typeof thread === "string" ? input : outgoingContent(this.options.messages.scope, thread, input);
408
+ const delivered = options.waitFor === "delivered";
409
+ if (typeof thread !== "string" && (input.recall || input.reaction))
410
+ return this.sendControl({ id: thread.id, kind: thread.kind, targetId: thread.targetId, teamId: thread.teamId, memberIds: wire.recipients }, wire, delivered);
411
+ return (delivered ? this.sender.send(threadId, wire) : this.sender.enqueue(threadId, wire)).then(next => { if (this.withdrawn)
412
+ throw new Error("CHAT_ACCESS_REVOKED"); return next; });
413
+ }
414
+ sendControl(thread, wire, delivered = false) {
415
+ const content = wire.body.content;
416
+ const recall = content.recall, reaction = content.reaction;
417
+ const id = recall?.messageId ?? reaction.messageId, key = JSON.stringify([thread.id, id, reaction?.emojiId ?? "recall"]);
418
+ const bytes = JSON.stringify(wire).length * 3 + 1024;
419
+ if (this.controlCount >= 4096 || this.controlBytes + bytes > 64 * 1024 * 1024)
420
+ return Promise.reject(new Error("MESSAGE_CONTROL_BACKPRESSURE"));
421
+ this.controlCount++;
422
+ this.controlBytes += bytes;
423
+ const previous = this.controlTails.get(key) ?? Promise.resolve();
424
+ let delivery;
425
+ const validated = previous.catch(() => { }).then(async () => {
426
+ this.assertOpen();
427
+ const item = this.getSnapshot(thread.id).timeline.find(item => item.id === id);
428
+ const target = (item?.message ?? await this.messages.cachedMessage(thread.id, id));
429
+ this.assertOpen();
430
+ const scope = this.options.messages.scope, from = item?.from ?? target?.from, body = target?.body;
431
+ const wrongScope = target?.threadId !== thread.id || target.appId !== undefined && target.appId !== scope.appId || body?.content?.appId !== undefined && body.content.appId !== scope.appId || body?.threadKind !== undefined && body.threadKind !== thread.kind;
432
+ const unavailable = !target || !from || id.startsWith("local-") || target.recalled || item?.recalled || target.legacyDisplay?.kind === "system" || item?.status === "failed" || item?.status === "sending";
433
+ const ordinary = body && ["message", "quote"].includes(String(body.kind)) && !isReadingControl(body, scope.appId);
434
+ if (wrongScope || unavailable || !ordinary)
435
+ throw new Error("MESSAGE_CONTROL_TARGET_UNAVAILABLE");
436
+ if (recall && from !== scope.profileId)
437
+ throw new Error("MESSAGE_RECALL_NOT_OWNED");
438
+ if (thread.kind === "dm" && from !== scope.profileId && from !== thread.targetId || thread.kind === "group" && (!wire.recipients?.includes(scope.profileId) || !wire.recipients.includes(from)))
439
+ throw new Error("MESSAGE_CONTROL_TARGET_UNAVAILABLE");
440
+ if (reaction) {
441
+ let floor = 0;
442
+ const events = item?.reactionEvents ?? target.reactionEvents ?? target.legacyDisplay?.reactionEvents ?? [];
443
+ for (const event of events)
444
+ if (event.from === scope.profileId && event.messageId === id && event.emojiId === reaction.emojiId && Number.isSafeInteger(event.revision) && event.revision > floor)
445
+ floor = event.revision;
446
+ for (const record of this.sender.snapshot(thread.id)) {
447
+ const envelope = record.message.body, event = envelope?.content?.reaction;
448
+ if (record.message.from === scope.profileId && envelope?.content?.appId === scope.appId && event?.messageId === id && event.emojiId === reaction.emojiId && Number.isSafeInteger(event.revision) && event.revision > floor)
449
+ floor = event.revision;
450
+ }
451
+ wire = { ...wire, reactionRevisionFloor: floor };
452
+ }
453
+ delivery = delivered ? this.sender.send(thread.id, wire) : this.sender.enqueue(thread.id, wire);
454
+ void delivery.catch(() => { });
455
+ });
456
+ const tail = validated.finally(() => { if (this.controlTails.get(key) === tail)
457
+ this.controlTails.delete(key); });
458
+ const task = tail.then(() => delivery).then(message => { if (this.withdrawn)
459
+ throw new Error("CHAT_ACCESS_REVOKED"); return message; }).finally(() => { this.controlCount--; this.controlBytes -= bytes; this.acceptedControls.delete(task); });
460
+ this.controlTails.set(key, tail);
461
+ this.acceptedControls.add(task);
462
+ return task;
463
+ }
464
+ async getReaders(threadId, messageId, page = {}, signal) {
465
+ this.assertOpen();
466
+ const result = await this.readers.getReaders({ ...page, threadId, messageId }, signal);
467
+ this.assertOpen();
468
+ if (!result.resetRequired) {
469
+ const previous = this.countSnapshots.get(threadId);
470
+ if (!previous || previous.revision <= result.revision) {
471
+ this.publishCounts({ appId: result.appId, threadId, revision: result.revision, counts: { ...previous?.counts, [messageId]: result.total } });
472
+ this.counts?.changed(threadId, result.revision);
473
+ }
474
+ }
475
+ return result;
476
+ }
477
+ /** Resolve voice media uniformly; pending local bytes are loaded only on demand. */
478
+ async getVoiceAttachment(threadId, messageId) {
479
+ this.assertOpen();
480
+ const item = this.getSnapshot(threadId).timeline.find(item => item.id === messageId);
481
+ if (!item || item.recalled)
482
+ throw new Error("VOICE_ATTACHMENT_UNAVAILABLE");
483
+ const local = await this.sender.acquireVoice(messageId, threadId);
484
+ try {
485
+ this.assertOpen();
486
+ }
487
+ catch (error) {
488
+ local?.release();
489
+ throw error;
490
+ }
491
+ const stillVisible = this.getSnapshot(threadId).timeline.find(item => item.id === messageId);
492
+ if (!stillVisible || stillVisible.recalled) {
493
+ local?.release();
494
+ throw new Error("VOICE_ATTACHMENT_UNAVAILABLE");
495
+ }
496
+ if (local)
497
+ return local;
498
+ const current = this.getSnapshot(threadId).timeline.find(item => item.id === messageId);
499
+ const body = current?.message?.body;
500
+ const value = body?.content?.voice?.url;
501
+ if (!current || current.recalled || typeof value !== "string")
502
+ throw new Error("VOICE_ATTACHMENT_UNAVAILABLE");
503
+ const url = new URL(value);
504
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
505
+ throw new Error("INVALID_VOICE_ATTACHMENT_URL");
506
+ return { url: value, release: () => { } };
142
507
  }
143
508
  retryMessage(messageId) { this.assertOpen(); return this.sender.retry(messageId); }
144
- observeMessages(threadId, ids, event) {
509
+ observeMessages(threadId, ids, event, clientInstanceId) {
510
+ if (this.closed || this.withdrawn) {
511
+ if (event === "receipts" && !threadId)
512
+ return Promise.resolve();
513
+ return Promise.reject(new Error(this.withdrawn ? "CHAT_ACCESS_REVOKED" : "CHAT_SESSION_CLOSED"));
514
+ }
515
+ if (event === "receipts") {
516
+ this.watchCounts(threadId, ids, clientInstanceId);
517
+ return Promise.resolve();
518
+ }
519
+ if (!threadId)
520
+ throw new Error("INVALID_READING_THREAD");
521
+ return this.readingOperation(() => event === "played" ? this.reading.recordExplicit(threadId, ids) : this.reading.recordVisible(threadId, ids));
522
+ }
523
+ /** Bridge transports can delegate social events without maintaining clients. */
524
+ async handleReach(event) {
145
525
  this.assertOpen();
146
- return event === "played" ? this.reading.recordExplicit(threadId, ids) : this.reading.recordVisible(threadId, ids);
526
+ if (!["social.readCountsChanged", "social.readStateChanged"].includes(event.eventType))
527
+ return undefined;
528
+ if (event.appId !== this.options.messages.scope.appId)
529
+ throw new Error("UNEXPECTED_READING_APPLICATION");
530
+ if (event.eventType === "social.readCountsChanged") {
531
+ if (typeof event.payload.threadId !== "string" || !Number.isSafeInteger(event.payload.revision) || Number(event.payload.revision) < 0)
532
+ throw new Error("INVALID_READ_COUNTS_EVENT");
533
+ this.counts?.changed(event.payload.threadId, Number(event.payload.revision));
534
+ }
535
+ else {
536
+ if (typeof event.payload.syncCursor !== "string" || !/^v1:\d+$/.test(event.payload.syncCursor) || typeof event.payload.profileId !== "string")
537
+ throw new Error("INVALID_READING_EVENT");
538
+ if (event.payload.profileId !== this.options.messages.scope.profileId)
539
+ throw new Error("Reading reach identity mismatch");
540
+ await this.initialize();
541
+ await this.synchronizeReading(event.payload.syncCursor, event.payload.profileId);
542
+ this.assertOpen();
543
+ }
544
+ return "handled";
545
+ }
546
+ wakeCounts() {
547
+ if (this.counts && this.countsThread && !this.closed)
548
+ this.countTasks.schedule("counts", async () => { this.counts?.reconnect(); });
549
+ }
550
+ publishCountSnapshot(thread) {
551
+ const previous = this.snapshots.get(thread);
552
+ if (!previous) {
553
+ this.publish(thread, false);
554
+ return;
555
+ }
556
+ // Receipt-only updates never serialize or rebuild message bodies.
557
+ this.snapshots.set(thread, { ...previous, readCounts: this.countSnapshots.get(thread) });
558
+ this.listeners.get(thread)?.forEach(listener => listener());
559
+ }
560
+ publishCounts(value) {
561
+ if (this.closed || this.withdrawn)
562
+ return;
563
+ const previous = this.countSnapshots.get(value.threadId);
564
+ if ((previous && previous.revision > value.revision) || JSON.stringify(previous) === JSON.stringify(value))
565
+ return;
566
+ this.countSnapshots.delete(value.threadId);
567
+ this.countSnapshots.set(value.threadId, value);
568
+ if (this.countSnapshots.size > 128) {
569
+ const oldest = this.countSnapshots.keys().next().value;
570
+ this.countSnapshots.delete(oldest);
571
+ this.publishCountSnapshot(oldest);
572
+ }
573
+ this.publishCountSnapshot(value.threadId);
574
+ }
575
+ watchCounts(threadId, ids, receiver = this.connection.identityScope().clientInstanceId) {
576
+ if (!threadId || !ids.length) {
577
+ this.countsThread = undefined;
578
+ this.counts?.watch(undefined, []);
579
+ return;
580
+ }
581
+ if (!receiver || receiver.length > 512 || /[\x00-\x1f]/.test(receiver))
582
+ throw new Error("INVALID_READ_COUNTS_RECEIVER");
583
+ if (!this.counts || this.countsReceiver !== receiver) {
584
+ if (this.counts) {
585
+ const closing = this.counts.close().finally(() => this.countClosings.delete(closing));
586
+ this.countClosings.add(closing);
587
+ }
588
+ const scope = this.options.messages.scope;
589
+ const client = new ReadCountsClient({ appId: scope.appId, profileId: scope.profileId, scope, access: () => this.messages.scopeAccess(), token: this.options.reading.sessionToken, clientInstanceId: receiver, hubHttpUrl: this.options.reading.hubHttpUrl, fetch: this.options.reading.fetch,
590
+ onChange: value => { if (!this.closed && this.counts === client)
591
+ this.publishCounts(value); },
592
+ onError: error => {
593
+ if (this.closed || this.counts !== client)
594
+ return;
595
+ if (/READ_COUNTS_HTTP_(401|403)|RESPONSE_CACHE_(ACCESS_REVOKED|SCOPE_CHANGED|INVALIDATED)/.test(String(error))) {
596
+ const threads = [...this.countSnapshots.keys()];
597
+ this.countSnapshots.clear();
598
+ threads.forEach(thread => this.publishCountSnapshot(thread));
599
+ }
600
+ this.options.onError(error);
601
+ } });
602
+ this.counts = client;
603
+ this.countsReceiver = receiver;
604
+ }
605
+ this.countsThread = threadId;
606
+ this.counts.watch(threadId, ids);
147
607
  }
148
608
  getSnapshot(threadId) {
609
+ if (this.closed || this.withdrawn) {
610
+ if (threadId === undefined)
611
+ return this.closedSummary;
612
+ return this.closedThread;
613
+ }
149
614
  if (threadId === undefined) {
150
615
  if (!this.globalSnapshot)
151
616
  this.globalSnapshot = this.buildGlobal();
@@ -153,56 +618,94 @@ export class ChatSession {
153
618
  }
154
619
  let snapshot = this.snapshots.get(threadId);
155
620
  if (!snapshot) {
156
- const messages = this.messages.getSnapshot(threadId);
157
- const state = this.reading.getSnapshot();
158
- snapshot = { messages: messages?.messages ?? [], hasEarlier: messages?.hasEarlier ?? false, resetVersion: messages?.resetVersion ?? 0, complete: messages?.complete ?? false, outgoing: this.sender.snapshot(threadId), unread: state?.threads.some(thread => (thread.conversationId ?? thread.id) === threadId) ? unreadInConversation(state, threadId) : { unreadMessageCount: 0, unreadMentionCount: 0, badgeCount: 0 } };
621
+ snapshot = this.buildThreadSnapshot(threadId);
159
622
  this.snapshots.set(threadId, snapshot);
160
623
  }
161
624
  return snapshot;
162
625
  }
163
- subscribe(target, listener) {
164
- this.assertOpen();
626
+ buildThreadSnapshot(threadId, previous) {
627
+ const source = this.messages.getSnapshot(threadId), raw = source?.messages ?? [];
628
+ const messages = previous && previous.messages.length === raw.length && previous.messages.every((message, index) => message === raw[index]) ? previous.messages : raw;
629
+ const outgoing = this.sender.snapshot(threadId);
630
+ const sameSource = previous && previous.messages === messages && previous.outgoing.length === outgoing.length && previous.outgoing.every((record, index) => record === outgoing[index]);
631
+ const candidates = new Map(messages.flatMap(message => { const candidate = this.options.readingCandidate?.(message); return candidate ? [[candidate.id, candidate]] : []; }));
632
+ const semanticReading = previous?.messageReading ?? {};
633
+ const semantics = sameSource ? previous.timeline : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, candidates, decode: this.decodeTimeline, reading: semanticReading, previous: previous?.timeline });
634
+ for (const item of semantics)
635
+ if (item.recalled)
636
+ candidates.set(item.id, { ...candidates.get(item.id), id: item.id, threadId, from: item.from, mine: item.mine, recalled: true });
637
+ const state = this.getReadingSnapshot();
638
+ const kind = state?.threads.find(thread => (thread.conversationId ?? thread.id) === threadId)?.kind ?? "dm";
639
+ const view = projectConversationReading(state, { id: threadId, kind }, [...candidates.values()]);
640
+ const unread = previous && JSON.stringify(previous.unread) === JSON.stringify(view.unread) ? previous.unread : view.unread;
641
+ const messageReading = previous && JSON.stringify(previous.messageReading) === JSON.stringify(view.messageReading) ? previous.messageReading : view.messageReading;
642
+ const timeline = messageReading === semanticReading ? semantics : projectMessageTimeline({ appId: this.options.messages.scope.appId, profileId: this.options.messages.scope.profileId, threadId, messages, outgoing, candidates, decode: this.decodeTimeline, reading: messageReading, previous: previous?.timeline });
643
+ return { group: this.groups?.snapshot(threadId).data, timeline, messages, hasEarlier: source?.hasEarlier ?? false, resetVersion: source?.resetVersion ?? 0, complete: source?.complete ?? false, outgoing, readCounts: this.countSnapshots.get(threadId), unread, messageReading };
644
+ }
645
+ subscribe(target, listener, fields) {
646
+ if (this.closed || this.withdrawn)
647
+ return () => { };
165
648
  if (typeof target === "function") {
166
649
  this.globalListeners.add(target);
167
650
  return () => { this.globalListeners.delete(target); };
168
651
  }
169
652
  const threadId = target;
170
- const notify = listener;
653
+ const selected = fields ? [...fields] : undefined;
654
+ let values = selected?.map(field => this.getSnapshot(threadId)[field]);
655
+ const equal = (a, b) => a === b || (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => value === b[index]));
656
+ const notify = selected ? () => {
657
+ const next = selected.map(field => this.getSnapshot(threadId)[field]);
658
+ const changed = next.some((value, index) => !equal(value, values[index]));
659
+ values = next;
660
+ if (changed)
661
+ listener();
662
+ } : listener;
171
663
  let listeners = this.listeners.get(threadId);
172
664
  if (!listeners) {
173
665
  listeners = new Set();
174
666
  this.listeners.set(threadId, listeners);
175
667
  }
176
668
  listeners.add(notify);
177
- const off = this.messages.subscribe(threadId, () => { });
669
+ const off = this.messages.subscribe(threadId, () => { }), unpin = this.groups?.pin(threadId);
178
670
  return () => {
179
671
  off();
672
+ unpin?.();
180
673
  listeners.delete(notify);
181
674
  if (!listeners.size)
182
675
  this.listeners.delete(threadId);
183
676
  };
184
677
  }
185
678
  publishAll() {
679
+ if (this.closed || this.withdrawn)
680
+ return;
186
681
  for (const threadId of [...this.snapshots.keys()])
187
682
  this.publish(threadId, false);
188
683
  this.publishGlobal();
189
684
  }
190
685
  buildGlobal() {
191
686
  const ids = new Set(this.snapshots.keys());
192
- for (const thread of this.reading.getSnapshot()?.threads ?? [])
687
+ for (const thread of this.getReadingSnapshot()?.threads ?? [])
193
688
  if (thread.teamId === this.options.messages.scope.teamId)
194
689
  ids.add(thread.conversationId ?? thread.id);
195
690
  for (const outgoing of this.sender.snapshot())
196
691
  ids.add(outgoing.threadId);
197
692
  const threads = [...ids].sort().map(threadId => {
198
693
  const snapshot = this.getSnapshot(threadId);
199
- return { threadId, lastMessage: snapshot.messages.at(-1), unread: snapshot.unread, outgoingCount: snapshot.outgoing.length };
694
+ return { threadId, group: snapshot.group, lastMessage: snapshot.timeline.at(-1)?.message, lastItem: snapshot.timeline.at(-1), unread: snapshot.unread, outgoingCount: snapshot.outgoing.length };
200
695
  });
201
- return { threads, unreadMessageCount: threads.reduce((sum, t) => sum + t.unread.unreadMessageCount, 0), badgeCount: threads.reduce((sum, t) => sum + t.unread.badgeCount, 0), connection: this.connectionState };
696
+ const state = this.getReadingSnapshot();
697
+ const known = Boolean(state && state.syncCursor !== "0" && threads.every(thread => thread.unread !== undefined));
698
+ return { threads, unreadMessageCount: known ? threads.reduce((sum, t) => sum + (t.unread?.unreadMessageCount ?? 0), 0) : undefined, badgeCount: known ? threads.reduce((sum, t) => sum + (t.unread?.badgeCount ?? 0), 0) : undefined, connection: this.connectionState };
202
699
  }
203
700
  publishGlobal() {
701
+ if (this.closed || this.withdrawn)
702
+ return;
204
703
  const next = this.buildGlobal();
205
- if (JSON.stringify(next) === JSON.stringify(this.globalSnapshot))
704
+ const previous = this.globalSnapshot;
705
+ if (previous && previous.connection === next.connection && previous.unreadMessageCount === next.unreadMessageCount && previous.badgeCount === next.badgeCount && previous.threads.length === next.threads.length && previous.threads.every((thread, index) => {
706
+ const other = next.threads[index];
707
+ return thread.threadId === other.threadId && thread.group === other.group && thread.lastMessage === other.lastMessage && thread.lastItem === other.lastItem && thread.outgoingCount === other.outgoingCount && JSON.stringify(thread.unread) === JSON.stringify(other.unread);
708
+ }))
206
709
  return;
207
710
  this.globalSnapshot = next;
208
711
  this.globalListeners.forEach(listener => listener());
@@ -210,8 +713,9 @@ export class ChatSession {
210
713
  publish(threadId, notifyGlobal = true) {
211
714
  const previous = this.snapshots.get(threadId);
212
715
  this.snapshots.delete(threadId);
213
- const next = this.getSnapshot(threadId);
214
- if (previous && JSON.stringify(previous) === JSON.stringify(next)) {
716
+ const next = this.buildThreadSnapshot(threadId, previous);
717
+ this.snapshots.set(threadId, next);
718
+ if (previous && previous.group === next.group && previous.messages === next.messages && previous.timeline === next.timeline && previous.outgoing.length === next.outgoing.length && previous.outgoing.every((record, index) => record === next.outgoing[index]) && previous.hasEarlier === next.hasEarlier && previous.resetVersion === next.resetVersion && previous.complete === next.complete && previous.readCounts === next.readCounts && JSON.stringify(previous.unread) === JSON.stringify(next.unread) && JSON.stringify(previous.messageReading) === JSON.stringify(next.messageReading)) {
215
719
  this.snapshots.set(threadId, previous);
216
720
  return;
217
721
  }
@@ -219,19 +723,99 @@ export class ChatSession {
219
723
  if (notifyGlobal)
220
724
  this.publishGlobal();
221
725
  }
222
- close() {
726
+ /** Permanently retire this identity/team session. UI withdrawal is immediate;
727
+ * completion means all durable stores have crossed the same authority epoch. */
728
+ revokeAccess() {
729
+ if (this.revoking)
730
+ return this.revoking;
731
+ if (this.closed)
732
+ return Promise.reject(new Error("CHAT_SESSION_CLOSED"));
733
+ this.withdrawn = true;
734
+ const knownThreads = [...new Set([...this.snapshots.keys(), ...this.historyEdges.keys()])];
735
+ this.catchUp.close();
736
+ this.countTasks.close();
737
+ this.off.splice(0).forEach(off => off());
738
+ this.sender.withdraw();
739
+ this.reading.withdraw();
740
+ this.readers.withdraw();
741
+ this.groups?.withdraw();
742
+ this.counts?.watch(undefined, []);
743
+ this.countSnapshots.clear();
744
+ this.snapshots.clear();
745
+ this.timelineDecoded = new WeakMap();
746
+ this.historyEdges.clear();
747
+ this.earlierReads.clear();
748
+ this.controlTails.clear();
749
+ this.globalSnapshot = undefined;
750
+ this.listeners.forEach(listeners => listeners.forEach(listener => listener()));
751
+ this.globalListeners.forEach(listener => listener());
752
+ this.readingListeners.forEach(listener => listener());
753
+ if (!this.options.sharedConnection)
754
+ this.connection.close();
755
+ return this.revoking = Promise.resolve().then(async () => {
756
+ const failures = [];
757
+ let version;
758
+ try {
759
+ version = await this.messages.revokeScope();
760
+ }
761
+ catch (error) {
762
+ failures.push(error);
763
+ version = 1;
764
+ try {
765
+ version = Math.max(1, (await this.messages.scopeAccess()).version);
766
+ }
767
+ catch (failure) {
768
+ failures.push(failure);
769
+ }
770
+ }
771
+ const results = await Promise.allSettled([
772
+ this.sender.revoke(version),
773
+ this.reading.revokeTeam(this.options.messages.scope.teamId, version, knownThreads),
774
+ this.readers.retireScope(version), this.counts?.retireScope(version), this.groups?.retire(version),
775
+ ]);
776
+ for (const result of results)
777
+ if (result.status === "rejected")
778
+ failures.push(result.reason);
779
+ try {
780
+ await this.dispose();
781
+ }
782
+ catch (error) {
783
+ failures.push(error);
784
+ }
785
+ if (failures.length)
786
+ throw new AggregateError(failures, "CHAT_ACCESS_CLEANUP_FAILED");
787
+ });
788
+ }
789
+ close() { return this.revoking ?? this.dispose(); }
790
+ dispose() {
223
791
  return this.closing ??= (async () => {
224
792
  this.closed = true;
225
793
  this.catchUp.close();
794
+ this.countTasks.close();
226
795
  this.off.splice(0).forEach(off => off());
227
796
  if (!this.options.sharedConnection)
228
797
  this.connection.close();
229
- this.readers.close();
230
- await Promise.all([this.sender.close(), this.reading.close(), this.messages.close()]);
231
- this.listeners.clear();
232
- this.globalListeners.clear();
233
- this.snapshots.clear();
234
- this.globalSnapshot = undefined;
798
+ const senderClosing = this.sender.close(), groupsClosing = this.groups?.close();
799
+ const readingBarrier = Promise.allSettled([...this.acceptedReading, ...this.acceptedControls]);
800
+ const readingClosing = readingBarrier.then(() => this.reading.close());
801
+ try {
802
+ const results = await Promise.allSettled([this.counts?.close(), ...this.countClosings, this.readers.close(), groupsClosing, senderClosing, readingClosing, this.messages.close(Promise.allSettled([senderClosing, groupsClosing, readingBarrier]))]);
803
+ const failed = results.find((result) => result.status === "rejected");
804
+ if (failed)
805
+ throw failed.reason;
806
+ }
807
+ finally {
808
+ this.listeners.clear();
809
+ this.globalListeners.clear();
810
+ this.readingListeners.clear();
811
+ this.snapshots.clear();
812
+ this.timelineDecoded = new WeakMap();
813
+ this.countSnapshots.clear();
814
+ this.historyEdges.clear();
815
+ this.earlierReads.clear();
816
+ this.controlTails.clear();
817
+ this.globalSnapshot = undefined;
818
+ }
235
819
  })();
236
820
  }
237
821
  }