@pmate/chat-sdk 0.12.0-next.8 → 0.12.0-next.81
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 +170 -83
- package/lib/advanced.d.ts +10 -0
- package/lib/advanced.js +10 -0
- package/lib/chat.d.ts +5 -0
- package/lib/chat.js +11 -0
- package/lib/chatContract.d.ts +151 -0
- package/lib/chatContract.js +1 -0
- package/lib/chatError.d.ts +14 -0
- package/lib/chatError.js +16 -0
- package/lib/chatTypes.d.ts +127 -0
- package/lib/chatTypes.js +1 -0
- package/lib/commands.d.ts +20 -0
- package/lib/commands.js +12 -0
- package/lib/components/ChatInputDesktop.js +5 -4
- package/lib/components/ChatInputMobile.js +5 -4
- package/lib/components/CommandMenu.d.ts +15 -0
- package/lib/components/CommandMenu.js +10 -0
- package/lib/credentials.d.ts +26 -0
- package/lib/credentials.js +110 -0
- package/lib/fileMetadata.d.ts +9 -0
- package/lib/fileMetadata.js +18 -0
- package/lib/framework.d.ts +11 -0
- package/lib/framework.js +12 -0
- package/lib/history.d.ts +4 -0
- package/lib/history.js +8 -0
- package/lib/hooks/useChatSnapshot.d.ts +4 -0
- package/lib/hooks/useChatSnapshot.js +9 -0
- package/lib/hooks/useComposerSuggestions.d.ts +50 -0
- package/lib/hooks/useComposerSuggestions.js +70 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/internal/chatEngine.d.ts +130 -0
- package/lib/internal/chatEngine.js +284 -0
- package/lib/internal/chatRegistry.d.ts +4 -0
- package/lib/internal/chatRegistry.js +4 -0
- package/lib/internal/createPublicChat.d.ts +4 -0
- package/lib/internal/createPublicChat.js +52 -0
- package/lib/messageView.d.ts +6 -0
- package/lib/messageView.js +103 -0
- package/lib/runtime/batchedReadIndex.d.ts +16 -3
- package/lib/runtime/batchedReadIndex.js +50 -4
- package/lib/runtime/cachedMessage.d.ts +12 -0
- package/lib/runtime/cachedMessage.js +18 -0
- package/lib/runtime/chatClient.d.ts +7 -0
- package/lib/runtime/chatClient.js +14 -3
- package/lib/runtime/chatError.d.ts +5 -0
- package/lib/runtime/chatError.js +20 -0
- package/lib/runtime/chatSession.d.ts +128 -13
- package/lib/runtime/chatSession.js +759 -48
- package/lib/runtime/connectivity.d.ts +4 -0
- package/lib/runtime/connectivity.js +8 -0
- package/lib/runtime/conversationDirectory.d.ts +37 -0
- package/lib/runtime/conversationDirectory.js +116 -0
- package/lib/runtime/conversationRead.d.ts +13 -0
- package/lib/runtime/conversationRead.js +34 -0
- package/lib/runtime/conversationReading.d.ts +28 -0
- package/lib/runtime/conversationReading.js +47 -0
- package/lib/runtime/createChat.d.ts +11 -7
- package/lib/runtime/createChat.js +46 -23
- package/lib/runtime/durableSender.d.ts +97 -14
- package/lib/runtime/durableSender.js +786 -71
- package/lib/runtime/groupClient.d.ts +91 -0
- package/lib/runtime/groupClient.js +380 -0
- package/lib/runtime/groupDenial.d.ts +2 -0
- package/lib/runtime/groupDenial.js +4 -0
- package/lib/runtime/index.d.ts +7 -0
- package/lib/runtime/index.js +5 -0
- package/lib/runtime/indexedDbRecovery.d.ts +4 -0
- package/lib/runtime/indexedDbRecovery.js +27 -0
- package/lib/runtime/messageClient.d.ts +35 -8
- package/lib/runtime/messageClient.js +364 -66
- package/lib/runtime/messageControls.d.ts +19 -0
- package/lib/runtime/messageControls.js +44 -0
- package/lib/runtime/messageLaunch.d.ts +44 -0
- package/lib/runtime/messageLaunch.js +137 -0
- package/lib/runtime/messageQuotes.d.ts +3 -0
- package/lib/runtime/messageQuotes.js +17 -0
- package/lib/runtime/messageReaders.d.ts +5 -1
- package/lib/runtime/messageReaders.js +16 -5
- package/lib/runtime/messageStore.d.ts +43 -2
- package/lib/runtime/messageStore.js +248 -14
- package/lib/runtime/messageTimeline.d.ts +50 -0
- package/lib/runtime/messageTimeline.js +120 -0
- package/lib/runtime/outboxSchema.d.ts +5 -0
- package/lib/runtime/outboxSchema.js +59 -0
- package/lib/runtime/outgoingContent.d.ts +94 -0
- package/lib/runtime/outgoingContent.js +118 -0
- package/lib/runtime/readBatchJournal.d.ts +35 -0
- package/lib/runtime/readBatchJournal.js +210 -0
- package/lib/runtime/readCounts.d.ts +15 -1
- package/lib/runtime/readCounts.js +97 -9
- package/lib/runtime/readIndex.d.ts +68 -7
- package/lib/runtime/readIndex.js +460 -22
- package/lib/runtime/readSchema.d.ts +2 -0
- package/lib/runtime/readSchema.js +58 -0
- package/lib/runtime/readSync.d.ts +1 -0
- package/lib/runtime/readSync.js +26 -9
- package/lib/runtime/readingClient.d.ts +19 -0
- package/lib/runtime/readingClient.js +131 -19
- package/lib/runtime/readingContent.d.ts +5 -0
- package/lib/runtime/readingContent.js +23 -0
- package/lib/runtime/responseCache.d.ts +40 -5
- package/lib/runtime/responseCache.js +182 -44
- package/lib/runtime/socialReadClient.d.ts +9 -4
- package/lib/runtime/socialReadClient.js +12 -8
- package/lib/runtime/voiceUpload.d.ts +2 -0
- package/lib/runtime/voiceUpload.js +27 -0
- package/lib/snapshot.d.ts +36 -0
- package/lib/snapshot.js +52 -0
- package/lib/types/chatInput.d.ts +3 -0
- package/package.json +32 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOffline, isOfflineNetworkFailure } from "./connectivity.js";
|
|
1
2
|
import { WindowedTasks } from "./coalescer.js";
|
|
2
3
|
import { IndexedDbMessageStore, messageScopeKey, mergeStoredMessage } from "./messageStore.js";
|
|
3
4
|
/** Local-first message facade; hosts never select a cache or network source. */
|
|
@@ -5,9 +6,12 @@ export class MessageClient {
|
|
|
5
6
|
options;
|
|
6
7
|
storage;
|
|
7
8
|
watches = new Map();
|
|
9
|
+
windowEdges = new Map();
|
|
10
|
+
accessListeners = new Set();
|
|
8
11
|
allListeners = new Set();
|
|
9
12
|
reads = new Map();
|
|
10
13
|
syncing = new Map();
|
|
14
|
+
completedSyncs = new Map();
|
|
11
15
|
paging = new Map();
|
|
12
16
|
writes = new Map();
|
|
13
17
|
activeWrites = new Set();
|
|
@@ -23,6 +27,16 @@ export class MessageClient {
|
|
|
23
27
|
queueBytes;
|
|
24
28
|
pendingBytes = 0;
|
|
25
29
|
pendingRecords = 0;
|
|
30
|
+
legacyImports = 0;
|
|
31
|
+
revokingScope;
|
|
32
|
+
scopeReady;
|
|
33
|
+
scopeVersion = 0;
|
|
34
|
+
scopeAllowed = true;
|
|
35
|
+
scopeDenied = false;
|
|
36
|
+
enqueuing = new Set();
|
|
37
|
+
ensureScope() {
|
|
38
|
+
return this.scopeReady ??= this.storage.scopeAccess().then(access => { this.scopeVersion = access.version; this.scopeAllowed = !access.revoked && !this.scopeDenied; });
|
|
39
|
+
}
|
|
26
40
|
constructor(options) {
|
|
27
41
|
this.options = options;
|
|
28
42
|
this.storage = options.storage ?? new IndexedDbMessageStore(options.scope, options.merge ?? mergeStoredMessage);
|
|
@@ -37,29 +51,98 @@ export class MessageClient {
|
|
|
37
51
|
this.invalidate(thread);
|
|
38
52
|
else
|
|
39
53
|
options.onError?.(error, thread); });
|
|
40
|
-
this.syncTasks = new WindowedTasks(options.syncWindowMs ?? 1000, (error, thread) =>
|
|
54
|
+
this.syncTasks = new WindowedTasks(options.syncWindowMs ?? 1000, (error, thread) => { if (!isOfflineNetworkFailure(error))
|
|
55
|
+
options.onError?.(error, thread); });
|
|
41
56
|
if (typeof BroadcastChannel !== "undefined") {
|
|
42
57
|
this.channel = new BroadcastChannel(`pmate-chat:${messageScopeKey(options.scope)}${this.storage.namespace === "pmate-chat-messages" ? "" : `:${this.storage.namespace}`}`);
|
|
43
58
|
this.channel.onmessage = event => {
|
|
44
59
|
if (this.closed)
|
|
45
60
|
return;
|
|
46
|
-
if (event.data?.type === "revoked" &&
|
|
61
|
+
if (event.data?.type === "scope-revoked" && Number.isSafeInteger(event.data.version)) {
|
|
62
|
+
const version = event.data.version;
|
|
63
|
+
void this.ensureScope().then(async () => {
|
|
64
|
+
const current = await this.storage.scopeAccess();
|
|
65
|
+
if (!this.closed && (this.scopeVersion < version || current.revoked)) {
|
|
66
|
+
this.denyScope();
|
|
67
|
+
options.onError?.(accessRevoked(), "");
|
|
68
|
+
}
|
|
69
|
+
}).catch(error => options.onError?.(error, ""));
|
|
70
|
+
}
|
|
71
|
+
else if (event.data?.type === "revoked" && typeof event.data.thread === "string" && Number.isSafeInteger(event.data.version)) {
|
|
47
72
|
const denied = event.data;
|
|
48
73
|
void (async () => {
|
|
49
74
|
const current = await this.storage.read(denied.thread, 1).then(page => page.coverage).catch(() => undefined);
|
|
75
|
+
if ((current?.scopeVersion ?? 0) !== (denied.scopeVersion ?? 0))
|
|
76
|
+
return;
|
|
50
77
|
if (this.closed || (current && !current.revoked && (current.resetVersion ?? 0) >= denied.version))
|
|
51
78
|
return;
|
|
52
79
|
this.denyView(denied.thread, denied.version);
|
|
53
|
-
options.onError?.(
|
|
80
|
+
options.onError?.(accessRevoked(), denied.thread);
|
|
54
81
|
})();
|
|
55
82
|
}
|
|
83
|
+
else if (event.data?.type === "legacy" && typeof event.data.thread === "string" && this.watches.has(event.data.thread)) {
|
|
84
|
+
const thread = event.data.thread;
|
|
85
|
+
void this.storage.read(thread, 1).then(page => this.reload(thread, !page.coverage?.complete)).catch(error => { if (!this.closed)
|
|
86
|
+
options.onError?.(error, thread); });
|
|
87
|
+
}
|
|
56
88
|
else if (typeof event.data === "string" && this.watches.has(event.data))
|
|
57
89
|
void this.reload(event.data).catch(error => { if (!this.closed)
|
|
58
90
|
options.onError?.(error, event.data); });
|
|
59
91
|
};
|
|
60
92
|
}
|
|
61
93
|
}
|
|
94
|
+
/** Shared guard for sibling caches; it never authorizes a denied old instance. */
|
|
95
|
+
subscribeAccessRevoked(listener) { this.accessListeners.add(listener); return () => { this.accessListeners.delete(listener); }; }
|
|
96
|
+
async scopeAccess() {
|
|
97
|
+
await this.ensureScope();
|
|
98
|
+
if (this.closed)
|
|
99
|
+
return { version: this.scopeVersion, allowed: false };
|
|
100
|
+
const current = await this.storage.scopeAccess();
|
|
101
|
+
return { version: current.version, allowed: !this.scopeDenied && !current.revoked && current.version === this.scopeVersion };
|
|
102
|
+
}
|
|
103
|
+
authorizingScope;
|
|
104
|
+
/** Recovery belongs to the SDK; withdrawn instances never regain authority. */
|
|
105
|
+
authorizeScope() {
|
|
106
|
+
return this.authorizingScope ??= (async () => {
|
|
107
|
+
await this.ensureScope();
|
|
108
|
+
if (this.closed || this.closing || this.scopeDenied)
|
|
109
|
+
throw accessRevoked();
|
|
110
|
+
const access = await this.scopeAccess();
|
|
111
|
+
if (access.allowed)
|
|
112
|
+
return;
|
|
113
|
+
if (access.version !== this.scopeVersion) {
|
|
114
|
+
this.denyScope();
|
|
115
|
+
throw accessRevoked();
|
|
116
|
+
}
|
|
117
|
+
if (!this.options.transport.authorizeScope)
|
|
118
|
+
throw accessRevoked();
|
|
119
|
+
try {
|
|
120
|
+
await this.options.transport.authorizeScope(this.abort.signal);
|
|
121
|
+
if (this.closed || this.closing || this.scopeDenied)
|
|
122
|
+
throw accessRevoked();
|
|
123
|
+
await this.storage.authorizeScope(this.scopeVersion);
|
|
124
|
+
if (this.closed || this.closing || this.scopeDenied)
|
|
125
|
+
throw accessRevoked();
|
|
126
|
+
this.scopeAllowed = true;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if ([401, 403].includes(error?.status ?? 0) || String(error).includes("MESSAGE_SCOPE_CHANGED"))
|
|
130
|
+
this.denyScope();
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
})().finally(() => { this.authorizingScope = undefined; });
|
|
134
|
+
}
|
|
62
135
|
getSnapshot(thread) { return this.watches.get(thread)?.snapshot; }
|
|
136
|
+
/** Exact authoritative row only; migration-only rows never prove delivery. */
|
|
137
|
+
async cachedMessage(thread, id) {
|
|
138
|
+
await this.ensureScope();
|
|
139
|
+
if (this.closed || this.closing || this.scopeDenied || this.revoked.has(thread))
|
|
140
|
+
throw accessRevoked();
|
|
141
|
+
const page = await this.storage.readSelected(thread, [id]);
|
|
142
|
+
if (page.coverage?.revoked || (page.coverage?.scopeVersion ?? 0) !== this.scopeVersion)
|
|
143
|
+
throw accessRevoked();
|
|
144
|
+
return page.messages[0];
|
|
145
|
+
}
|
|
63
146
|
subscribe(thread, listener) {
|
|
64
147
|
let watch = this.watches.get(thread);
|
|
65
148
|
if (!watch) {
|
|
@@ -71,16 +154,18 @@ export class MessageClient {
|
|
|
71
154
|
}
|
|
72
155
|
subscribeAll(listener) { this.allListeners.add(listener); return () => { this.allListeners.delete(listener); }; }
|
|
73
156
|
/** Close the connection gap for saved windows with four bounded background lanes. */
|
|
74
|
-
async restore(since) {
|
|
75
|
-
|
|
157
|
+
async restore(since, threadId) {
|
|
158
|
+
if (this.closed || this.closing || this.scopeDenied || isOffline())
|
|
159
|
+
return;
|
|
160
|
+
const queue = (await this.storage.windows()).filter(window => (window.syncedAt ?? 0) < since && !window.revoked && (threadId === undefined || window.thread === threadId));
|
|
76
161
|
await Promise.all(Array.from({ length: Math.min(4, queue.length) }, async () => {
|
|
77
|
-
while (queue.length && !this.closed && !this.closing) {
|
|
162
|
+
while (queue.length && !this.closed && !this.closing && !this.scopeDenied && !isOffline()) {
|
|
78
163
|
const window = queue.shift();
|
|
79
164
|
try {
|
|
80
165
|
await this.synchronize(window.thread);
|
|
81
166
|
}
|
|
82
167
|
catch (error) {
|
|
83
|
-
if (!this.closed && !this.closing)
|
|
168
|
+
if (!this.closed && !this.closing && !isOfflineNetworkFailure(error))
|
|
84
169
|
this.options.onError?.(error, window.thread);
|
|
85
170
|
}
|
|
86
171
|
}
|
|
@@ -95,11 +180,22 @@ export class MessageClient {
|
|
|
95
180
|
if (running)
|
|
96
181
|
return running;
|
|
97
182
|
const task = (async () => {
|
|
183
|
+
const completedAtStart = this.completedSyncs.get(thread);
|
|
184
|
+
await this.ensureScope();
|
|
185
|
+
if (this.scopeDenied)
|
|
186
|
+
throw accessRevoked();
|
|
98
187
|
const local = await this.storage.read(thread, limit);
|
|
188
|
+
if ((local.coverage?.scopeVersion ?? 0) !== this.scopeVersion) {
|
|
189
|
+
this.denyScope();
|
|
190
|
+
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
191
|
+
}
|
|
99
192
|
if (refresh || !local.coverage?.complete || local.coverage.revoked || this.revoked.has(thread)) {
|
|
100
193
|
if (typeof navigator !== "undefined" && navigator.onLine === false) {
|
|
101
|
-
if (!refresh &&
|
|
102
|
-
|
|
194
|
+
if (!refresh && !local.coverage?.revoked) {
|
|
195
|
+
const fallback = await this.fallback(thread, limit);
|
|
196
|
+
if (fallback.messages.length)
|
|
197
|
+
return fallback;
|
|
198
|
+
}
|
|
103
199
|
throw new Error("MESSAGE_OFFLINE_UNAVAILABLE");
|
|
104
200
|
}
|
|
105
201
|
this.revoked.delete(thread);
|
|
@@ -107,10 +203,18 @@ export class MessageClient {
|
|
|
107
203
|
await this.synchronize(thread);
|
|
108
204
|
}
|
|
109
205
|
catch (error) {
|
|
110
|
-
if (
|
|
206
|
+
if (String(error).includes("MESSAGE_SCOPE_CHANGED")) {
|
|
207
|
+
this.denyScope();
|
|
111
208
|
throw error;
|
|
112
|
-
|
|
113
|
-
|
|
209
|
+
}
|
|
210
|
+
if (refresh || [401, 403].includes(error?.status ?? 0))
|
|
211
|
+
throw error;
|
|
212
|
+
const fallback = await this.fallback(thread, limit);
|
|
213
|
+
if (!fallback.messages.length)
|
|
214
|
+
throw error;
|
|
215
|
+
if (!isOfflineNetworkFailure(error))
|
|
216
|
+
this.options.onError?.(error, thread);
|
|
217
|
+
return fallback;
|
|
114
218
|
}
|
|
115
219
|
}
|
|
116
220
|
if (this.closed)
|
|
@@ -120,17 +224,24 @@ export class MessageClient {
|
|
|
120
224
|
result = await this.fillWindow(thread, limit);
|
|
121
225
|
}
|
|
122
226
|
catch (error) {
|
|
227
|
+
if (String(error).includes("MESSAGE_SCOPE_CHANGED")) {
|
|
228
|
+
this.denyScope();
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
123
231
|
if ([401, 403].includes(error?.status ?? 0))
|
|
124
232
|
throw error;
|
|
125
|
-
const cached = await this.
|
|
233
|
+
const cached = await this.fallback(thread, limit);
|
|
126
234
|
if (!cached.messages.length)
|
|
127
235
|
throw error;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return
|
|
236
|
+
if (!isOfflineNetworkFailure(error))
|
|
237
|
+
this.options.onError?.(error, thread);
|
|
238
|
+
return cached;
|
|
131
239
|
}
|
|
132
240
|
const snapshot = await this.reload(thread);
|
|
241
|
+
if (this.scopeDenied || this.revoked.has(thread))
|
|
242
|
+
throw accessRevoked();
|
|
133
243
|
if (local.coverage?.complete && Date.now() - (local.coverage.syncedAt ?? 0) > (this.options.freshnessMs ?? 30_000)
|
|
244
|
+
&& !this.syncing.has(thread) && this.completedSyncs.get(thread) === completedAtStart
|
|
134
245
|
&& (typeof navigator === "undefined" || navigator.onLine !== false))
|
|
135
246
|
this.invalidate(thread);
|
|
136
247
|
return limit === 100 ? snapshot : result;
|
|
@@ -141,15 +252,86 @@ export class MessageClient {
|
|
|
141
252
|
}
|
|
142
253
|
/** Application connection/wake events invalidate, rather than fetching directly. */
|
|
143
254
|
invalidate(thread) {
|
|
144
|
-
if (!this.closed && !this.closing && !this.revoked.has(thread))
|
|
145
|
-
this.syncTasks.schedule(thread, () =>
|
|
255
|
+
if (!this.closed && !this.closing && !this.scopeDenied && !isOffline() && !this.revoked.has(thread))
|
|
256
|
+
this.syncTasks.schedule(thread, async () => {
|
|
257
|
+
if (!isOffline())
|
|
258
|
+
await this.synchronize(thread);
|
|
259
|
+
});
|
|
146
260
|
}
|
|
147
|
-
async
|
|
261
|
+
async fallback(thread, limit, before) {
|
|
262
|
+
await this.ensureScope();
|
|
263
|
+
if (this.scopeDenied || this.revoked.has(thread))
|
|
264
|
+
throw accessRevoked();
|
|
265
|
+
const page = await this.storage.readFallback(thread, limit, before);
|
|
266
|
+
if ((page.coverage?.scopeVersion ?? 0) !== this.scopeVersion) {
|
|
267
|
+
this.denyScope();
|
|
268
|
+
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
269
|
+
}
|
|
270
|
+
if (!before && page.messages.length)
|
|
271
|
+
await this.reload(thread, true);
|
|
272
|
+
return { messages: page.messages, hasEarlier: true, complete: false, resetVersion: page.coverage?.resetVersion ?? 0 };
|
|
273
|
+
}
|
|
274
|
+
/** Migration batches remain separate from authoritative message coverage. */
|
|
275
|
+
importLegacy(thread, messages) {
|
|
276
|
+
if (this.closed || this.closing)
|
|
277
|
+
return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
|
|
278
|
+
if (messages.length > 128)
|
|
279
|
+
return Promise.reject(new Error("MESSAGE_BATCH_TOO_LARGE"));
|
|
280
|
+
if (this.legacyImports >= 4)
|
|
281
|
+
return Promise.reject(new Error("MESSAGE_STORAGE_BACKPRESSURE"));
|
|
282
|
+
let records;
|
|
283
|
+
try {
|
|
284
|
+
let bytes = 0;
|
|
285
|
+
for (const message of messages) {
|
|
286
|
+
bytes += this.messageBytes(message);
|
|
287
|
+
if (bytes > Math.min(this.transactionBytes, 1_048_576))
|
|
288
|
+
throw new Error("MESSAGE_BATCH_TOO_LARGE");
|
|
289
|
+
}
|
|
290
|
+
records = structuredClone(messages);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return Promise.reject(error);
|
|
294
|
+
}
|
|
295
|
+
this.legacyImports++;
|
|
296
|
+
const task = this.storage.importLegacy(thread, records).then(async () => {
|
|
297
|
+
const page = await this.storage.read(thread, 1);
|
|
298
|
+
await this.reload(thread, !page.coverage?.complete);
|
|
299
|
+
this.channel?.postMessage({ type: "legacy", thread });
|
|
300
|
+
}).finally(() => { this.legacyImports--; this.activeWrites.delete(task); });
|
|
301
|
+
this.activeWrites.add(task);
|
|
302
|
+
return task;
|
|
303
|
+
}
|
|
304
|
+
async reload(thread, fallback = false) {
|
|
305
|
+
await this.ensureScope();
|
|
148
306
|
const limit = 100;
|
|
149
|
-
|
|
307
|
+
let page = await (fallback ? this.storage.readFallback(thread, limit) : this.storage.read(thread, limit));
|
|
308
|
+
const edge = this.windowEdges.get(thread);
|
|
309
|
+
if (edge && edge.version !== (page.coverage?.resetVersion ?? 0))
|
|
310
|
+
this.windowEdges.delete(thread);
|
|
311
|
+
if (edge && edge.version === (page.coverage?.resetVersion ?? 0)) {
|
|
312
|
+
const pages = [page.messages];
|
|
313
|
+
const beforeEdge = (message) => message.timestamp < edge.timestamp || message.timestamp === edge.timestamp && message.id < edge.id;
|
|
314
|
+
let oldest = page.messages[0];
|
|
315
|
+
while (oldest && !beforeEdge(oldest) && (oldest.timestamp !== edge.timestamp || oldest.id !== edge.id) && page.hasEarlier) {
|
|
316
|
+
const next = await (fallback ? this.storage.readFallback(thread, limit, { timestamp: oldest.timestamp, id: oldest.id }) : this.storage.read(thread, limit, { timestamp: oldest.timestamp, id: oldest.id }));
|
|
317
|
+
if ((next.coverage?.resetVersion ?? 0) !== edge.version || (next.coverage?.scopeVersion ?? 0) !== (page.coverage?.scopeVersion ?? 0)) {
|
|
318
|
+
this.windowEdges.delete(thread);
|
|
319
|
+
return this.reload(thread, fallback);
|
|
320
|
+
}
|
|
321
|
+
pages.push(next.messages);
|
|
322
|
+
page = next;
|
|
323
|
+
oldest = next.messages[0];
|
|
324
|
+
}
|
|
325
|
+
const messages = pages.reverse().flat();
|
|
326
|
+
page = { ...page, messages: messages.filter(message => !beforeEdge(message)), hasEarlier: page.hasEarlier || messages.some(beforeEdge) };
|
|
327
|
+
}
|
|
150
328
|
if (this.closed)
|
|
151
329
|
throw new Error("MESSAGE_CLIENT_CLOSED");
|
|
152
|
-
if (this.
|
|
330
|
+
if ((page.coverage?.scopeVersion ?? 0) !== this.scopeVersion) {
|
|
331
|
+
this.denyScope();
|
|
332
|
+
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
333
|
+
}
|
|
334
|
+
if (this.scopeDenied || this.revoked.has(thread)) {
|
|
153
335
|
page.messages = [];
|
|
154
336
|
page.hasEarlier = false;
|
|
155
337
|
}
|
|
@@ -160,7 +342,7 @@ export class MessageClient {
|
|
|
160
342
|
return old && JSON.stringify(old) === JSON.stringify(message) ? old : message;
|
|
161
343
|
});
|
|
162
344
|
const old = watch?.snapshot;
|
|
163
|
-
const resetVersion = Math.max(page.coverage?.resetVersion ?? 0, this.revoked.has(thread) ? watch?.snapshot.resetVersion ?? 0 : 0), complete = !this.revoked.has(thread) && (page.coverage?.complete ?? false);
|
|
345
|
+
const resetVersion = Math.max(page.coverage?.resetVersion ?? 0, this.revoked.has(thread) ? watch?.snapshot.resetVersion ?? 0 : 0), complete = !fallback && !this.scopeDenied && !this.revoked.has(thread) && (page.coverage?.complete ?? false);
|
|
164
346
|
const hasEarlier = !this.revoked.has(thread) && (page.hasEarlier || Boolean(page.coverage?.nextBefore));
|
|
165
347
|
if (old && old.resetVersion === resetVersion && old.complete === complete && old.hasEarlier === hasEarlier && old.messages.length === messages.length && old.messages.every((message, index) => message === messages[index]))
|
|
166
348
|
return old;
|
|
@@ -179,8 +361,7 @@ export class MessageClient {
|
|
|
179
361
|
if (this.closed || this.closing)
|
|
180
362
|
return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
|
|
181
363
|
if (this.revoked.has(thread))
|
|
182
|
-
return Promise.reject(
|
|
183
|
-
const queue = this.writes.get(thread) ?? [];
|
|
364
|
+
return Promise.reject(accessRevoked());
|
|
184
365
|
if (messages.length > 1000 || this.pendingRecords + messages.length > 4096)
|
|
185
366
|
return Promise.reject(new Error("MESSAGE_STORAGE_BACKPRESSURE"));
|
|
186
367
|
let bytes = 0;
|
|
@@ -197,15 +378,25 @@ export class MessageClient {
|
|
|
197
378
|
}
|
|
198
379
|
this.pendingBytes += bytes;
|
|
199
380
|
this.pendingRecords += messages.length;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
381
|
+
let resolve, reject;
|
|
382
|
+
const committed = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
383
|
+
const enqueue = this.ensureScope().then(() => {
|
|
384
|
+
if (this.closed || this.scopeDenied)
|
|
385
|
+
throw accessRevoked();
|
|
386
|
+
if (!this.scopeAllowed)
|
|
387
|
+
throw new Error("MESSAGE_SCOPE_NOT_READY");
|
|
388
|
+
const queue = this.writes.get(thread) ?? [];
|
|
389
|
+
queue.push({ messages, bytes, version: this.watches.get(thread)?.snapshot.resetVersion ?? 0, scopeVersion: this.scopeVersion, resolve, reject });
|
|
390
|
+
this.writes.set(thread, queue);
|
|
391
|
+
this.writeTasks.schedule(thread, () => this.flushWrites(thread));
|
|
392
|
+
}).catch(error => { this.pendingBytes -= bytes; this.pendingRecords -= messages.length; reject(error); }).finally(() => this.enqueuing.delete(enqueue));
|
|
393
|
+
this.enqueuing.add(enqueue);
|
|
394
|
+
return committed;
|
|
204
395
|
}
|
|
205
|
-
async persist(thread, messages, checkpoint, complete, nextBefore, syncedAt, expectedVersion) {
|
|
396
|
+
async persist(thread, messages, checkpoint, complete, nextBefore, syncedAt, expectedVersion, expectedScopeVersion) {
|
|
206
397
|
const initial = (await this.storage.read(thread, 1)).coverage;
|
|
207
398
|
const version = expectedVersion ?? initial?.resetVersion ?? 0;
|
|
208
|
-
const guard = { version, authoritative: complete === true };
|
|
399
|
+
const guard = { version, scopeVersion: expectedScopeVersion ?? this.scopeVersion, authoritative: complete === true };
|
|
209
400
|
let changed = 0;
|
|
210
401
|
let navigationChanged = false;
|
|
211
402
|
// Coverage advances only after all bounded batches have durably committed.
|
|
@@ -213,7 +404,7 @@ export class MessageClient {
|
|
|
213
404
|
const commit = async () => {
|
|
214
405
|
if (!batch.length)
|
|
215
406
|
return;
|
|
216
|
-
if (this.closed || this.revoked.has(thread))
|
|
407
|
+
if (this.closed || this.scopeDenied || this.revoked.has(thread))
|
|
217
408
|
throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
|
|
218
409
|
changed += await this.storage.commit(thread, batch, undefined, guard);
|
|
219
410
|
batch = [];
|
|
@@ -229,11 +420,13 @@ export class MessageClient {
|
|
|
229
420
|
await commit();
|
|
230
421
|
if (complete !== undefined || checkpoint !== undefined) {
|
|
231
422
|
const old = (await this.storage.read(thread, 1)).coverage;
|
|
232
|
-
if (this.closed || this.revoked.has(thread))
|
|
423
|
+
if (this.closed || this.scopeDenied || this.revoked.has(thread))
|
|
233
424
|
throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
|
|
234
425
|
navigationChanged = (nextBefore !== undefined && nextBefore !== old?.nextBefore) || (complete !== undefined && complete !== old?.complete);
|
|
235
426
|
await this.storage.commit(thread, [], { resetVersion: version, revoked: complete === true ? false : old?.revoked, syncedAt: syncedAt ?? old?.syncedAt, complete: complete ?? old?.complete ?? false, checkpoint: checkpoint ?? old?.checkpoint, nextBefore: nextBefore === undefined ? old?.nextBefore : nextBefore }, guard);
|
|
236
427
|
}
|
|
428
|
+
if (complete === true)
|
|
429
|
+
this.scopeAllowed = true;
|
|
237
430
|
if (changed || navigationChanged) {
|
|
238
431
|
await this.reload(thread);
|
|
239
432
|
this.channel?.postMessage(thread);
|
|
@@ -264,14 +457,14 @@ export class MessageClient {
|
|
|
264
457
|
try {
|
|
265
458
|
const groups = new Map();
|
|
266
459
|
for (const write of queue) {
|
|
267
|
-
const group = groups.get(
|
|
460
|
+
const key = `${write.version}:${write.scopeVersion}`, group = groups.get(key) ?? [];
|
|
268
461
|
group.push(write);
|
|
269
|
-
groups.set(
|
|
462
|
+
groups.set(key, group);
|
|
270
463
|
}
|
|
271
464
|
let failed = false, failure;
|
|
272
|
-
for (const
|
|
465
|
+
for (const writes of groups.values()) {
|
|
273
466
|
try {
|
|
274
|
-
await this.persist(thread, writes.flatMap(write => write.messages), undefined, undefined, undefined, undefined, version);
|
|
467
|
+
await this.persist(thread, writes.flatMap(write => write.messages), undefined, undefined, undefined, undefined, writes[0].version, writes[0].scopeVersion);
|
|
275
468
|
writes.forEach(write => write.resolve());
|
|
276
469
|
}
|
|
277
470
|
catch (error) {
|
|
@@ -287,40 +480,58 @@ export class MessageClient {
|
|
|
287
480
|
this.release(queue);
|
|
288
481
|
}
|
|
289
482
|
}
|
|
483
|
+
assertNetworkAvailable(thread) {
|
|
484
|
+
if (this.closed || this.closing || this.scopeDenied || this.revoked.has(thread))
|
|
485
|
+
throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
|
|
486
|
+
if (isOffline())
|
|
487
|
+
throw new TypeError("MESSAGE_OFFLINE_UNAVAILABLE");
|
|
488
|
+
}
|
|
290
489
|
synchronize(thread) {
|
|
291
|
-
if (this.closed || this.closing || this.revoked.has(thread))
|
|
490
|
+
if (this.closed || this.closing || this.scopeDenied || this.revoked.has(thread))
|
|
292
491
|
return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
|
|
293
492
|
const running = this.syncing.get(thread);
|
|
294
493
|
if (running)
|
|
295
494
|
return running;
|
|
296
495
|
const task = (async () => {
|
|
496
|
+
await this.ensureScope();
|
|
297
497
|
const coverage = (await this.storage.read(thread, 1)).coverage;
|
|
498
|
+
const scopeVersion = coverage?.scopeVersion ?? 0;
|
|
499
|
+
if (scopeVersion !== this.scopeVersion) {
|
|
500
|
+
this.denyScope();
|
|
501
|
+
throw new Error("MESSAGE_SCOPE_CHANGED");
|
|
502
|
+
}
|
|
298
503
|
let version = coverage?.resetVersion ?? 0;
|
|
299
504
|
let after = coverage?.complete ? coverage.checkpoint : undefined;
|
|
505
|
+
this.assertNetworkAvailable(thread);
|
|
300
506
|
let page = after && this.options.transport.changes
|
|
301
507
|
? await this.options.transport.changes(thread, after, this.abort.signal)
|
|
302
508
|
: await this.options.transport.load(thread, this.abort.signal);
|
|
303
509
|
if (page.resetRequired) {
|
|
304
|
-
if (this.closed || this.revoked.has(thread))
|
|
510
|
+
if (this.closed || this.scopeDenied || this.revoked.has(thread))
|
|
305
511
|
throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
|
|
306
|
-
version = await this.storage.resetThread(thread);
|
|
512
|
+
version = await this.storage.resetThread(thread, false, scopeVersion);
|
|
307
513
|
after = undefined;
|
|
514
|
+
this.assertNetworkAvailable(thread);
|
|
308
515
|
page = await this.options.transport.load(thread, this.abort.signal);
|
|
309
516
|
}
|
|
310
517
|
for (;;) {
|
|
311
|
-
if (this.closed || this.revoked.has(thread))
|
|
312
|
-
throw
|
|
518
|
+
if (this.closed || this.scopeDenied || this.revoked.has(thread))
|
|
519
|
+
throw accessRevoked();
|
|
313
520
|
if (page.resetRequired)
|
|
314
521
|
throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
|
|
315
|
-
await this.persist(thread, page.messages, page.checkpoint, true, after ? undefined : page.nextBefore, Date.now(), version);
|
|
522
|
+
await this.persist(thread, page.messages, page.checkpoint, true, after ? undefined : page.nextBefore, Date.now(), version, scopeVersion);
|
|
316
523
|
if (!page.hasMoreChanges)
|
|
317
524
|
break;
|
|
318
525
|
if (!page.checkpoint || page.checkpoint === after || !this.options.transport.changes)
|
|
319
526
|
throw new Error("MESSAGE_SYNC_NO_PROGRESS");
|
|
320
527
|
after = page.checkpoint;
|
|
528
|
+
this.assertNetworkAvailable(thread);
|
|
321
529
|
page = await this.options.transport.changes(thread, after, this.abort.signal);
|
|
322
530
|
}
|
|
531
|
+
this.completedSyncs.set(thread, (this.completedSyncs.get(thread) ?? 0) + 1);
|
|
323
532
|
})().catch(async (error) => {
|
|
533
|
+
if (String(error).includes("MESSAGE_SCOPE_CHANGED"))
|
|
534
|
+
this.denyScope();
|
|
324
535
|
if (!this.closed && !this.closing && error?.status === 403) {
|
|
325
536
|
try {
|
|
326
537
|
await this.revoke(thread);
|
|
@@ -335,14 +546,51 @@ export class MessageClient {
|
|
|
335
546
|
this.syncing.set(thread, task);
|
|
336
547
|
return task;
|
|
337
548
|
}
|
|
338
|
-
|
|
549
|
+
/** Keep an explicitly requested history range in the shared subscribed view. */
|
|
550
|
+
async expandWindow(thread, page) {
|
|
551
|
+
const first = page.messages[0];
|
|
552
|
+
if (!first || !page.complete)
|
|
553
|
+
return;
|
|
554
|
+
const old = this.windowEdges.get(thread);
|
|
555
|
+
if (!old || old.version !== page.resetVersion || first.timestamp < old.timestamp || first.timestamp === old.timestamp && first.id < old.id)
|
|
556
|
+
this.windowEdges.set(thread, { timestamp: first.timestamp, id: first.id, version: page.resetVersion });
|
|
557
|
+
const snapshot = await this.reload(thread);
|
|
558
|
+
if (snapshot.resetVersion !== page.resetVersion)
|
|
559
|
+
throw new Error("MESSAGE_PROJECTION_CHANGED");
|
|
560
|
+
}
|
|
561
|
+
async earlier(thread, before, limit = 100, expand = false) {
|
|
562
|
+
await this.ensureScope();
|
|
563
|
+
if (this.scopeDenied)
|
|
564
|
+
throw accessRevoked();
|
|
339
565
|
if (this.closed)
|
|
340
566
|
throw new Error("MESSAGE_CLIENT_CLOSED");
|
|
341
567
|
if (this.revoked.has(thread))
|
|
342
|
-
throw
|
|
343
|
-
|
|
344
|
-
await this.
|
|
345
|
-
|
|
568
|
+
throw accessRevoked();
|
|
569
|
+
try {
|
|
570
|
+
if (!(await this.storage.read(thread, 1)).coverage?.complete) {
|
|
571
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false)
|
|
572
|
+
throw new Error("MESSAGE_OFFLINE_UNAVAILABLE");
|
|
573
|
+
await this.synchronize(thread);
|
|
574
|
+
}
|
|
575
|
+
const page = await this.fillWindow(thread, limit, before);
|
|
576
|
+
if (expand)
|
|
577
|
+
await this.expandWindow(thread, page);
|
|
578
|
+
return page;
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
if (String(error).includes("MESSAGE_SCOPE_CHANGED")) {
|
|
582
|
+
this.denyScope();
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
if ([401, 403].includes(error?.status ?? 0))
|
|
586
|
+
throw error;
|
|
587
|
+
const fallback = await this.fallback(thread, limit, before);
|
|
588
|
+
if (!fallback.messages.length)
|
|
589
|
+
throw error;
|
|
590
|
+
if (!isOfflineNetworkFailure(error))
|
|
591
|
+
this.options.onError?.(error, thread);
|
|
592
|
+
return fallback;
|
|
593
|
+
}
|
|
346
594
|
}
|
|
347
595
|
async fillWindow(thread, limit, before) {
|
|
348
596
|
let page = await this.storage.read(thread, limit, before);
|
|
@@ -354,21 +602,22 @@ export class MessageClient {
|
|
|
354
602
|
page = await this.storage.read(thread, limit, before);
|
|
355
603
|
continue;
|
|
356
604
|
}
|
|
357
|
-
const token = page.coverage.nextBefore, version = page.coverage.resetVersion ?? 0;
|
|
605
|
+
const token = page.coverage.nextBefore, version = page.coverage.resetVersion ?? 0, scopeVersion = page.coverage.scopeVersion ?? 0;
|
|
358
606
|
const task = (async () => {
|
|
607
|
+
this.assertNetworkAvailable(thread);
|
|
359
608
|
const remote = await this.options.transport.older(thread, token, this.abort.signal);
|
|
360
|
-
if (this.closed || this.revoked.has(thread))
|
|
609
|
+
if (this.closed || this.scopeDenied || this.revoked.has(thread))
|
|
361
610
|
throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
|
|
362
611
|
if (remote.resetRequired) {
|
|
363
612
|
if (++resets > 2)
|
|
364
613
|
throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
|
|
365
|
-
await this.storage.resetThread(thread);
|
|
614
|
+
await this.storage.resetThread(thread, false, scopeVersion);
|
|
366
615
|
await this.synchronize(thread);
|
|
367
616
|
return;
|
|
368
617
|
}
|
|
369
618
|
if (remote.nextBefore === token)
|
|
370
619
|
throw new Error("MESSAGE_SYNC_NO_PROGRESS");
|
|
371
|
-
await this.persist(thread, remote.messages, undefined, true, remote.nextBefore ?? null, undefined, version);
|
|
620
|
+
await this.persist(thread, remote.messages, undefined, true, remote.nextBefore ?? null, undefined, version, scopeVersion);
|
|
372
621
|
})().catch(async (error) => {
|
|
373
622
|
if (!this.closed && !this.closing && error?.status === 403) {
|
|
374
623
|
try {
|
|
@@ -385,9 +634,12 @@ export class MessageClient {
|
|
|
385
634
|
await task;
|
|
386
635
|
page = await this.storage.read(thread, limit, before);
|
|
387
636
|
}
|
|
637
|
+
if (this.scopeDenied || this.revoked.has(thread))
|
|
638
|
+
throw accessRevoked();
|
|
388
639
|
return { messages: page.messages, hasEarlier: page.hasEarlier || Boolean(page.coverage?.nextBefore), resetVersion: page.coverage?.resetVersion ?? 0, complete: page.coverage?.complete ?? false };
|
|
389
640
|
}
|
|
390
641
|
denyView(thread, version) {
|
|
642
|
+
this.windowEdges.delete(thread);
|
|
391
643
|
this.revoked.add(thread);
|
|
392
644
|
const watch = this.watches.get(thread);
|
|
393
645
|
const snapshot = { messages: [], hasEarlier: false, complete: false, resetVersion: Math.max(watch?.snapshot.resetVersion ?? 0, version ?? (watch?.snapshot.resetVersion ?? 0) + 1) };
|
|
@@ -401,34 +653,80 @@ export class MessageClient {
|
|
|
401
653
|
this.watches.set(thread, { snapshot, listeners: new Set() });
|
|
402
654
|
this.allListeners.forEach(listener => listener(thread));
|
|
403
655
|
}
|
|
656
|
+
denyScope() {
|
|
657
|
+
const notify = !this.scopeDenied;
|
|
658
|
+
this.scopeDenied = true;
|
|
659
|
+
this.scopeAllowed = false;
|
|
660
|
+
this.abort.abort();
|
|
661
|
+
this.syncTasks.close();
|
|
662
|
+
this.writeTasks.close();
|
|
663
|
+
for (const thread of this.watches.keys())
|
|
664
|
+
this.denyView(thread);
|
|
665
|
+
for (const queue of this.writes.values()) {
|
|
666
|
+
queue.forEach(write => write.reject(accessRevoked()));
|
|
667
|
+
this.release(queue);
|
|
668
|
+
}
|
|
669
|
+
this.writes.clear();
|
|
670
|
+
if (notify)
|
|
671
|
+
this.accessListeners.forEach(listener => listener());
|
|
672
|
+
}
|
|
673
|
+
revokeScope() {
|
|
674
|
+
if (this.closed || this.closing)
|
|
675
|
+
return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
|
|
676
|
+
return this.revokingScope ??= (async () => {
|
|
677
|
+
this.denyScope();
|
|
678
|
+
try {
|
|
679
|
+
await this.ensureScope();
|
|
680
|
+
return await this.storage.revokeScope(this.scopeVersion);
|
|
681
|
+
}
|
|
682
|
+
finally {
|
|
683
|
+
const access = await this.storage.scopeAccess();
|
|
684
|
+
this.channel?.postMessage({ type: "scope-revoked", version: access.version });
|
|
685
|
+
}
|
|
686
|
+
})();
|
|
687
|
+
}
|
|
404
688
|
async revoke(thread) {
|
|
405
689
|
this.denyView(thread);
|
|
406
690
|
const queue = this.writes.get(thread);
|
|
407
691
|
this.writes.delete(thread);
|
|
408
692
|
if (queue) {
|
|
409
|
-
queue.forEach(write => write.reject(
|
|
693
|
+
queue.forEach(write => write.reject(accessRevoked()));
|
|
410
694
|
this.release(queue);
|
|
411
695
|
}
|
|
412
696
|
try {
|
|
413
|
-
await this.storage.resetThread(thread, true);
|
|
697
|
+
await this.storage.resetThread(thread, true, this.scopeVersion);
|
|
414
698
|
await this.reload(thread);
|
|
415
699
|
}
|
|
416
700
|
finally {
|
|
417
|
-
this.channel?.postMessage({ type: "revoked", thread, version: this.watches.get(thread)?.snapshot.resetVersion });
|
|
701
|
+
this.channel?.postMessage({ type: "revoked", thread, version: this.watches.get(thread)?.snapshot.resetVersion, scopeVersion: this.scopeVersion });
|
|
418
702
|
}
|
|
419
703
|
}
|
|
420
|
-
async close() {
|
|
421
|
-
|
|
704
|
+
async close(beforeDispose) {
|
|
705
|
+
if (this.closed)
|
|
706
|
+
return;
|
|
422
707
|
this.closing = true;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
708
|
+
try {
|
|
709
|
+
await Promise.all([...this.enqueuing]);
|
|
710
|
+
this.syncTasks.close();
|
|
711
|
+
this.writeTasks.close();
|
|
712
|
+
const flushes = [...this.writes.keys()].map(thread => this.flushWrites(thread));
|
|
713
|
+
const results = await Promise.allSettled([...flushes, ...this.activeWrites, ...(this.revokingScope ? [this.revokingScope] : [])]);
|
|
714
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
715
|
+
if (failed)
|
|
716
|
+
throw failed.reason;
|
|
717
|
+
await beforeDispose;
|
|
718
|
+
}
|
|
719
|
+
finally {
|
|
720
|
+
this.closed = true;
|
|
721
|
+
this.abort.abort();
|
|
722
|
+
this.channel?.close();
|
|
723
|
+
this.watches.clear();
|
|
724
|
+
this.windowEdges.clear();
|
|
725
|
+
this.completedSyncs.clear();
|
|
726
|
+
this.allListeners.clear();
|
|
727
|
+
this.accessListeners.clear();
|
|
728
|
+
this.storage.close();
|
|
729
|
+
}
|
|
433
730
|
}
|
|
434
731
|
}
|
|
732
|
+
function accessRevoked() { return Object.assign(new Error("MESSAGE_ACCESS_REVOKED"), { status: 403 }); }
|