@pmate/chat-sdk 0.12.0-next.38 → 0.12.0-next.39
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/lib/runtime/chatSession.js +1 -1
- package/lib/runtime/readIndex.d.ts +11 -2
- package/lib/runtime/readIndex.js +41 -9
- package/lib/runtime/readSync.d.ts +1 -0
- package/lib/runtime/readSync.js +8 -5
- package/lib/runtime/readingClient.d.ts +1 -0
- package/lib/runtime/readingClient.js +4 -3
- package/package.json +1 -1
|
@@ -67,7 +67,7 @@ export class ChatSession {
|
|
|
67
67
|
return options.connection.onReach(event);
|
|
68
68
|
throw new Error("REACH_HANDLER_UNAVAILABLE");
|
|
69
69
|
} });
|
|
70
|
-
this.reading = new ReadingClient({ ...options.reading, teamId: scope.teamId, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true });
|
|
70
|
+
this.reading = new ReadingClient({ ...options.reading, teamId: scope.teamId, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true, metadataOnly: true });
|
|
71
71
|
this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, access: () => this.messages.scopeAccess(), onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
|
|
72
72
|
this.sender = new DurableChatSender(this.connection, scope, options.onError, { access: () => this.messages.scopeAccess() });
|
|
73
73
|
this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
|
|
@@ -7,9 +7,16 @@ export type IndexedReadMessage = {
|
|
|
7
7
|
mentions: string[];
|
|
8
8
|
readPolicy: "visible" | "explicit";
|
|
9
9
|
recalled: boolean;
|
|
10
|
-
/**
|
|
10
|
+
/** Legacy standalone readers may retain payloads; unified sessions use the message store. */
|
|
11
11
|
payload: unknown;
|
|
12
|
+
/** Body-free control classification retained by metadata-only reading indexes. */
|
|
13
|
+
readingControl?: boolean;
|
|
12
14
|
};
|
|
15
|
+
/** Retain read semantics without a second durable copy of message bodies. */
|
|
16
|
+
export declare function readingMetadata<T extends {
|
|
17
|
+
appId: string;
|
|
18
|
+
threads: IndexedReadThread[];
|
|
19
|
+
}>(state: T): T;
|
|
13
20
|
export type ReadingOperation = {
|
|
14
21
|
operationId: string;
|
|
15
22
|
threadId: string;
|
|
@@ -98,6 +105,7 @@ export interface ReadIndexPersistence {
|
|
|
98
105
|
export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
99
106
|
private storageScope?;
|
|
100
107
|
private migrateLegacy;
|
|
108
|
+
private metadataOnly;
|
|
101
109
|
private database;
|
|
102
110
|
private profiles;
|
|
103
111
|
private threadRows;
|
|
@@ -106,11 +114,12 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
|
106
114
|
private normalizedStates;
|
|
107
115
|
private versions;
|
|
108
116
|
private closed;
|
|
109
|
-
constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean);
|
|
117
|
+
constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean, metadataOnly?: boolean);
|
|
110
118
|
private transaction;
|
|
111
119
|
private key;
|
|
112
120
|
private hydrate;
|
|
113
121
|
private loadCurrent;
|
|
122
|
+
private loadStored;
|
|
114
123
|
/** Message bodies are immutable for local read observations: keep them out of
|
|
115
124
|
* profile/cursor writes, and update only changed normalized rows on sync. */
|
|
116
125
|
private persist;
|
package/lib/runtime/readIndex.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { Dexie } from "dexie";
|
|
2
2
|
import { isReadingControl } from "./readingContent.js";
|
|
3
3
|
import { isValidChatTeamId } from "./thread.js";
|
|
4
|
+
/** Retain read semantics without a second durable copy of message bodies. */
|
|
5
|
+
export function readingMetadata(state) {
|
|
6
|
+
let changed = false;
|
|
7
|
+
const threads = state.threads.map(thread => {
|
|
8
|
+
let different = false;
|
|
9
|
+
const messages = thread.messages.map(message => {
|
|
10
|
+
if (message.payload === null && typeof message.readingControl === "boolean")
|
|
11
|
+
return message;
|
|
12
|
+
different = true;
|
|
13
|
+
return { ...message, payload: null, readingControl: Boolean(message.readingControl) || isReadingControl(message.payload, state.appId) };
|
|
14
|
+
});
|
|
15
|
+
if (!different)
|
|
16
|
+
return thread;
|
|
17
|
+
changed = true;
|
|
18
|
+
return { ...thread, messages };
|
|
19
|
+
});
|
|
20
|
+
return changed ? { ...state, threads } : state;
|
|
21
|
+
}
|
|
4
22
|
export class ReadSyncGap extends Error {
|
|
5
23
|
cursor;
|
|
6
24
|
constructor(cursor) {
|
|
@@ -159,7 +177,7 @@ export function unreadInThread(state, threadId, hidden) {
|
|
|
159
177
|
if (!thread)
|
|
160
178
|
throw new Error("UNKNOWN_READ_THREAD");
|
|
161
179
|
const explicit = new Set(thread.explicitReadMessageIds);
|
|
162
|
-
const messages = thread.messages.filter(message => !message.recalled && !hidden?.get(message.id)?.has(message.from) && !isReadingControl(message.payload, state.appId) && message.from !== state.profileId && message.recipients.includes(state.profileId)
|
|
180
|
+
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)
|
|
163
181
|
&& (message.readPolicy === "explicit" ? !explicit.has(message.id) : message.seq > thread.readThroughSeq));
|
|
164
182
|
const unreadMessageCount = messages.length;
|
|
165
183
|
const unreadMentionCount = messages.filter(message => message.mentions.includes(state.profileId)).length;
|
|
@@ -189,6 +207,7 @@ export function removeReadTeam(state, teamId, knownThreadIds = []) {
|
|
|
189
207
|
export class IndexedDbReadIndex {
|
|
190
208
|
storageScope;
|
|
191
209
|
migrateLegacy;
|
|
210
|
+
metadataOnly;
|
|
192
211
|
database;
|
|
193
212
|
profiles;
|
|
194
213
|
threadRows;
|
|
@@ -197,9 +216,10 @@ export class IndexedDbReadIndex {
|
|
|
197
216
|
normalizedStates = new WeakSet();
|
|
198
217
|
versions;
|
|
199
218
|
closed = false;
|
|
200
|
-
constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false) {
|
|
219
|
+
constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false, metadataOnly = false) {
|
|
201
220
|
this.storageScope = storageScope;
|
|
202
221
|
this.migrateLegacy = migrateLegacy;
|
|
222
|
+
this.metadataOnly = metadataOnly;
|
|
203
223
|
if (!factory)
|
|
204
224
|
throw new Error("READ_STORAGE_UNAVAILABLE");
|
|
205
225
|
this.database = new Dexie(name, { indexedDB: factory, IDBKeyRange: globalThis.IDBKeyRange });
|
|
@@ -274,13 +294,23 @@ export class IndexedDbReadIndex {
|
|
|
274
294
|
return current;
|
|
275
295
|
}
|
|
276
296
|
async loadCurrent(appId, profileId) {
|
|
297
|
+
const current = await this.loadStored(appId, profileId);
|
|
298
|
+
if (!current || !this.metadataOnly)
|
|
299
|
+
return current;
|
|
300
|
+
const metadata = readingMetadata(current);
|
|
301
|
+
if (metadata !== current)
|
|
302
|
+
await this.persist(appId, profileId, current, metadata);
|
|
303
|
+
return metadata;
|
|
304
|
+
}
|
|
305
|
+
async loadStored(appId, profileId) {
|
|
277
306
|
const current = await this.profiles.get(this.key(appId, profileId));
|
|
278
307
|
if (current?.normalized)
|
|
279
308
|
return this.hydrate(current, this.key(appId, profileId));
|
|
280
309
|
if (current) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
310
|
+
const next = this.metadataOnly ? readingMetadata(current) : current;
|
|
311
|
+
if (this.migrateLegacy || this.metadataOnly)
|
|
312
|
+
await this.persist(appId, profileId, undefined, next, true);
|
|
313
|
+
return next;
|
|
284
314
|
}
|
|
285
315
|
if (!this.migrateLegacy || this.storageScope === undefined)
|
|
286
316
|
return undefined;
|
|
@@ -291,7 +321,8 @@ export class IndexedDbReadIndex {
|
|
|
291
321
|
const legacyRow = await this.profiles.get([appId, profileId]);
|
|
292
322
|
if (!legacyRow)
|
|
293
323
|
return undefined;
|
|
294
|
-
const
|
|
324
|
+
const hydrated = await this.hydrate(legacyRow, [appId, profileId]);
|
|
325
|
+
const legacy = this.metadataOnly ? readingMetadata(hydrated) : hydrated;
|
|
295
326
|
if (legacy.appId !== appId || legacy.profileId !== profileId || legacy.schemaVersion !== 1)
|
|
296
327
|
throw new Error("READ_IDENTITY_MISMATCH");
|
|
297
328
|
const ownership = { ...createReadIndex(appId, profileId), legacyOwner: this.storageScope };
|
|
@@ -380,10 +411,10 @@ export class IndexedDbReadIndex {
|
|
|
380
411
|
this.normalizedStates.add(next);
|
|
381
412
|
}
|
|
382
413
|
async load(appId, profileId) {
|
|
383
|
-
return this.transaction(this.migrateLegacy ? "rw" : "r", () => this.loadCurrent(appId, profileId));
|
|
414
|
+
return this.transaction(this.migrateLegacy || this.metadataOnly ? "rw" : "r", () => this.loadCurrent(appId, profileId));
|
|
384
415
|
}
|
|
385
416
|
async loadVersioned(appId, profileId) {
|
|
386
|
-
return this.transaction(this.migrateLegacy ? "rw" : "r", async () => ({
|
|
417
|
+
return this.transaction(this.migrateLegacy || this.metadataOnly ? "rw" : "r", async () => ({
|
|
387
418
|
state: await this.loadCurrent(appId, profileId), versions: (await this.versions.get(this.key(appId, profileId)))?.teams ?? {},
|
|
388
419
|
}));
|
|
389
420
|
}
|
|
@@ -407,7 +438,8 @@ export class IndexedDbReadIndex {
|
|
|
407
438
|
async update(appId, profileId, transform) {
|
|
408
439
|
return this.transaction("rw", async () => {
|
|
409
440
|
const previous = await this.loadCurrent(appId, profileId);
|
|
410
|
-
const
|
|
441
|
+
const transformed = transform(previous ?? createReadIndex(appId, profileId), (await this.versions.get(this.key(appId, profileId)))?.teams ?? {});
|
|
442
|
+
const next = this.metadataOnly ? readingMetadata(transformed) : transformed;
|
|
411
443
|
if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
|
|
412
444
|
throw new Error("READ_IDENTITY_MISMATCH");
|
|
413
445
|
await this.persist(appId, profileId, previous, next);
|
package/lib/runtime/readSync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertReadIndexVersions, applyReadChange, ReadSyncGap, createReadIndex, installReadSnapshot } from "./readIndex.js";
|
|
1
|
+
import { readingMetadata, 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;
|
|
@@ -9,8 +9,11 @@ export class ReadSyncClient {
|
|
|
9
9
|
this.options = options;
|
|
10
10
|
}
|
|
11
11
|
async loadLocal() {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const { appId, profileId, persistence, metadataOnly } = this.options;
|
|
13
|
+
const state = await persistence.load(appId, profileId) ?? createReadIndex(appId, profileId);
|
|
14
|
+
if (!metadataOnly || readingMetadata(state) === state)
|
|
15
|
+
return state;
|
|
16
|
+
return persistence.update(appId, profileId, readingMetadata);
|
|
14
17
|
}
|
|
15
18
|
stop() { this.controller.abort(); }
|
|
16
19
|
synchronize() {
|
|
@@ -84,7 +87,7 @@ export class ReadSyncClient {
|
|
|
84
87
|
throw new Error("READ_SYNC_STOPPED");
|
|
85
88
|
const next = await persistence.update(appId, profileId, (state, currentVersions) => {
|
|
86
89
|
assertReadIndexVersions(versions, currentVersions);
|
|
87
|
-
const applied = delta.changes.reduce(applyReadChange, state);
|
|
90
|
+
const applied = delta.changes.reduce((current, change) => applyReadChange(current, this.options.metadataOnly ? { ...change, thread: readingMetadata({ appId, threads: [change.thread] }).threads[0] } : change), state);
|
|
88
91
|
if (applied.syncCursor !== delta.syncCursor)
|
|
89
92
|
throw new ReadSyncGap(applied.syncCursor);
|
|
90
93
|
return applied;
|
|
@@ -107,7 +110,7 @@ export class ReadSyncClient {
|
|
|
107
110
|
// Merge into the latest transaction, including reads made while HTTP was pending.
|
|
108
111
|
const next = await persistence.update(appId, profileId, (state, currentVersions) => {
|
|
109
112
|
assertReadIndexVersions(versions, currentVersions);
|
|
110
|
-
return installReadSnapshot(state, snapshot);
|
|
113
|
+
return installReadSnapshot(state, this.options.metadataOnly ? readingMetadata(snapshot) : snapshot);
|
|
111
114
|
});
|
|
112
115
|
if (!next.pending.length)
|
|
113
116
|
this.readAfter = 0;
|
|
@@ -3,7 +3,7 @@ import { isOffline, isOfflineNetworkFailure } from "./connectivity.js";
|
|
|
3
3
|
import { recordConversationRead } from "./conversationRead.js";
|
|
4
4
|
import { BatchedReadIndex } from "./batchedReadIndex.js";
|
|
5
5
|
import { WindowedTasks } from "./coalescer.js";
|
|
6
|
-
import { IndexedDbReadIndex, recordReadIntent, assertReadIndexVersions } from "./readIndex.js";
|
|
6
|
+
import { IndexedDbReadIndex, readingMetadata, recordReadIntent, assertReadIndexVersions } from "./readIndex.js";
|
|
7
7
|
import { ReadSyncClient } from "./readSync.js";
|
|
8
8
|
/** Owns durable local reads, network coalescing, retries and state notifications.
|
|
9
9
|
* Hosts report actual visibility; loading or subscribing never marks messages read. */
|
|
@@ -30,7 +30,7 @@ export class ReadingClient {
|
|
|
30
30
|
this.options = options;
|
|
31
31
|
if (options.teamId !== undefined && !isValidChatTeamId(options.teamId))
|
|
32
32
|
throw new Error("INVALID_READ_TEAM");
|
|
33
|
-
const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope, options.migrateLegacy));
|
|
33
|
+
const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope, options.migrateLegacy, options.metadataOnly));
|
|
34
34
|
this.backing = storage;
|
|
35
35
|
this.persistence = new BatchedReadIndex(storage);
|
|
36
36
|
this.tasks = new WindowedTasks(1000, error => this.failed(error));
|
|
@@ -112,7 +112,8 @@ export class ReadingClient {
|
|
|
112
112
|
const expected = await versions;
|
|
113
113
|
const next = await this.persistence.update(this.options.appId, this.options.profileId, (state, currentVersions) => {
|
|
114
114
|
assertReadIndexVersions(expected, currentVersions, teamId);
|
|
115
|
-
|
|
115
|
+
const next = transform(state);
|
|
116
|
+
return this.options.metadataOnly ? readingMetadata(next) : next;
|
|
116
117
|
});
|
|
117
118
|
if (!this.closed) {
|
|
118
119
|
this.publish(next);
|
package/package.json
CHANGED