@pmate/chat-sdk 0.12.0-next.20 → 0.12.0-next.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/lib/runtime/batchedReadIndex.d.ts +11 -2
- package/lib/runtime/batchedReadIndex.js +21 -1
- package/lib/runtime/chatSession.d.ts +10 -0
- package/lib/runtime/chatSession.js +129 -21
- package/lib/runtime/durableSender.d.ts +28 -4
- package/lib/runtime/durableSender.js +154 -34
- package/lib/runtime/messageClient.d.ts +17 -1
- package/lib/runtime/messageClient.js +164 -39
- package/lib/runtime/messageReaders.d.ts +4 -0
- package/lib/runtime/messageReaders.js +12 -4
- package/lib/runtime/messageStore.d.ts +16 -3
- package/lib/runtime/messageStore.js +74 -14
- package/lib/runtime/readCounts.d.ts +3 -0
- package/lib/runtime/readCounts.js +23 -5
- package/lib/runtime/readIndex.d.ts +21 -2
- package/lib/runtime/readIndex.js +47 -2
- package/lib/runtime/readSync.js +12 -4
- package/lib/runtime/readingClient.d.ts +11 -0
- package/lib/runtime/readingClient.js +78 -11
- package/lib/runtime/responseCache.d.ts +15 -0
- package/lib/runtime/responseCache.js +64 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,9 +158,17 @@ await chat.observeMessages(threadId, [voiceId], "played")
|
|
|
158
158
|
await chat.sendMessage(threadId, { to, body, recipients })
|
|
159
159
|
const readers = await chat.getReaders(threadId, messageId)
|
|
160
160
|
off(); offSummary()
|
|
161
|
+
// Ordinary navigation/shutdown preserves offline data.
|
|
161
162
|
await chat.close()
|
|
162
163
|
```
|
|
163
164
|
|
|
165
|
+
```ts
|
|
166
|
+
// When team access is withdrawn, use this instead of ordinary close(). It hides
|
|
167
|
+
// snapshots immediately, retires durable caches/outbox and closes this session.
|
|
168
|
+
// Other teams' reading operations remain intact. Create a new session to recover.
|
|
169
|
+
await chat.revokeAccess()
|
|
170
|
+
```
|
|
171
|
+
|
|
164
172
|
The facade owns all clients below. Message reads transparently restore IndexedDB
|
|
165
173
|
windows and fetch missing ranges through the authenticated Hub. Its normalized
|
|
166
174
|
messages retain the protocol body; product rendering remains with the host.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ReadIndexPersistence, type ReadIndexState } from "./readIndex.js";
|
|
1
|
+
import { type ReadIndexPersistence, type ReadIndexState, type ReadIndexTransform } from "./readIndex.js";
|
|
2
2
|
type Change = {
|
|
3
|
-
transform:
|
|
3
|
+
transform: ReadIndexTransform;
|
|
4
4
|
resolve: (state: ReadIndexState) => void;
|
|
5
5
|
reject: (error: unknown) => void;
|
|
6
6
|
};
|
|
@@ -15,6 +15,15 @@ export declare class BatchedReadIndex implements ReadIndexPersistence {
|
|
|
15
15
|
private closed;
|
|
16
16
|
constructor(storage: ReadIndexPersistence, windowMs?: number, batchSize?: number, queueLimit?: number);
|
|
17
17
|
load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
|
|
18
|
+
loadVersioned(appId: string, profileId: string): Promise<{
|
|
19
|
+
state: ReadIndexState | undefined;
|
|
20
|
+
versions: import("./readIndex.js").ReadIndexVersions;
|
|
21
|
+
} | {
|
|
22
|
+
state: ReadIndexState | undefined;
|
|
23
|
+
versions: {};
|
|
24
|
+
}>;
|
|
25
|
+
loadVersions(appId: string, profileId: string): Promise<{}>;
|
|
26
|
+
invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
|
|
18
27
|
update(appId: string, profileId: string, transform: Change["transform"]): Promise<ReadIndexState>;
|
|
19
28
|
private drain;
|
|
20
29
|
flush(appId: string, profileId: string): Promise<void>;
|
|
@@ -22,6 +22,26 @@ export class BatchedReadIndex {
|
|
|
22
22
|
throw new Error("READ_STORAGE_CLOSED");
|
|
23
23
|
return this.storage.load(appId, profileId);
|
|
24
24
|
}
|
|
25
|
+
async loadVersioned(appId, profileId) {
|
|
26
|
+
await this.flush(appId, profileId);
|
|
27
|
+
if (this.closed)
|
|
28
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
29
|
+
return this.storage.loadVersioned ? this.storage.loadVersioned(appId, profileId) : { state: await this.storage.load(appId, profileId), versions: {} };
|
|
30
|
+
}
|
|
31
|
+
async loadVersions(appId, profileId) {
|
|
32
|
+
if (this.closed)
|
|
33
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
34
|
+
return this.storage.loadVersions ? this.storage.loadVersions(appId, profileId) : {};
|
|
35
|
+
}
|
|
36
|
+
async invalidateTeam(appId, profileId, teamId, version, knownThreadIds) {
|
|
37
|
+
if (this.closed)
|
|
38
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
39
|
+
if (!this.storage.invalidateTeam)
|
|
40
|
+
throw new Error("READ_SCOPE_INVALIDATION_UNAVAILABLE");
|
|
41
|
+
// Do not flush a blocked old HTTP transform first. The durable epoch is the
|
|
42
|
+
// barrier; any queued transform sees it inside its eventual transaction.
|
|
43
|
+
return this.storage.invalidateTeam(appId, profileId, teamId, version, knownThreadIds);
|
|
44
|
+
}
|
|
25
45
|
update(appId, profileId, transform) {
|
|
26
46
|
if (this.closed)
|
|
27
47
|
return Promise.reject(new Error("READ_STORAGE_CLOSED"));
|
|
@@ -48,7 +68,7 @@ export class BatchedReadIndex {
|
|
|
48
68
|
return;
|
|
49
69
|
const run = (async () => {
|
|
50
70
|
try {
|
|
51
|
-
const next = await this.storage.update(queue.appId, queue.profileId, state => batch.reduce((current, change) => change.transform(current), state));
|
|
71
|
+
const next = await this.storage.update(queue.appId, queue.profileId, (state, versions) => batch.reduce((current, change) => change.transform(current, versions), state));
|
|
52
72
|
batch.forEach(change => change.resolve(next));
|
|
53
73
|
}
|
|
54
74
|
catch (error) {
|
|
@@ -59,9 +59,14 @@ export declare class ChatSession<T extends StoredMessage> {
|
|
|
59
59
|
private off;
|
|
60
60
|
private globalListeners;
|
|
61
61
|
private globalSnapshot?;
|
|
62
|
+
private closedThread;
|
|
63
|
+
private closedSummary;
|
|
62
64
|
private connectionState;
|
|
63
65
|
private catchUp;
|
|
64
66
|
private readingStarted;
|
|
67
|
+
private readingStarting?;
|
|
68
|
+
private withdrawn;
|
|
69
|
+
private revoking?;
|
|
65
70
|
private closed;
|
|
66
71
|
private starting?;
|
|
67
72
|
private initializing?;
|
|
@@ -71,6 +76,7 @@ export declare class ChatSession<T extends StoredMessage> {
|
|
|
71
76
|
/** Initializes local state and background reading sync without opening a
|
|
72
77
|
* second transport when the host already owns calls/presence. */
|
|
73
78
|
initialize(): Promise<void>;
|
|
79
|
+
private startReading;
|
|
74
80
|
/** One-time migration only: never establishes trusted history coverage. */
|
|
75
81
|
importLegacyMessages(threadId: string, messages: T[]): Promise<void>;
|
|
76
82
|
getReadingSnapshot(): import("./readIndex.js").ReadIndexState | undefined;
|
|
@@ -117,5 +123,9 @@ export declare class ChatSession<T extends StoredMessage> {
|
|
|
117
123
|
private buildGlobal;
|
|
118
124
|
private publishGlobal;
|
|
119
125
|
private publish;
|
|
126
|
+
/** Permanently retire this identity/team session. UI withdrawal is immediate;
|
|
127
|
+
* completion means all durable stores have crossed the same authority epoch. */
|
|
128
|
+
revokeAccess(): Promise<void>;
|
|
120
129
|
close(): Promise<void>;
|
|
130
|
+
private dispose;
|
|
121
131
|
}
|
|
@@ -27,9 +27,14 @@ export class ChatSession {
|
|
|
27
27
|
off = [];
|
|
28
28
|
globalListeners = new Set();
|
|
29
29
|
globalSnapshot;
|
|
30
|
+
closedThread = { messages: [], outgoing: [], hasEarlier: false, resetVersion: 0, complete: false, unread: undefined };
|
|
31
|
+
closedSummary = { threads: [], unreadMessageCount: undefined, badgeCount: undefined, connection: "closed" };
|
|
30
32
|
connectionState = "closed";
|
|
31
33
|
catchUp;
|
|
32
34
|
readingStarted = false;
|
|
35
|
+
readingStarting;
|
|
36
|
+
withdrawn = false;
|
|
37
|
+
revoking;
|
|
33
38
|
closed = false;
|
|
34
39
|
starting;
|
|
35
40
|
initializing;
|
|
@@ -54,8 +59,8 @@ export class ChatSession {
|
|
|
54
59
|
throw new Error("REACH_HANDLER_UNAVAILABLE");
|
|
55
60
|
} });
|
|
56
61
|
this.reading = new ReadingClient({ ...options.reading, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true });
|
|
57
|
-
this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
|
|
58
|
-
this.sender = new DurableChatSender(this.connection, scope, options.onError);
|
|
62
|
+
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 });
|
|
63
|
+
this.sender = new DurableChatSender(this.connection, scope, options.onError, { access: () => this.messages.scopeAccess() });
|
|
59
64
|
this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
|
|
60
65
|
options.onError(error); });
|
|
61
66
|
this.countTasks = new WindowedTasks(50, error => { if (!this.closed)
|
|
@@ -66,6 +71,8 @@ export class ChatSession {
|
|
|
66
71
|
window.addEventListener("focus", wake);
|
|
67
72
|
this.off.push(() => { window.removeEventListener("online", wake); window.removeEventListener("focus", wake); });
|
|
68
73
|
}
|
|
74
|
+
this.off.push(this.messages.subscribeAccessRevoked(() => { if (!this.withdrawn && !this.closed)
|
|
75
|
+
void this.revokeAccess().catch(error => options.onError(error)); }));
|
|
69
76
|
this.off.push(this.messages.subscribeAll(thread => this.publish(thread)));
|
|
70
77
|
this.off.push(this.sender.subscribe(() => this.publishAll()), this.reading.subscribe(() => this.publishAll()));
|
|
71
78
|
this.off.push(this.connection.watchMessages(message => {
|
|
@@ -112,21 +119,37 @@ export class ChatSession {
|
|
|
112
119
|
return this.initializing ??= (async () => {
|
|
113
120
|
await this.sender.initialize();
|
|
114
121
|
this.assertOpen();
|
|
115
|
-
this.
|
|
116
|
-
await this.reading.start();
|
|
122
|
+
await this.startReading();
|
|
117
123
|
this.assertOpen();
|
|
118
124
|
})().catch(error => { this.initializing = undefined; throw error; });
|
|
119
125
|
}
|
|
126
|
+
startReading() {
|
|
127
|
+
if (this.readingStarted)
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
return this.readingStarting ??= (async () => {
|
|
130
|
+
const access = await this.messages.scopeAccess();
|
|
131
|
+
if (!access.allowed)
|
|
132
|
+
return;
|
|
133
|
+
this.assertOpen();
|
|
134
|
+
await this.reading.prepareTeam(this.options.messages.scope.teamId, access.version);
|
|
135
|
+
await this.reading.start();
|
|
136
|
+
this.assertOpen();
|
|
137
|
+
this.readingStarted = true;
|
|
138
|
+
})().finally(() => { this.readingStarting = undefined; });
|
|
139
|
+
}
|
|
120
140
|
/** One-time migration only: never establishes trusted history coverage. */
|
|
121
141
|
importLegacyMessages(threadId, messages) { this.assertOpen(); return this.messages.importLegacy(threadId, messages); }
|
|
122
|
-
getReadingSnapshot() { return this.reading.getSnapshot(); }
|
|
142
|
+
getReadingSnapshot() { return this.closed || this.withdrawn ? undefined : this.reading.getSnapshot(); }
|
|
123
143
|
subscribeReading(listener) { this.assertOpen(); return this.reading.subscribe(listener); }
|
|
124
144
|
refreshReading() { this.assertOpen(); this.reading.refresh(); }
|
|
125
145
|
synchronizeReading(cursor, profileId) { this.assertOpen(); return this.reading.synchronize(cursor, profileId); }
|
|
126
146
|
/** Explicit text-history acknowledgement; unheard audio stays unread. */
|
|
127
|
-
markRead(threadId, messages = []) { this.assertOpen(); return this.reading.markRead(threadId, messages)
|
|
128
|
-
|
|
129
|
-
assertOpen() { if (this.
|
|
147
|
+
markRead(threadId, messages = []) { this.assertOpen(); return this.reading.markRead(threadId, messages).then(next => { if (this.withdrawn)
|
|
148
|
+
throw new Error("CHAT_ACCESS_REVOKED"); return next; }); }
|
|
149
|
+
updateReading(transform) { this.assertOpen(); return this.reading.update(transform).then(next => { if (this.withdrawn)
|
|
150
|
+
throw new Error("CHAT_ACCESS_REVOKED"); return next; }); }
|
|
151
|
+
assertOpen() { if (this.withdrawn)
|
|
152
|
+
throw new Error("CHAT_ACCESS_REVOKED"); if (this.closed)
|
|
130
153
|
throw new Error("CHAT_SESSION_CLOSED"); }
|
|
131
154
|
async getMessages(threadId, options = {}) {
|
|
132
155
|
this.assertOpen();
|
|
@@ -145,7 +168,11 @@ export class ChatSession {
|
|
|
145
168
|
return this.readAll(threadId);
|
|
146
169
|
}
|
|
147
170
|
const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
|
|
171
|
+
this.assertOpen();
|
|
148
172
|
await this.sender.confirm(page.messages.map(message => message.id));
|
|
173
|
+
if (this.initializing && !this.readingStarted && page.complete)
|
|
174
|
+
await this.startReading();
|
|
175
|
+
this.assertOpen();
|
|
149
176
|
const edge = this.historyEdges.get(threadId);
|
|
150
177
|
if (!options.before && page.complete && (!edge || edge.version !== page.resetVersion))
|
|
151
178
|
this.rememberHistoryEdge(threadId, page);
|
|
@@ -206,6 +233,7 @@ export class ChatSession {
|
|
|
206
233
|
return { messages: pages.reverse().flat(), hasEarlier: false, complete: true, resetVersion: version };
|
|
207
234
|
}
|
|
208
235
|
catch (error) {
|
|
236
|
+
this.assertOpen();
|
|
209
237
|
if ([401, 403].includes(error?.status ?? 0))
|
|
210
238
|
throw error;
|
|
211
239
|
if (String(error).includes("MESSAGE_PROJECTION_CHANGED"))
|
|
@@ -219,7 +247,8 @@ export class ChatSession {
|
|
|
219
247
|
}
|
|
220
248
|
sendMessage(threadId, input) {
|
|
221
249
|
this.assertOpen();
|
|
222
|
-
return this.sender.enqueue(threadId, input)
|
|
250
|
+
return this.sender.enqueue(threadId, input).then(next => { if (this.withdrawn)
|
|
251
|
+
throw new Error("CHAT_ACCESS_REVOKED"); return next; });
|
|
223
252
|
}
|
|
224
253
|
async getReaders(threadId, messageId, page = {}, signal) {
|
|
225
254
|
this.assertOpen();
|
|
@@ -245,7 +274,7 @@ export class ChatSession {
|
|
|
245
274
|
}
|
|
246
275
|
if (!threadId)
|
|
247
276
|
throw new Error("INVALID_READING_THREAD");
|
|
248
|
-
return event === "played" ? this.reading.recordExplicit(threadId, ids) : this.reading.recordVisible(threadId, ids);
|
|
277
|
+
return (event === "played" ? this.reading.recordExplicit(threadId, ids) : this.reading.recordVisible(threadId, ids)).then(next => { this.assertOpen(); return next; });
|
|
249
278
|
}
|
|
250
279
|
/** Bridge transports can delegate social events without maintaining clients. */
|
|
251
280
|
async handleReach(event) {
|
|
@@ -285,6 +314,8 @@ export class ChatSession {
|
|
|
285
314
|
this.listeners.get(thread)?.forEach(listener => listener());
|
|
286
315
|
}
|
|
287
316
|
publishCounts(value) {
|
|
317
|
+
if (this.closed || this.withdrawn)
|
|
318
|
+
return;
|
|
288
319
|
const previous = this.countSnapshots.get(value.threadId);
|
|
289
320
|
if ((previous && previous.revision > value.revision) || JSON.stringify(previous) === JSON.stringify(value))
|
|
290
321
|
return;
|
|
@@ -311,13 +342,13 @@ export class ChatSession {
|
|
|
311
342
|
this.countClosings.add(closing);
|
|
312
343
|
}
|
|
313
344
|
const scope = this.options.messages.scope;
|
|
314
|
-
const client = new ReadCountsClient({ appId: scope.appId, profileId: scope.profileId, scope, token: this.options.reading.sessionToken, clientInstanceId: receiver, hubHttpUrl: this.options.reading.hubHttpUrl, fetch: this.options.reading.fetch,
|
|
345
|
+
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,
|
|
315
346
|
onChange: value => { if (!this.closed && this.counts === client)
|
|
316
347
|
this.publishCounts(value); },
|
|
317
348
|
onError: error => {
|
|
318
349
|
if (this.closed || this.counts !== client)
|
|
319
350
|
return;
|
|
320
|
-
if (/READ_COUNTS_HTTP_(401|403)/.test(String(error))) {
|
|
351
|
+
if (/READ_COUNTS_HTTP_(401|403)|RESPONSE_CACHE_(ACCESS_REVOKED|SCOPE_CHANGED|INVALIDATED)/.test(String(error))) {
|
|
321
352
|
const threads = [...this.countSnapshots.keys()];
|
|
322
353
|
this.countSnapshots.clear();
|
|
323
354
|
threads.forEach(thread => this.publishCountSnapshot(thread));
|
|
@@ -331,6 +362,11 @@ export class ChatSession {
|
|
|
331
362
|
this.counts.watch(threadId, ids);
|
|
332
363
|
}
|
|
333
364
|
getSnapshot(threadId) {
|
|
365
|
+
if (this.closed || this.withdrawn) {
|
|
366
|
+
if (threadId === undefined)
|
|
367
|
+
return this.closedSummary;
|
|
368
|
+
return this.closedThread;
|
|
369
|
+
}
|
|
334
370
|
if (threadId === undefined) {
|
|
335
371
|
if (!this.globalSnapshot)
|
|
336
372
|
this.globalSnapshot = this.buildGlobal();
|
|
@@ -377,6 +413,8 @@ export class ChatSession {
|
|
|
377
413
|
};
|
|
378
414
|
}
|
|
379
415
|
publishAll() {
|
|
416
|
+
if (this.closed || this.withdrawn)
|
|
417
|
+
return;
|
|
380
418
|
for (const threadId of [...this.snapshots.keys()])
|
|
381
419
|
this.publish(threadId, false);
|
|
382
420
|
this.publishGlobal();
|
|
@@ -397,6 +435,8 @@ export class ChatSession {
|
|
|
397
435
|
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 };
|
|
398
436
|
}
|
|
399
437
|
publishGlobal() {
|
|
438
|
+
if (this.closed || this.withdrawn)
|
|
439
|
+
return;
|
|
400
440
|
const next = this.buildGlobal();
|
|
401
441
|
if (JSON.stringify(next) === JSON.stringify(this.globalSnapshot))
|
|
402
442
|
return;
|
|
@@ -415,7 +455,67 @@ export class ChatSession {
|
|
|
415
455
|
if (notifyGlobal)
|
|
416
456
|
this.publishGlobal();
|
|
417
457
|
}
|
|
418
|
-
|
|
458
|
+
/** Permanently retire this identity/team session. UI withdrawal is immediate;
|
|
459
|
+
* completion means all durable stores have crossed the same authority epoch. */
|
|
460
|
+
revokeAccess() {
|
|
461
|
+
if (this.revoking)
|
|
462
|
+
return this.revoking;
|
|
463
|
+
if (this.closed)
|
|
464
|
+
return Promise.reject(new Error("CHAT_SESSION_CLOSED"));
|
|
465
|
+
this.withdrawn = true;
|
|
466
|
+
const knownThreads = [...new Set([...this.snapshots.keys(), ...this.historyEdges.keys()])];
|
|
467
|
+
this.catchUp.close();
|
|
468
|
+
this.countTasks.close();
|
|
469
|
+
this.off.splice(0).forEach(off => off());
|
|
470
|
+
this.sender.withdraw();
|
|
471
|
+
this.reading.withdraw();
|
|
472
|
+
this.readers.withdraw();
|
|
473
|
+
this.counts?.watch(undefined, []);
|
|
474
|
+
this.countSnapshots.clear();
|
|
475
|
+
this.snapshots.clear();
|
|
476
|
+
this.historyEdges.clear();
|
|
477
|
+
this.earlierReads.clear();
|
|
478
|
+
this.globalSnapshot = undefined;
|
|
479
|
+
this.listeners.forEach(listeners => listeners.forEach(listener => listener()));
|
|
480
|
+
this.globalListeners.forEach(listener => listener());
|
|
481
|
+
if (!this.options.sharedConnection)
|
|
482
|
+
this.connection.close();
|
|
483
|
+
return this.revoking = Promise.resolve().then(async () => {
|
|
484
|
+
const failures = [];
|
|
485
|
+
let version;
|
|
486
|
+
try {
|
|
487
|
+
version = await this.messages.revokeScope();
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
failures.push(error);
|
|
491
|
+
version = 1;
|
|
492
|
+
try {
|
|
493
|
+
version = Math.max(1, (await this.messages.scopeAccess()).version);
|
|
494
|
+
}
|
|
495
|
+
catch (failure) {
|
|
496
|
+
failures.push(failure);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const results = await Promise.allSettled([
|
|
500
|
+
this.sender.revoke(version),
|
|
501
|
+
this.reading.revokeTeam(this.options.messages.scope.teamId, version, knownThreads),
|
|
502
|
+
this.readers.retireScope(version), this.counts?.retireScope(version),
|
|
503
|
+
]);
|
|
504
|
+
for (const result of results)
|
|
505
|
+
if (result.status === "rejected")
|
|
506
|
+
failures.push(result.reason);
|
|
507
|
+
try {
|
|
508
|
+
await this.dispose();
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
failures.push(error);
|
|
512
|
+
}
|
|
513
|
+
if (failures.length)
|
|
514
|
+
throw new AggregateError(failures, "CHAT_ACCESS_CLEANUP_FAILED");
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
close() { return this.revoking ?? this.dispose(); }
|
|
518
|
+
dispose() {
|
|
419
519
|
return this.closing ??= (async () => {
|
|
420
520
|
this.closed = true;
|
|
421
521
|
this.catchUp.close();
|
|
@@ -423,14 +523,22 @@ export class ChatSession {
|
|
|
423
523
|
this.off.splice(0).forEach(off => off());
|
|
424
524
|
if (!this.options.sharedConnection)
|
|
425
525
|
this.connection.close();
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
526
|
+
const senderClosing = this.sender.close();
|
|
527
|
+
try {
|
|
528
|
+
const results = await Promise.allSettled([this.counts?.close(), ...this.countClosings, this.readers.close(), senderClosing, this.reading.close(), this.messages.close(senderClosing)]);
|
|
529
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
530
|
+
if (failed)
|
|
531
|
+
throw failed.reason;
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
this.listeners.clear();
|
|
535
|
+
this.globalListeners.clear();
|
|
536
|
+
this.snapshots.clear();
|
|
537
|
+
this.countSnapshots.clear();
|
|
538
|
+
this.historyEdges.clear();
|
|
539
|
+
this.earlierReads.clear();
|
|
540
|
+
this.globalSnapshot = undefined;
|
|
541
|
+
}
|
|
434
542
|
})();
|
|
435
543
|
}
|
|
436
544
|
}
|
|
@@ -8,13 +8,30 @@ export type OutgoingMessage = {
|
|
|
8
8
|
message: ChatRuntimeMessage;
|
|
9
9
|
status: "sending" | "failed" | "sent";
|
|
10
10
|
confirmed?: boolean;
|
|
11
|
+
accessVersion?: number;
|
|
12
|
+
};
|
|
13
|
+
export type SenderScopeAccess = {
|
|
14
|
+
version: number;
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type DurableSenderOptions = {
|
|
18
|
+
indexedDB?: IDBFactory;
|
|
19
|
+
name?: string;
|
|
20
|
+
access?: () => Promise<SenderScopeAccess>;
|
|
11
21
|
};
|
|
12
22
|
/** Persists the exact wire message before sending; retries preserve its hash. */
|
|
13
23
|
export declare class DurableChatSender {
|
|
14
24
|
private client;
|
|
15
25
|
private onError;
|
|
26
|
+
private options;
|
|
16
27
|
private db;
|
|
17
28
|
private outbox;
|
|
29
|
+
private epochs;
|
|
30
|
+
private version;
|
|
31
|
+
private withdrawn;
|
|
32
|
+
private revoking?;
|
|
33
|
+
private channel?;
|
|
34
|
+
private accepted;
|
|
18
35
|
private scope;
|
|
19
36
|
private identity;
|
|
20
37
|
private values;
|
|
@@ -22,12 +39,19 @@ export declare class DurableChatSender {
|
|
|
22
39
|
private listeners;
|
|
23
40
|
private running;
|
|
24
41
|
private closed;
|
|
42
|
+
private closing;
|
|
43
|
+
private empty;
|
|
25
44
|
private offConnection;
|
|
26
45
|
private ready;
|
|
27
|
-
constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?: DurableSenderOptions);
|
|
47
|
+
private readAccess;
|
|
48
|
+
private restore;
|
|
49
|
+
private checkAccess;
|
|
50
|
+
private withEpoch;
|
|
51
|
+
private deleteRows;
|
|
52
|
+
/** Stop disclosure immediately; durable invalidation is coordinated separately. */
|
|
53
|
+
withdraw(): void;
|
|
54
|
+
revoke(targetVersion?: number): Promise<void>;
|
|
31
55
|
initialize(): Promise<void>;
|
|
32
56
|
subscribe(listener: () => void): () => void;
|
|
33
57
|
snapshot(threadId?: string): readonly OutgoingMessage[];
|