@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
package/lib/runtime/readIndex.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
import { prepareReadSchema } from "./readSchema.js";
|
|
2
|
+
import { ReadBatchJournal, lockReadOwner } from "./readBatchJournal.js";
|
|
1
3
|
import { Dexie } from "dexie";
|
|
4
|
+
import { isReadingControl } from "./readingContent.js";
|
|
2
5
|
import { isValidChatTeamId } from "./thread.js";
|
|
6
|
+
/** Retain read semantics without a second durable copy of message bodies. */
|
|
7
|
+
export function readingMetadata(state) {
|
|
8
|
+
let changed = false;
|
|
9
|
+
const threads = state.threads.map(thread => {
|
|
10
|
+
let different = false;
|
|
11
|
+
const messages = thread.messages.map(message => {
|
|
12
|
+
if (message.payload === null && typeof message.readingControl === "boolean")
|
|
13
|
+
return message;
|
|
14
|
+
different = true;
|
|
15
|
+
return { ...message, payload: null, readingControl: Boolean(message.readingControl) || isReadingControl(message.payload, state.appId) };
|
|
16
|
+
});
|
|
17
|
+
if (!different)
|
|
18
|
+
return thread;
|
|
19
|
+
changed = true;
|
|
20
|
+
return { ...thread, messages };
|
|
21
|
+
});
|
|
22
|
+
return changed ? { ...state, threads } : state;
|
|
23
|
+
}
|
|
3
24
|
export class ReadSyncGap extends Error {
|
|
4
25
|
cursor;
|
|
5
26
|
constructor(cursor) {
|
|
@@ -86,12 +107,12 @@ export function recordReadIntent(current, intent) {
|
|
|
86
107
|
throw new Error("INVALID_READ_INTENT");
|
|
87
108
|
const observed = current.observedReadIntents ?? [];
|
|
88
109
|
if (observed.some(item => item.threadId === intent.threadId && item.messageId === intent.messageId && item.readPolicy === intent.readPolicy))
|
|
89
|
-
return current;
|
|
110
|
+
return resolveObservedReads(current);
|
|
90
111
|
if (observed.some(item => item.operationId === intent.operationId) || current.pending.some(item => item.operationId === intent.operationId))
|
|
91
112
|
throw new Error("INVALID_READ_OPERATION_ID");
|
|
92
113
|
return resolveObservedReads({ ...current, observedReadIntents: [...observed, structuredClone(intent)] });
|
|
93
114
|
}
|
|
94
|
-
function resolveObservedReads(current) {
|
|
115
|
+
export function resolveObservedReads(current) {
|
|
95
116
|
let next = current;
|
|
96
117
|
const remaining = [];
|
|
97
118
|
for (const intent of current.observedReadIntents ?? []) {
|
|
@@ -108,6 +129,9 @@ function resolveObservedReads(current) {
|
|
|
108
129
|
next = intent.readPolicy === "explicit" ? recordExplicitRead(next, intent.threadId, intent.messageId, intent.operationId)
|
|
109
130
|
: recordVisibleRead(next, intent.threadId, intent.messageId, intent.operationId);
|
|
110
131
|
}
|
|
132
|
+
const previous = current.observedReadIntents;
|
|
133
|
+
if (next === current && previous && previous.length === remaining.length && previous.every((intent, i) => intent === remaining[i]))
|
|
134
|
+
return current;
|
|
111
135
|
return { ...next, observedReadIntents: remaining };
|
|
112
136
|
}
|
|
113
137
|
/** Called only for a message the visible viewport actually reached. */
|
|
@@ -153,32 +177,108 @@ export function coalesceReadOperations(current) {
|
|
|
153
177
|
return [...batches.values()];
|
|
154
178
|
}
|
|
155
179
|
/** Counts are over the indexed message coverage, not an estimate of missing history. */
|
|
156
|
-
export function unreadInThread(state, threadId) {
|
|
180
|
+
export function unreadInThread(state, threadId, hidden) {
|
|
157
181
|
const thread = state.threads.find(t => t.id === threadId);
|
|
158
182
|
if (!thread)
|
|
159
183
|
throw new Error("UNKNOWN_READ_THREAD");
|
|
160
184
|
const explicit = new Set(thread.explicitReadMessageIds);
|
|
161
|
-
const messages = thread.messages.filter(message => !message.recalled && message.from !== state.profileId && message.recipients.includes(state.profileId)
|
|
185
|
+
const messages = thread.messages.filter(message => !message.recalled && !hidden?.get(message.id)?.has(message.from) && !(message.readingControl || isReadingControl(message.payload, state.appId)) && message.from !== state.profileId && message.recipients.includes(state.profileId)
|
|
162
186
|
&& (message.readPolicy === "explicit" ? !explicit.has(message.id) : message.seq > thread.readThroughSeq));
|
|
163
187
|
const unreadMessageCount = messages.length;
|
|
164
188
|
const unreadMentionCount = messages.filter(message => message.mentions.includes(state.profileId)).length;
|
|
165
189
|
return { unreadMessageCount, unreadMentionCount, badgeCount: thread.kind === "group" ? unreadMentionCount : unreadMessageCount };
|
|
166
190
|
}
|
|
167
|
-
|
|
191
|
+
function readTeamVersion(versions, team) { return Object.hasOwn(versions, team) ? versions[team] : 0; }
|
|
192
|
+
export function assertReadIndexVersions(expected, current = {}, teamId) {
|
|
193
|
+
const teams = new Set(teamId === undefined ? [...Object.keys(expected), ...Object.keys(current)] : [teamId]);
|
|
194
|
+
if ([...teams].some(team => readTeamVersion(expected, team) !== readTeamVersion(current, team)))
|
|
195
|
+
throw new Error("READ_SCOPE_CHANGED");
|
|
196
|
+
}
|
|
197
|
+
/** Remove only the withdrawn team's original sources, logical conversations and
|
|
198
|
+
* queued observations. Other teams' pending operations must survive. */
|
|
199
|
+
export function removeReadTeam(state, teamId, knownThreadIds = []) {
|
|
200
|
+
const removed = new Set(knownThreadIds);
|
|
201
|
+
for (const thread of state.threads)
|
|
202
|
+
if (thread.teamId === teamId) {
|
|
203
|
+
removed.add(thread.id);
|
|
204
|
+
if (thread.conversationId)
|
|
205
|
+
removed.add(thread.conversationId);
|
|
206
|
+
}
|
|
207
|
+
return { ...state, syncCursor: "0", threads: state.threads.filter(thread => thread.teamId !== teamId && !removed.has(thread.id)),
|
|
208
|
+
pending: state.pending.filter(operation => !removed.has(operation.threadId)),
|
|
209
|
+
observedReadIntents: state.observedReadIntents?.filter(intent => !removed.has(intent.threadId)) };
|
|
210
|
+
}
|
|
211
|
+
/** Preserves the database and logical keys; store renaming fences old format writers. */
|
|
168
212
|
export class IndexedDbReadIndex {
|
|
169
213
|
storageScope;
|
|
214
|
+
migrateLegacy;
|
|
215
|
+
metadataOnly;
|
|
170
216
|
database;
|
|
171
217
|
profiles;
|
|
218
|
+
threadRows;
|
|
219
|
+
messageRows;
|
|
220
|
+
positionRows;
|
|
221
|
+
normalizedStates = new WeakSet();
|
|
222
|
+
orderedStates = new WeakSet();
|
|
223
|
+
legacyClean = new Set();
|
|
224
|
+
observationMessages = new WeakMap();
|
|
225
|
+
versions;
|
|
172
226
|
closed = false;
|
|
173
|
-
|
|
227
|
+
journal;
|
|
228
|
+
factory;
|
|
229
|
+
schemaReady;
|
|
230
|
+
ownerTail = Promise.resolve();
|
|
231
|
+
constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false, metadataOnly = false) {
|
|
174
232
|
this.storageScope = storageScope;
|
|
233
|
+
this.migrateLegacy = migrateLegacy;
|
|
234
|
+
this.metadataOnly = metadataOnly;
|
|
175
235
|
if (!factory)
|
|
176
236
|
throw new Error("READ_STORAGE_UNAVAILABLE");
|
|
237
|
+
this.factory = factory;
|
|
177
238
|
this.database = new Dexie(name, { indexedDB: factory, IDBKeyRange: globalThis.IDBKeyRange });
|
|
178
239
|
// The empty schema retains the legacy out-of-line primary key and records.
|
|
179
240
|
this.database.version(1).stores({ profiles: "" });
|
|
180
|
-
this.
|
|
241
|
+
this.database.version(2).stores({ profiles: "", versions: "" });
|
|
242
|
+
this.database.version(3).stores({ readThreads: "[owner+id],owner", readMessages: "[owner+thread+id],[owner+thread],owner", readPositions: "[owner+id],owner" });
|
|
243
|
+
// Native indexes track scalar source identity even when an older writer
|
|
244
|
+
// changes rows. Reading these keys does not deserialize messageIds arrays.
|
|
245
|
+
this.database.version(4).stores({ readThreads: "[owner+id],owner,[owner+id+value.kind+value.teamId],[owner+value.conversationId+value.kind+value.teamId+id],[owner+id+value.conversationId]" });
|
|
246
|
+
// Keep v4 indexes for already-open older SDKs, and make source discovery
|
|
247
|
+
// dense for legitimate legacy rows whose optional team field is absent.
|
|
248
|
+
this.database.version(5).stores({ readThreads: "[owner+id],owner,[owner+id+value.kind+value.teamId],[owner+value.conversationId+value.kind+value.teamId+id],[owner+id+value.conversationId],[owner+id+value.kind],[owner+value.conversationId+value.kind+id]" });
|
|
249
|
+
this.database.version(6).stores({ profiles: "unsupportedReadingFormat", versions: "unsupportedReadingFormat", readingProfiles: "", readingVersions: "", readThreads: null, readMessages: null, readPositions: null, readingThreads: "[owner+id],owner,[owner+id+value.kind+value.teamId],[owner+value.conversationId+value.kind+value.teamId+id],[owner+id+value.conversationId],[owner+id+value.kind],[owner+value.conversationId+value.kind+id]", readingMessages: "[owner+thread+id],[owner+thread],owner", readingPositions: "[owner+id],owner", readBatches: "[owner+key],owner" });
|
|
250
|
+
this.journal = new ReadBatchJournal(this.database, () => this.closed);
|
|
251
|
+
this.positionRows = this.database.table("readingPositions");
|
|
252
|
+
this.threadRows = this.database.table("readingThreads");
|
|
253
|
+
this.messageRows = this.database.table("readingMessages");
|
|
254
|
+
this.profiles = this.database.table("readingProfiles");
|
|
255
|
+
this.versions = this.database.table("readingVersions");
|
|
181
256
|
this.database.on("versionchange", () => { this.database.close({ disableAutoOpen: false }); return false; });
|
|
257
|
+
this.schemaReady = prepareReadSchema(factory, name);
|
|
258
|
+
void this.schemaReady.catch(() => { });
|
|
259
|
+
}
|
|
260
|
+
async owned(appId, profileId, run) {
|
|
261
|
+
const previous = this.ownerTail;
|
|
262
|
+
let release;
|
|
263
|
+
this.ownerTail = new Promise(resolve => { release = resolve; });
|
|
264
|
+
await previous;
|
|
265
|
+
try {
|
|
266
|
+
if (this.closed)
|
|
267
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
268
|
+
await this.schemaReady;
|
|
269
|
+
if (this.closed)
|
|
270
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
271
|
+
const owner = JSON.stringify(this.key(appId, profileId));
|
|
272
|
+
return await lockReadOwner(this.factory, this.database.name, owner, async () => { await this.journal.recover(owner); return run(); }, work => this.journal.lease(owner, work));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (this.closed)
|
|
276
|
+
throw new Error("READ_STORAGE_CLOSED");
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
release();
|
|
281
|
+
}
|
|
182
282
|
}
|
|
183
283
|
async transaction(mode, execute) {
|
|
184
284
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -186,11 +286,15 @@ export class IndexedDbReadIndex {
|
|
|
186
286
|
throw new Error("READ_STORAGE_CLOSED");
|
|
187
287
|
let started = false;
|
|
188
288
|
try {
|
|
189
|
-
return await this.database.transaction(mode, this.profiles, async () => {
|
|
289
|
+
return await this.database.transaction(mode, [this.profiles, this.versions, this.threadRows, this.messageRows, this.positionRows, this.database.table("readBatches")], async () => {
|
|
190
290
|
if (this.closed)
|
|
191
291
|
throw new Error("READ_STORAGE_CLOSED");
|
|
192
292
|
started = true;
|
|
193
|
-
|
|
293
|
+
await this.journal.checkLeases();
|
|
294
|
+
const result = await execute();
|
|
295
|
+
if (mode === "rw")
|
|
296
|
+
await this.journal.checkLeases(true);
|
|
297
|
+
return result;
|
|
194
298
|
});
|
|
195
299
|
}
|
|
196
300
|
catch (error) {
|
|
@@ -205,21 +309,355 @@ export class IndexedDbReadIndex {
|
|
|
205
309
|
throw new Error("READ_STORAGE_UNAVAILABLE");
|
|
206
310
|
}
|
|
207
311
|
key(appId, profileId) { return this.storageScope === undefined ? [appId, profileId] : [appId, profileId, this.storageScope]; }
|
|
208
|
-
|
|
209
|
-
|
|
312
|
+
/** Keyset pages avoid materializing a second, unbounded array of durable rows.
|
|
313
|
+
* Keep the caller's transaction so every page belongs to one consistent view. */
|
|
314
|
+
async readPages(table, owner, key, visit) {
|
|
315
|
+
let after;
|
|
316
|
+
for (;;) {
|
|
317
|
+
const rows = await table.where(":id").between(after ?? [owner, Dexie.minKey], [owner, Dexie.maxKey], !after, true).limit(128).toArray();
|
|
318
|
+
for (const row of rows)
|
|
319
|
+
visit(row);
|
|
320
|
+
if (rows.length < 128)
|
|
321
|
+
return;
|
|
322
|
+
after = key(rows[rows.length - 1]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async hydrate(current, key) {
|
|
326
|
+
if (current.normalized) {
|
|
327
|
+
const owner = JSON.stringify(key);
|
|
328
|
+
const byThread = new Map();
|
|
329
|
+
const byPosition = new Map(), byId = new Map();
|
|
330
|
+
await Promise.all([
|
|
331
|
+
this.readPages(this.threadRows, owner, row => [row.owner, row.id], row => byId.set(row.id, row)),
|
|
332
|
+
this.readPages(this.messageRows, owner, row => [row.owner, row.thread, row.id], row => {
|
|
333
|
+
if (!byThread.has(row.thread))
|
|
334
|
+
byThread.set(row.thread, new Map());
|
|
335
|
+
byThread.get(row.thread).set(row.id, row);
|
|
336
|
+
}),
|
|
337
|
+
this.readPages(this.positionRows, owner, row => [row.owner, row.id], row => byPosition.set(row.id, row)),
|
|
338
|
+
]);
|
|
339
|
+
const { normalized: _normalized, threadOrder, ...state } = current;
|
|
340
|
+
const result = { ...state, threads: (threadOrder ?? []).map(id => {
|
|
341
|
+
const row = byId.get(id);
|
|
342
|
+
const position = byPosition.get(id);
|
|
343
|
+
if (!row || !position)
|
|
344
|
+
throw new Error("READ_STORAGE_INCOMPLETE");
|
|
345
|
+
const stored = byThread.get(id) ?? new Map();
|
|
346
|
+
let messages;
|
|
347
|
+
if (row.messageCount !== undefined && !row.messageIds.length) {
|
|
348
|
+
const ordered = [...stored.values()], ranked = ordered.every(row => Number.isSafeInteger(row.ordinal));
|
|
349
|
+
ordered.sort((a, b) => ranked ? a.ordinal - b.ordinal : a.value.seq - b.value.seq);
|
|
350
|
+
if (ordered.length !== row.messageCount)
|
|
351
|
+
throw new Error("READ_STORAGE_INCOMPLETE");
|
|
352
|
+
messages = ordered.map(row => row.value);
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
messages = row.messageIds.map(messageId => {
|
|
356
|
+
const message = stored.get(messageId);
|
|
357
|
+
if (!message)
|
|
358
|
+
throw new Error("READ_STORAGE_INCOMPLETE");
|
|
359
|
+
return message.value;
|
|
360
|
+
});
|
|
361
|
+
return { ...row.value, readThroughSeq: position.readThroughSeq, explicitReadMessageIds: position.explicitReadMessageIds, messages };
|
|
362
|
+
}) };
|
|
363
|
+
this.normalizedStates.add(result);
|
|
364
|
+
if ((threadOrder ?? []).every(id => byId.get(id)?.messageCount !== undefined && !byId.get(id).messageIds.length && [...(byThread.get(id)?.values() ?? [])].every(row => Number.isSafeInteger(row.ordinal))))
|
|
365
|
+
this.orderedStates.add(result);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
return current;
|
|
210
369
|
}
|
|
211
|
-
async
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
370
|
+
async loadCurrent(appId, profileId) {
|
|
371
|
+
const current = await this.loadStored(appId, profileId);
|
|
372
|
+
if (!current || !this.metadataOnly)
|
|
373
|
+
return current;
|
|
374
|
+
const metadata = readingMetadata(current);
|
|
375
|
+
if (metadata !== current)
|
|
376
|
+
await this.writeState(appId, profileId, current, metadata);
|
|
377
|
+
return metadata;
|
|
378
|
+
}
|
|
379
|
+
async loadStored(appId, profileId) {
|
|
380
|
+
const current = await this.transaction("r", async () => {
|
|
381
|
+
if (this.storageScope === undefined && (await this.profiles.get([appId, profileId, "legacy-owner-v1"]))?.legacyOwner)
|
|
382
|
+
return undefined;
|
|
383
|
+
const row = await this.profiles.get(this.key(appId, profileId));
|
|
384
|
+
if (row && (row.appId !== appId || row.profileId !== profileId || row.schemaVersion !== 1))
|
|
216
385
|
throw new Error("READ_IDENTITY_MISMATCH");
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
386
|
+
return row?.normalized ? this.hydrate(row, this.key(appId, profileId)) : row;
|
|
387
|
+
});
|
|
388
|
+
if (current) {
|
|
389
|
+
await this.cleanupClaimedLegacy(appId, profileId);
|
|
390
|
+
if (!this.normalizedStates.has(current) && (this.migrateLegacy || this.metadataOnly)) {
|
|
391
|
+
const next = this.metadataOnly ? readingMetadata(current) : current;
|
|
392
|
+
await this.writeState(appId, profileId, undefined, next);
|
|
393
|
+
return next;
|
|
394
|
+
}
|
|
395
|
+
return current;
|
|
396
|
+
}
|
|
397
|
+
if (!this.migrateLegacy || this.storageScope === undefined)
|
|
398
|
+
return undefined;
|
|
399
|
+
const legacyOwner = JSON.stringify([appId, profileId]);
|
|
400
|
+
const legacy = await lockReadOwner(this.factory, this.database.name, legacyOwner, async () => {
|
|
401
|
+
await this.journal.recover(legacyOwner);
|
|
402
|
+
const allowed = await this.transaction("rw", async () => {
|
|
403
|
+
const key = [appId, profileId, "legacy-owner-v1"], marker = await this.profiles.get(key);
|
|
404
|
+
if (marker && marker.legacyOwner !== this.storageScope)
|
|
405
|
+
return false;
|
|
406
|
+
const raw = await this.profiles.get([appId, profileId]);
|
|
407
|
+
if (!raw)
|
|
408
|
+
return false;
|
|
409
|
+
if (raw.appId !== appId || raw.profileId !== profileId || raw.schemaVersion !== 1)
|
|
410
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
411
|
+
if (!marker)
|
|
412
|
+
await this.profiles.put({ ...createReadIndex(appId, profileId), legacyOwner: this.storageScope }, key);
|
|
413
|
+
return true;
|
|
414
|
+
});
|
|
415
|
+
if (!allowed)
|
|
416
|
+
return undefined;
|
|
417
|
+
return this.transaction("r", async () => { const row = await this.profiles.get([appId, profileId]); return row?.normalized ? this.hydrate(row, [appId, profileId]) : row; });
|
|
418
|
+
}, work => this.journal.lease(legacyOwner, work));
|
|
419
|
+
if (!legacy)
|
|
420
|
+
return undefined;
|
|
421
|
+
if (legacy.appId !== appId || legacy.profileId !== profileId || legacy.schemaVersion !== 1)
|
|
422
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
423
|
+
const next = this.metadataOnly ? readingMetadata(legacy) : legacy;
|
|
424
|
+
await this.writeState(appId, profileId, undefined, next);
|
|
425
|
+
await this.cleanupClaimedLegacy(appId, profileId);
|
|
426
|
+
return next;
|
|
427
|
+
}
|
|
428
|
+
async cleanupClaimedLegacy(appId, profileId) {
|
|
429
|
+
if (this.storageScope === undefined || !this.migrateLegacy)
|
|
430
|
+
return;
|
|
431
|
+
const key = JSON.stringify(this.key(appId, profileId));
|
|
432
|
+
if (this.legacyClean.has(key))
|
|
433
|
+
return;
|
|
434
|
+
const marker = await this.transaction("r", () => this.profiles.get([appId, profileId, "legacy-owner-v1"]));
|
|
435
|
+
if (marker?.legacyOwner === this.storageScope) {
|
|
436
|
+
await this.transaction("rw", () => this.profiles.delete([appId, profileId]));
|
|
437
|
+
await this.cleanUnreachable(JSON.stringify([appId, profileId]), undefined);
|
|
438
|
+
}
|
|
439
|
+
this.legacyClean.add(key);
|
|
440
|
+
}
|
|
441
|
+
async load(appId, profileId) {
|
|
442
|
+
return this.owned(appId, profileId, () => this.loadCurrent(appId, profileId));
|
|
443
|
+
}
|
|
444
|
+
async loadVersioned(appId, profileId) {
|
|
445
|
+
return this.owned(appId, profileId, async () => ({ state: await this.loadCurrent(appId, profileId), versions: await this.transaction("r", async () => (await this.versions.get(this.key(appId, profileId)))?.teams ?? {}) }));
|
|
446
|
+
}
|
|
447
|
+
async loadVersions(appId, profileId) {
|
|
448
|
+
return this.owned(appId, profileId, () => this.transaction("r", async () => (await this.versions.get(this.key(appId, profileId)))?.teams ?? {}));
|
|
449
|
+
}
|
|
450
|
+
async invalidateTeam(appId, profileId, teamId, version, knownThreadIds = []) {
|
|
451
|
+
if (!isValidChatTeamId(teamId) || !Number.isSafeInteger(version) || version < 1)
|
|
452
|
+
throw new Error("INVALID_READ_SCOPE_VERSION");
|
|
453
|
+
return this.owned(appId, profileId, async () => {
|
|
454
|
+
const teams = await this.transaction("r", async () => (await this.versions.get(this.key(appId, profileId)))?.teams ?? {});
|
|
455
|
+
if (readTeamVersion(teams, teamId) >= version)
|
|
456
|
+
return await this.loadCurrent(appId, profileId) ?? createReadIndex(appId, profileId);
|
|
457
|
+
return this.updateOwned(appId, profileId, state => removeReadTeam(state, teamId, knownThreadIds), undefined, { teams: { ...teams, [teamId]: version } });
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async observe(appId, profileId, transform, scope) {
|
|
461
|
+
if (!this.metadataOnly || scope.targets.length > 128)
|
|
462
|
+
return undefined;
|
|
463
|
+
const key = this.key(appId, profileId), owner = JSON.stringify(key), base = scope.base;
|
|
464
|
+
const header = await this.profiles.get(key);
|
|
465
|
+
if (header && (header.appId !== appId || header.profileId !== profileId || header.schemaVersion !== 1))
|
|
466
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
467
|
+
if (!header?.normalized || base.schemaVersion !== 1 || base.appId !== appId || base.profileId !== profileId || base.syncCursor !== header.syncCursor ||
|
|
468
|
+
header.threadOrder?.length !== base.threads.length || header.threadOrder.some((id, i) => id !== base.threads[i].id))
|
|
469
|
+
return undefined;
|
|
470
|
+
const sources = new Map();
|
|
471
|
+
for (const threadId of new Set(scope.targets.map(target => target.threadId))) {
|
|
472
|
+
const direct = await this.threadRows.where("[owner+id+value.kind]").between([owner, threadId, Dexie.minKey], [owner, threadId, Dexie.maxKey], true, true).limit(129).keys();
|
|
473
|
+
const assigned = await this.threadRows.where("[owner+value.conversationId+value.kind+id]").between([owner, threadId, Dexie.minKey], [owner, threadId, Dexie.maxKey], true, true).limit(129).keys();
|
|
474
|
+
if (direct.length + assigned.length > 128)
|
|
475
|
+
return undefined;
|
|
476
|
+
const identities = [...new Map([...direct.map(value => ({ id: value[1], kind: value[2] })), ...assigned.map(value => ({ id: value[3], kind: value[2] }))].map(value => [value.id, value])).values()];
|
|
477
|
+
const indexedIds = new Set(identities.map(identity => identity.id));
|
|
478
|
+
const expected = base.threads.filter(thread => thread.id === threadId || thread.conversationId === threadId);
|
|
479
|
+
// Sparse indexes omit legacy sources without team metadata. A partial
|
|
480
|
+
// family cannot safely resolve an observation for the logical thread.
|
|
481
|
+
if (indexedIds.size !== expected.length || expected.some(thread => !indexedIds.has(thread.id)))
|
|
482
|
+
return undefined;
|
|
483
|
+
for (const identity of identities) {
|
|
484
|
+
const cached = base.threads.find(thread => thread.id === identity.id);
|
|
485
|
+
if (!cached || cached.kind !== identity.kind)
|
|
486
|
+
return undefined;
|
|
487
|
+
const team = await this.threadRows.where("[owner+id+value.kind+value.teamId]").between([owner, identity.id, Dexie.minKey], [owner, identity.id, Dexie.maxKey], true, true).limit(2).keys();
|
|
488
|
+
if (team.length > 1 || cached.teamId !== team[0]?.[3])
|
|
489
|
+
return undefined;
|
|
490
|
+
const mapping = await this.threadRows.where("[owner+id+value.conversationId]").between([owner, identity.id, Dexie.minKey], [owner, identity.id, Dexie.maxKey], true, true).limit(2).keys();
|
|
491
|
+
if (mapping.length > 1 || cached.conversationId !== mapping[0]?.[2])
|
|
492
|
+
return undefined;
|
|
493
|
+
let source = sources.get(identity.id);
|
|
494
|
+
if (!source) {
|
|
495
|
+
source = { cached, ids: new Set() };
|
|
496
|
+
sources.set(identity.id, source);
|
|
497
|
+
}
|
|
498
|
+
for (const target of scope.targets)
|
|
499
|
+
if (target.threadId === threadId)
|
|
500
|
+
source.ids.add(target.messageId);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if ([...sources.values()].reduce((sum, source) => sum + source.ids.size, 0) > 128)
|
|
504
|
+
return undefined;
|
|
505
|
+
const sourceIds = [...sources.keys()];
|
|
506
|
+
// Cursors are the mutable cross-document view. Refresh their small rows,
|
|
507
|
+
// while message metadata remains targeted; otherwise a later batch based
|
|
508
|
+
// on an older snapshot could regress a different conversation's progress.
|
|
509
|
+
const positionById = new Map();
|
|
510
|
+
await this.readPages(this.positionRows, owner, row => [row.owner, row.id], row => positionById.set(row.id, row));
|
|
511
|
+
if (base.threads.some(thread => !positionById.has(thread.id)))
|
|
512
|
+
return undefined;
|
|
513
|
+
const positions = sourceIds.map(id => positionById.get(id));
|
|
514
|
+
const partialThreads = [];
|
|
515
|
+
for (let i = 0; i < sourceIds.length; i++) {
|
|
516
|
+
const id = sourceIds[i], source = sources.get(id), position = positions[i];
|
|
517
|
+
if (!position)
|
|
518
|
+
return undefined;
|
|
519
|
+
let cached = this.observationMessages.get(source.cached.messages);
|
|
520
|
+
if (!cached) {
|
|
521
|
+
cached = new Map(source.cached.messages.map(message => [message.id, message]));
|
|
522
|
+
this.observationMessages.set(source.cached.messages, cached);
|
|
523
|
+
}
|
|
524
|
+
const ids = [...source.ids], rows = await this.messageRows.bulkGet(ids.map(messageId => [owner, id, messageId]));
|
|
525
|
+
const messages = [];
|
|
526
|
+
for (let j = 0; j < ids.length; j++) {
|
|
527
|
+
const previous = cached.get(ids[j]), message = rows[j]?.value;
|
|
528
|
+
if (message && (message.payload !== null || typeof message.readingControl !== "boolean"))
|
|
529
|
+
return undefined;
|
|
530
|
+
if (JSON.stringify(previous) !== JSON.stringify(message))
|
|
531
|
+
return undefined;
|
|
532
|
+
if (message)
|
|
533
|
+
messages.push(previous);
|
|
534
|
+
}
|
|
535
|
+
partialThreads.push({ ...source.cached, messages, readThroughSeq: position.readThroughSeq, explicitReadMessageIds: position.explicitReadMessageIds });
|
|
536
|
+
}
|
|
537
|
+
const partial = { schemaVersion: 1, appId, profileId, syncCursor: header.syncCursor, pending: header.pending, observedReadIntents: header.observedReadIntents, threads: partialThreads };
|
|
538
|
+
const result = transform(partial, (await this.versions.get(key))?.teams ?? {});
|
|
539
|
+
if (result.appId !== appId || result.profileId !== profileId || result.schemaVersion !== 1)
|
|
540
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
541
|
+
if (result.syncCursor !== partial.syncCursor || result.threads.length !== partialThreads.length || result.threads.some((thread, i) => thread.id !== partialThreads[i].id || thread.kind !== partialThreads[i].kind || thread.teamId !== partialThreads[i].teamId || thread.conversationId !== partialThreads[i].conversationId || thread.messages !== partialThreads[i].messages))
|
|
542
|
+
return undefined;
|
|
543
|
+
const writes = [];
|
|
544
|
+
for (let i = 0; i < result.threads.length; i++) {
|
|
545
|
+
const thread = result.threads[i], before = positions[i];
|
|
546
|
+
if (thread.readThroughSeq !== before.readThroughSeq || JSON.stringify(thread.explicitReadMessageIds) !== JSON.stringify(before.explicitReadMessageIds))
|
|
547
|
+
writes.push({ owner, id: thread.id, readThroughSeq: thread.readThroughSeq, explicitReadMessageIds: thread.explicitReadMessageIds });
|
|
548
|
+
}
|
|
549
|
+
if (writes.length > 126)
|
|
550
|
+
return undefined;
|
|
551
|
+
if (writes.length)
|
|
552
|
+
await this.positionRows.bulkPut(writes);
|
|
553
|
+
if (JSON.stringify(header.pending) !== JSON.stringify(result.pending) || JSON.stringify(header.observedReadIntents) !== JSON.stringify(result.observedReadIntents))
|
|
554
|
+
await this.profiles.put({ ...header, pending: result.pending, observedReadIntents: result.observedReadIntents }, key);
|
|
555
|
+
const byId = new Map(positionById);
|
|
556
|
+
result.threads.forEach(thread => byId.set(thread.id, thread));
|
|
557
|
+
let changed = false;
|
|
558
|
+
const threads = base.threads.map(thread => {
|
|
559
|
+
const position = byId.get(thread.id);
|
|
560
|
+
if (!position || thread.readThroughSeq === position.readThroughSeq && JSON.stringify(thread.explicitReadMessageIds) === JSON.stringify(position.explicitReadMessageIds))
|
|
561
|
+
return thread;
|
|
562
|
+
changed = true;
|
|
563
|
+
return { ...thread, readThroughSeq: position.readThroughSeq, explicitReadMessageIds: position.explicitReadMessageIds };
|
|
220
564
|
});
|
|
565
|
+
if (!changed && JSON.stringify(base.pending) === JSON.stringify(result.pending) && JSON.stringify(base.observedReadIntents) === JSON.stringify(result.observedReadIntents))
|
|
566
|
+
return base;
|
|
567
|
+
return { ...base, threads, pending: result.pending, observedReadIntents: result.observedReadIntents };
|
|
568
|
+
}
|
|
569
|
+
async update(appId, profileId, transform, observation) {
|
|
570
|
+
return this.owned(appId, profileId, () => this.updateOwned(appId, profileId, transform, observation));
|
|
571
|
+
}
|
|
572
|
+
async updateOwned(appId, profileId, transform, observation, versions) {
|
|
573
|
+
if (observation) {
|
|
574
|
+
const observed = await this.transaction("rw", () => this.observe(appId, profileId, transform, observation));
|
|
575
|
+
if (observed)
|
|
576
|
+
return observed;
|
|
577
|
+
}
|
|
578
|
+
if (this.storageScope === undefined && (await this.profiles.get([appId, profileId, "legacy-owner-v1"]))?.legacyOwner)
|
|
579
|
+
throw new Error("READ_LEGACY_OWNERSHIP_CHANGED");
|
|
580
|
+
const loaded = { previous: await this.loadCurrent(appId, profileId), versions: await this.transaction("r", async () => (await this.versions.get(this.key(appId, profileId)))?.teams ?? {}) };
|
|
581
|
+
const previous = loaded.previous, transformed = transform(previous ?? createReadIndex(appId, profileId), loaded.versions);
|
|
582
|
+
const next = this.metadataOnly ? readingMetadata(transformed) : transformed;
|
|
583
|
+
if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
|
|
584
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
585
|
+
if (next === previous && !versions)
|
|
586
|
+
return next;
|
|
587
|
+
await this.writeState(appId, profileId, previous, next, versions);
|
|
588
|
+
return next;
|
|
589
|
+
}
|
|
590
|
+
async writeState(appId, profileId, previous, next, versions) {
|
|
591
|
+
const owner = JSON.stringify(this.key(appId, profileId)), header = { ...next, threads: [], normalized: true, threadOrder: next.threads.map(thread => thread.id) };
|
|
592
|
+
if (!previous || !this.orderedStates.has(previous))
|
|
593
|
+
await this.cleanUnreachable(owner, previous);
|
|
594
|
+
await this.journal.commit(owner, this.changes(owner, previous, next), { profileKey: this.key(appId, profileId), header, versions });
|
|
595
|
+
this.normalizedStates.add(next);
|
|
596
|
+
this.orderedStates.add(next);
|
|
597
|
+
}
|
|
598
|
+
async cleanUnreachable(owner, previous) {
|
|
599
|
+
const threads = new Map(previous?.threads.map(thread => [thread.id, new Set(thread.messages.map(message => message.id))]) ?? []);
|
|
600
|
+
for (const name of ["readingThreads", "readingPositions", "readingMessages"]) {
|
|
601
|
+
const table = this.database.table(name);
|
|
602
|
+
let after;
|
|
603
|
+
for (;;) {
|
|
604
|
+
const keys = await table.where(":id").between(after ?? [owner, Dexie.minKey], [owner, Dexie.maxKey], !after, true).limit(126).primaryKeys();
|
|
605
|
+
if (!keys.length)
|
|
606
|
+
break;
|
|
607
|
+
const dead = keys.filter(key => !threads.has(key[1]) || name === "readingMessages" && !threads.get(key[1]).has(key[2]));
|
|
608
|
+
if (dead.length)
|
|
609
|
+
await this.transaction("rw", () => table.bulkDelete(dead));
|
|
610
|
+
after = keys[keys.length - 1];
|
|
611
|
+
if (keys.length < 126)
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
*changes(owner, previous, next) {
|
|
617
|
+
const oldThreads = new Map(previous?.threads.map(thread => [thread.id, thread]) ?? []), ids = new Set(), modern = previous !== undefined && this.orderedStates.has(previous);
|
|
618
|
+
for (const thread of next.threads) {
|
|
619
|
+
if (ids.has(thread.id))
|
|
620
|
+
throw new Error("DUPLICATE_READ_THREAD");
|
|
621
|
+
ids.add(thread.id);
|
|
622
|
+
const old = oldThreads.get(thread.id);
|
|
623
|
+
if (old === thread && modern)
|
|
624
|
+
continue;
|
|
625
|
+
const { messages, readThroughSeq, explicitReadMessageIds, ...value } = thread;
|
|
626
|
+
if (!modern || !old || old.readThroughSeq !== readThroughSeq || JSON.stringify(old.explicitReadMessageIds) !== JSON.stringify(explicitReadMessageIds))
|
|
627
|
+
yield { table: "readingPositions", key: [owner, thread.id], value: { owner, id: thread.id, readThroughSeq, explicitReadMessageIds } };
|
|
628
|
+
const metadata = { owner, id: thread.id, value, messageIds: [], messageCount: messages.length };
|
|
629
|
+
const oldMetadata = old ? { owner, id: old.id, value: (({ messages: _m, readThroughSeq: _r, explicitReadMessageIds: _e, ...rest }) => rest)(old), messageIds: modern ? [] : old.messages.map(message => message.id), ...(modern ? { messageCount: old.messages.length } : {}) } : undefined;
|
|
630
|
+
if (JSON.stringify(metadata) !== JSON.stringify(oldMetadata))
|
|
631
|
+
yield { table: "readingThreads", key: [owner, thread.id], value: metadata };
|
|
632
|
+
if (old?.messages === messages && modern)
|
|
633
|
+
continue;
|
|
634
|
+
const byId = new Map(old?.messages.map(message => [message.id, message]) ?? []), ranks = new Map(old?.messages.map((message, index) => [message.id, index]) ?? []), seen = new Set();
|
|
635
|
+
for (const [ordinal, message] of messages.entries()) {
|
|
636
|
+
if (seen.has(message.id))
|
|
637
|
+
throw new Error("INVALID_READ_MESSAGE");
|
|
638
|
+
seen.add(message.id);
|
|
639
|
+
const prior = byId.get(message.id);
|
|
640
|
+
if (!modern || ranks.get(message.id) !== ordinal || prior !== message && JSON.stringify(prior) !== JSON.stringify(message))
|
|
641
|
+
yield { table: "readingMessages", key: [owner, thread.id, message.id], value: { owner, thread: thread.id, id: message.id, value: message, ordinal } };
|
|
642
|
+
}
|
|
643
|
+
for (const id of byId.keys())
|
|
644
|
+
if (!seen.has(id))
|
|
645
|
+
yield { table: "readingMessages", key: [owner, thread.id, id] };
|
|
646
|
+
}
|
|
647
|
+
for (const old of oldThreads.values())
|
|
648
|
+
if (!ids.has(old.id)) {
|
|
649
|
+
yield { table: "readingThreads", key: [owner, old.id] };
|
|
650
|
+
yield { table: "readingPositions", key: [owner, old.id] };
|
|
651
|
+
for (const message of old.messages)
|
|
652
|
+
yield { table: "readingMessages", key: [owner, old.id, message.id] };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async close() { this.closed = true; try {
|
|
656
|
+
await this.journal.releaseLeases();
|
|
221
657
|
}
|
|
222
|
-
|
|
658
|
+
finally {
|
|
659
|
+
this.database.close();
|
|
660
|
+
} }
|
|
223
661
|
}
|
|
224
662
|
export function sourcesInConversation(state, conversationId) {
|
|
225
663
|
return state.threads.filter(thread => (thread.conversationId ?? thread.id) === conversationId);
|
|
@@ -231,14 +669,14 @@ function readSource(state, conversationId, messageId) {
|
|
|
231
669
|
return sources[0];
|
|
232
670
|
}
|
|
233
671
|
/** Aggregate logical history while each original source retains its own read cursor. */
|
|
234
|
-
export function unreadInConversation(state, conversationId) {
|
|
672
|
+
export function unreadInConversation(state, conversationId, hidden) {
|
|
235
673
|
const sources = sourcesInConversation(state, conversationId);
|
|
236
674
|
if (!sources.length)
|
|
237
675
|
throw new Error("UNKNOWN_READ_CONVERSATION");
|
|
238
676
|
if (new Set(sources.map(source => source.teamId)).size > 1)
|
|
239
677
|
throw new Error("READ_CONVERSATION_TEAM_MISMATCH");
|
|
240
678
|
return sources.reduce((total, source) => {
|
|
241
|
-
const unread = unreadInThread(state, source.id);
|
|
679
|
+
const unread = unreadInThread(state, source.id, hidden);
|
|
242
680
|
return { unreadMessageCount: total.unreadMessageCount + unread.unreadMessageCount, unreadMentionCount: total.unreadMentionCount + unread.unreadMentionCount, badgeCount: total.badgeCount + unread.badgeCount };
|
|
243
681
|
}, { unreadMessageCount: 0, unreadMentionCount: 0, badgeCount: 0 });
|
|
244
682
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Metadata-only store renaming fences SDKs that cannot read chunked snapshots. */
|
|
2
|
+
export async function prepareReadSchema(factory, name) {
|
|
3
|
+
const existing = await new Promise((resolve, reject) => {
|
|
4
|
+
const request = factory.open(name);
|
|
5
|
+
let fresh = false;
|
|
6
|
+
request.onupgradeneeded = event => { if (event.oldVersion === 0) {
|
|
7
|
+
fresh = true;
|
|
8
|
+
request.transaction.abort();
|
|
9
|
+
} };
|
|
10
|
+
request.onerror = event => { event.preventDefault(); if (fresh)
|
|
11
|
+
resolve(undefined);
|
|
12
|
+
else
|
|
13
|
+
reject(request.error); };
|
|
14
|
+
request.onsuccess = () => resolve(request.result);
|
|
15
|
+
});
|
|
16
|
+
if (!existing)
|
|
17
|
+
return;
|
|
18
|
+
const prepared = existing.objectStoreNames.contains("readingProfiles") && existing.objectStoreNames.contains("readingMessages");
|
|
19
|
+
const version = existing.version;
|
|
20
|
+
existing.close();
|
|
21
|
+
if (version >= 60 && prepared)
|
|
22
|
+
return;
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const request = factory.open(name, Math.max(60, version + 1));
|
|
25
|
+
request.onerror = () => reject(request.error);
|
|
26
|
+
request.onupgradeneeded = () => {
|
|
27
|
+
const db = request.result, tx = request.transaction;
|
|
28
|
+
for (const [before, after] of [["profiles", "readingProfiles"], ["versions", "readingVersions"], ["readThreads", "readingThreads"], ["readMessages", "readingMessages"], ["readPositions", "readingPositions"]]) {
|
|
29
|
+
if (db.objectStoreNames.contains(before)) {
|
|
30
|
+
if (db.objectStoreNames.contains(after)) {
|
|
31
|
+
tx.abort();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
tx.objectStore(before).name = after;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!db.objectStoreNames.contains("readingProfiles"))
|
|
38
|
+
db.createObjectStore("readingProfiles");
|
|
39
|
+
if (!db.objectStoreNames.contains("readingVersions"))
|
|
40
|
+
db.createObjectStore("readingVersions");
|
|
41
|
+
// Explicit-key writes from older SDKs must fail rather than recreate a cache
|
|
42
|
+
// beside a partially applied transition or silently return an empty history.
|
|
43
|
+
db.createObjectStore("profiles", { keyPath: "unsupportedReadingFormat" });
|
|
44
|
+
db.createObjectStore("versions", { keyPath: "unsupportedReadingFormat" });
|
|
45
|
+
const define = (name, keyPath, indexes) => {
|
|
46
|
+
const store = db.objectStoreNames.contains(name) ? tx.objectStore(name) : db.createObjectStore(name, { keyPath });
|
|
47
|
+
for (const [index, path] of Object.entries(indexes))
|
|
48
|
+
if (!store.indexNames.contains(index))
|
|
49
|
+
store.createIndex(index, path);
|
|
50
|
+
};
|
|
51
|
+
define("readingThreads", ["owner", "id"], { owner: "owner", "[owner+id+value.kind+value.teamId]": ["owner", "id", "value.kind", "value.teamId"], "[owner+value.conversationId+value.kind+value.teamId+id]": ["owner", "value.conversationId", "value.kind", "value.teamId", "id"], "[owner+id+value.conversationId]": ["owner", "id", "value.conversationId"], "[owner+id+value.kind]": ["owner", "id", "value.kind"], "[owner+value.conversationId+value.kind+id]": ["owner", "value.conversationId", "value.kind", "id"] });
|
|
52
|
+
define("readingMessages", ["owner", "thread", "id"], { owner: "owner", "[owner+thread]": ["owner", "thread"] });
|
|
53
|
+
define("readingPositions", ["owner", "id"], { owner: "owner" });
|
|
54
|
+
define("readBatches", ["owner", "key"], { owner: "owner" });
|
|
55
|
+
};
|
|
56
|
+
request.onsuccess = () => { request.result.close(); resolve(); };
|
|
57
|
+
});
|
|
58
|
+
}
|