@pmate/chat-sdk 0.12.0-next.47 → 0.12.0-next.48
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.
|
@@ -71,6 +71,7 @@ export declare function installReadSnapshot(current: ReadIndexState, snapshot: R
|
|
|
71
71
|
export declare function applyReadChange(current: ReadIndexState, change: ReadChange): ReadIndexState;
|
|
72
72
|
/** Persist observations even if the message arrived before its Sequence metadata. */
|
|
73
73
|
export declare function recordReadIntent(current: ReadIndexState, intent: ObservedReadIntent): ReadIndexState;
|
|
74
|
+
export declare function resolveObservedReads(current: ReadIndexState): ReadIndexState;
|
|
74
75
|
/** Called only for a message the visible viewport actually reached. */
|
|
75
76
|
export declare function recordVisibleRead(current: ReadIndexState, threadId: string, messageId: string, operationId: string): ReadIndexState;
|
|
76
77
|
export declare function recordExplicitRead(current: ReadIndexState, threadId: string, messageId: string, operationId: string): ReadIndexState;
|
package/lib/runtime/readIndex.js
CHANGED
|
@@ -105,12 +105,12 @@ export function recordReadIntent(current, intent) {
|
|
|
105
105
|
throw new Error("INVALID_READ_INTENT");
|
|
106
106
|
const observed = current.observedReadIntents ?? [];
|
|
107
107
|
if (observed.some(item => item.threadId === intent.threadId && item.messageId === intent.messageId && item.readPolicy === intent.readPolicy))
|
|
108
|
-
return current;
|
|
108
|
+
return resolveObservedReads(current);
|
|
109
109
|
if (observed.some(item => item.operationId === intent.operationId) || current.pending.some(item => item.operationId === intent.operationId))
|
|
110
110
|
throw new Error("INVALID_READ_OPERATION_ID");
|
|
111
111
|
return resolveObservedReads({ ...current, observedReadIntents: [...observed, structuredClone(intent)] });
|
|
112
112
|
}
|
|
113
|
-
function resolveObservedReads(current) {
|
|
113
|
+
export function resolveObservedReads(current) {
|
|
114
114
|
let next = current;
|
|
115
115
|
const remaining = [];
|
|
116
116
|
for (const intent of current.observedReadIntents ?? []) {
|
|
@@ -127,6 +127,9 @@ function resolveObservedReads(current) {
|
|
|
127
127
|
next = intent.readPolicy === "explicit" ? recordExplicitRead(next, intent.threadId, intent.messageId, intent.operationId)
|
|
128
128
|
: recordVisibleRead(next, intent.threadId, intent.messageId, intent.operationId);
|
|
129
129
|
}
|
|
130
|
+
const previous = current.observedReadIntents;
|
|
131
|
+
if (next === current && previous && previous.length === remaining.length && previous.every((intent, i) => intent === remaining[i]))
|
|
132
|
+
return current;
|
|
130
133
|
return { ...next, observedReadIntents: remaining };
|
|
131
134
|
}
|
|
132
135
|
/** Called only for a message the visible viewport actually reached. */
|
|
@@ -231,6 +234,9 @@ export class IndexedDbReadIndex {
|
|
|
231
234
|
// Native indexes track scalar source identity even when an older writer
|
|
232
235
|
// changes rows. Reading these keys does not deserialize messageIds arrays.
|
|
233
236
|
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]" });
|
|
237
|
+
// Keep v4 indexes for already-open older SDKs, and make source discovery
|
|
238
|
+
// dense for legitimate legacy rows whose optional team field is absent.
|
|
239
|
+
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]" });
|
|
234
240
|
this.positionRows = this.database.table("readPositions");
|
|
235
241
|
this.threadRows = this.database.table("readThreads");
|
|
236
242
|
this.messageRows = this.database.table("readMessages");
|
|
@@ -451,16 +457,23 @@ export class IndexedDbReadIndex {
|
|
|
451
457
|
return undefined;
|
|
452
458
|
const sources = new Map();
|
|
453
459
|
for (const threadId of new Set(scope.targets.map(target => target.threadId))) {
|
|
454
|
-
const direct = await this.threadRows.where("[owner+id+value.kind
|
|
455
|
-
const assigned = await this.threadRows.where("[owner+value.conversationId+value.kind+
|
|
460
|
+
const direct = await this.threadRows.where("[owner+id+value.kind]").between([owner, threadId, Dexie.minKey], [owner, threadId, Dexie.maxKey], true, true).limit(129).keys();
|
|
461
|
+
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();
|
|
456
462
|
if (direct.length + assigned.length > 128)
|
|
457
463
|
return undefined;
|
|
458
|
-
const identities = [...direct.map(value => ({ id: value[1], kind: value[2]
|
|
459
|
-
|
|
464
|
+
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()];
|
|
465
|
+
const indexedIds = new Set(identities.map(identity => identity.id));
|
|
466
|
+
const expected = base.threads.filter(thread => thread.id === threadId || thread.conversationId === threadId);
|
|
467
|
+
// Sparse indexes omit legacy sources without team metadata. A partial
|
|
468
|
+
// family cannot safely resolve an observation for the logical thread.
|
|
469
|
+
if (indexedIds.size !== expected.length || expected.some(thread => !indexedIds.has(thread.id)))
|
|
460
470
|
return undefined;
|
|
461
471
|
for (const identity of identities) {
|
|
462
472
|
const cached = base.threads.find(thread => thread.id === identity.id);
|
|
463
|
-
if (!cached || cached.kind !== identity.kind
|
|
473
|
+
if (!cached || cached.kind !== identity.kind)
|
|
474
|
+
return undefined;
|
|
475
|
+
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();
|
|
476
|
+
if (team.length > 1 || cached.teamId !== team[0]?.[3])
|
|
464
477
|
return undefined;
|
|
465
478
|
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();
|
|
466
479
|
if (mapping.length > 1 || cached.conversationId !== mapping[0]?.[2])
|
package/lib/runtime/readSync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readingMetadata, assertReadIndexVersions, applyReadChange, ReadSyncGap, createReadIndex, installReadSnapshot } from "./readIndex.js";
|
|
1
|
+
import { readingMetadata, resolveObservedReads, assertReadIndexVersions, applyReadChange, ReadSyncGap, createReadIndex, installReadSnapshot } from "./readIndex.js";
|
|
2
2
|
/** Keeps local operations until an authoritative projection covers them. */
|
|
3
3
|
export class ReadSyncClient {
|
|
4
4
|
options;
|
|
@@ -11,9 +11,13 @@ export class ReadSyncClient {
|
|
|
11
11
|
async loadLocal() {
|
|
12
12
|
const { appId, profileId, persistence, metadataOnly } = this.options;
|
|
13
13
|
const state = await persistence.load(appId, profileId) ?? createReadIndex(appId, profileId);
|
|
14
|
-
|
|
14
|
+
const normalize = (current) => {
|
|
15
|
+
const next = metadataOnly ? readingMetadata(current) : current;
|
|
16
|
+
return next.observedReadIntents?.length ? resolveObservedReads(next) : next;
|
|
17
|
+
};
|
|
18
|
+
if (normalize(state) === state)
|
|
15
19
|
return state;
|
|
16
|
-
return persistence.update(appId, profileId,
|
|
20
|
+
return persistence.update(appId, profileId, normalize);
|
|
17
21
|
}
|
|
18
22
|
stop() { this.controller.abort(); }
|
|
19
23
|
synchronize() {
|
package/package.json
CHANGED