@pmate/chat-sdk 0.12.0-next.10 → 0.12.0-next.12
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type MessageReadersQuery } from "./messageReaders.js";
|
|
2
2
|
import { type MessageClientOptions, type MessageSnapshot } from "./messageClient.js";
|
|
3
3
|
import { DurableChatSender } from "./durableSender.js";
|
|
4
|
-
import { type ReadingClientOptions } from "./readingClient.js";
|
|
4
|
+
import { ReadingClient, type ReadingClientOptions } from "./readingClient.js";
|
|
5
5
|
import { ChatClient, type ChatClientOptions, type ChatConnectionState } from "./chatClient.js";
|
|
6
6
|
import { unreadInConversation } from "./readIndex.js";
|
|
7
7
|
import type { StoredMessage } from "./messageStore.js";
|
|
@@ -20,7 +20,7 @@ export type ChatThreadSnapshot<T> = {
|
|
|
20
20
|
hasEarlier: boolean;
|
|
21
21
|
resetVersion: number;
|
|
22
22
|
complete: boolean;
|
|
23
|
-
unread: ReturnType<typeof unreadInConversation
|
|
23
|
+
unread: ReturnType<typeof unreadInConversation> | undefined;
|
|
24
24
|
};
|
|
25
25
|
export type ChatSummarySnapshot<T> = {
|
|
26
26
|
threads: readonly {
|
|
@@ -29,8 +29,8 @@ export type ChatSummarySnapshot<T> = {
|
|
|
29
29
|
unread: ChatThreadSnapshot<T>["unread"];
|
|
30
30
|
outgoingCount: number;
|
|
31
31
|
}[];
|
|
32
|
-
unreadMessageCount: number;
|
|
33
|
-
badgeCount: number;
|
|
32
|
+
unreadMessageCount: number | undefined;
|
|
33
|
+
badgeCount: number | undefined;
|
|
34
34
|
connection: ChatConnectionState;
|
|
35
35
|
};
|
|
36
36
|
/** Owns all browser message clients. Hosts subscribe to stable thread snapshots. */
|
|
@@ -53,9 +53,18 @@ export declare class ChatSession<T extends StoredMessage> {
|
|
|
53
53
|
private readingStarted;
|
|
54
54
|
private closed;
|
|
55
55
|
private starting?;
|
|
56
|
+
private initializing?;
|
|
56
57
|
private closing?;
|
|
57
58
|
constructor(options: ChatSessionOptions<T>);
|
|
58
59
|
start(): Promise<void>;
|
|
60
|
+
/** Initializes local state and background reading sync without opening a
|
|
61
|
+
* second transport when the host already owns calls/presence. */
|
|
62
|
+
initialize(): Promise<void>;
|
|
63
|
+
getReadingSnapshot(): import("./readIndex.js").ReadIndexState | undefined;
|
|
64
|
+
subscribeReading(listener: () => void): () => void;
|
|
65
|
+
refreshReading(): void;
|
|
66
|
+
synchronizeReading(cursor?: string, profileId?: string): Promise<import("./readIndex.js").ReadIndexState>;
|
|
67
|
+
updateReading(transform: Parameters<ReadingClient["update"]>[0]): Promise<import("./readIndex.js").ReadIndexState>;
|
|
59
68
|
private assertOpen;
|
|
60
69
|
getMessages(threadId: string, options?: {
|
|
61
70
|
limit?: number;
|
|
@@ -25,6 +25,7 @@ export class ChatSession {
|
|
|
25
25
|
readingStarted = false;
|
|
26
26
|
closed = false;
|
|
27
27
|
starting;
|
|
28
|
+
initializing;
|
|
28
29
|
closing;
|
|
29
30
|
constructor(options) {
|
|
30
31
|
this.options = options;
|
|
@@ -38,7 +39,7 @@ export class ChatSession {
|
|
|
38
39
|
}
|
|
39
40
|
this.messages = new MessageClient(options.messages);
|
|
40
41
|
this.connection = options.sharedConnection ?? new ChatClient(options.connection);
|
|
41
|
-
this.reading = new ReadingClient({ ...options.reading, storageScope: JSON.stringify([scope.accountId
|
|
42
|
+
this.reading = new ReadingClient({ ...options.reading, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true });
|
|
42
43
|
this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
|
|
43
44
|
this.sender = new DurableChatSender(this.connection, scope, options.onError);
|
|
44
45
|
this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
|
|
@@ -72,13 +73,28 @@ export class ChatSession {
|
|
|
72
73
|
start() {
|
|
73
74
|
this.assertOpen();
|
|
74
75
|
return this.starting ??= (async () => {
|
|
76
|
+
await this.initialize();
|
|
77
|
+
this.assertOpen();
|
|
78
|
+
await this.connection.connect();
|
|
79
|
+
})().catch(error => { this.starting = undefined; throw error; });
|
|
80
|
+
}
|
|
81
|
+
/** Initializes local state and background reading sync without opening a
|
|
82
|
+
* second transport when the host already owns calls/presence. */
|
|
83
|
+
initialize() {
|
|
84
|
+
this.assertOpen();
|
|
85
|
+
return this.initializing ??= (async () => {
|
|
75
86
|
await this.sender.initialize();
|
|
87
|
+
this.assertOpen();
|
|
76
88
|
this.readingStarted = true;
|
|
77
89
|
await this.reading.start();
|
|
78
90
|
this.assertOpen();
|
|
79
|
-
|
|
80
|
-
})().catch(error => { this.starting = undefined; throw error; });
|
|
91
|
+
})().catch(error => { this.initializing = undefined; throw error; });
|
|
81
92
|
}
|
|
93
|
+
getReadingSnapshot() { return this.reading.getSnapshot(); }
|
|
94
|
+
subscribeReading(listener) { this.assertOpen(); return this.reading.subscribe(listener); }
|
|
95
|
+
refreshReading() { this.assertOpen(); this.reading.refresh(); }
|
|
96
|
+
synchronizeReading(cursor, profileId) { this.assertOpen(); return this.reading.synchronize(cursor, profileId); }
|
|
97
|
+
updateReading(transform) { this.assertOpen(); return this.reading.update(transform); }
|
|
82
98
|
assertOpen() { if (this.closed)
|
|
83
99
|
throw new Error("CHAT_SESSION_CLOSED"); }
|
|
84
100
|
async getMessages(threadId, options = {}) {
|
|
@@ -191,7 +207,7 @@ export class ChatSession {
|
|
|
191
207
|
if (!snapshot) {
|
|
192
208
|
const messages = this.messages.getSnapshot(threadId);
|
|
193
209
|
const state = this.reading.getSnapshot();
|
|
194
|
-
snapshot = { messages: messages?.messages ?? [], hasEarlier: messages?.hasEarlier ?? false, resetVersion: messages?.resetVersion ?? 0, complete: messages?.complete ?? false, outgoing: this.sender.snapshot(threadId), unread: state?.threads.some(thread => (thread.conversationId ?? thread.id) === threadId) ? unreadInConversation(state, threadId) :
|
|
210
|
+
snapshot = { messages: messages?.messages ?? [], hasEarlier: messages?.hasEarlier ?? false, resetVersion: messages?.resetVersion ?? 0, complete: messages?.complete ?? false, outgoing: this.sender.snapshot(threadId), unread: state?.threads.some(thread => (thread.conversationId ?? thread.id) === threadId) ? unreadInConversation(state, threadId) : undefined };
|
|
195
211
|
this.snapshots.set(threadId, snapshot);
|
|
196
212
|
}
|
|
197
213
|
return snapshot;
|
|
@@ -234,7 +250,9 @@ export class ChatSession {
|
|
|
234
250
|
const snapshot = this.getSnapshot(threadId);
|
|
235
251
|
return { threadId, lastMessage: snapshot.messages.at(-1), unread: snapshot.unread, outgoingCount: snapshot.outgoing.length };
|
|
236
252
|
});
|
|
237
|
-
|
|
253
|
+
const state = this.reading.getSnapshot();
|
|
254
|
+
const known = Boolean(state && state.syncCursor !== "0" && threads.every(thread => thread.unread !== undefined));
|
|
255
|
+
return { threads, unreadMessageCount: known ? threads.reduce((sum, t) => sum + (t.unread?.unreadMessageCount ?? 0), 0) : undefined, badgeCount: known ? threads.reduce((sum, t) => sum + (t.unread?.badgeCount ?? 0), 0) : undefined, connection: this.connectionState };
|
|
238
256
|
}
|
|
239
257
|
publishGlobal() {
|
|
240
258
|
const next = this.buildGlobal();
|
|
@@ -85,12 +85,14 @@ export interface ReadIndexPersistence {
|
|
|
85
85
|
/** Dexie preserves the legacy database/store and outbound [appId, profileId] keys. */
|
|
86
86
|
export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
87
87
|
private storageScope?;
|
|
88
|
+
private migrateLegacy;
|
|
88
89
|
private database;
|
|
89
90
|
private profiles;
|
|
90
91
|
private closed;
|
|
91
|
-
constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined);
|
|
92
|
+
constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean);
|
|
92
93
|
private transaction;
|
|
93
94
|
private key;
|
|
95
|
+
private loadCurrent;
|
|
94
96
|
load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
|
|
95
97
|
update(appId: string, profileId: string, transform: (current: ReadIndexState) => ReadIndexState): Promise<ReadIndexState>;
|
|
96
98
|
close(): Promise<void>;
|
package/lib/runtime/readIndex.js
CHANGED
|
@@ -167,11 +167,13 @@ export function unreadInThread(state, threadId) {
|
|
|
167
167
|
/** Dexie preserves the legacy database/store and outbound [appId, profileId] keys. */
|
|
168
168
|
export class IndexedDbReadIndex {
|
|
169
169
|
storageScope;
|
|
170
|
+
migrateLegacy;
|
|
170
171
|
database;
|
|
171
172
|
profiles;
|
|
172
173
|
closed = false;
|
|
173
|
-
constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope) {
|
|
174
|
+
constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false) {
|
|
174
175
|
this.storageScope = storageScope;
|
|
176
|
+
this.migrateLegacy = migrateLegacy;
|
|
175
177
|
if (!factory)
|
|
176
178
|
throw new Error("READ_STORAGE_UNAVAILABLE");
|
|
177
179
|
this.database = new Dexie(name, { indexedDB: factory, IDBKeyRange: globalThis.IDBKeyRange });
|
|
@@ -205,12 +207,31 @@ export class IndexedDbReadIndex {
|
|
|
205
207
|
throw new Error("READ_STORAGE_UNAVAILABLE");
|
|
206
208
|
}
|
|
207
209
|
key(appId, profileId) { return this.storageScope === undefined ? [appId, profileId] : [appId, profileId, this.storageScope]; }
|
|
210
|
+
async loadCurrent(appId, profileId) {
|
|
211
|
+
const current = await this.profiles.get(this.key(appId, profileId));
|
|
212
|
+
if (current || !this.migrateLegacy || this.storageScope === undefined)
|
|
213
|
+
return current;
|
|
214
|
+
const markerKey = [appId, profileId, "legacy-owner-v1"];
|
|
215
|
+
const marker = await this.profiles.get(markerKey);
|
|
216
|
+
if (marker && marker.legacyOwner !== this.storageScope)
|
|
217
|
+
return undefined;
|
|
218
|
+
const legacy = await this.profiles.get([appId, profileId]);
|
|
219
|
+
if (!legacy)
|
|
220
|
+
return undefined;
|
|
221
|
+
if (legacy.appId !== appId || legacy.profileId !== profileId || legacy.schemaVersion !== 1)
|
|
222
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
223
|
+
const ownership = { ...createReadIndex(appId, profileId), legacyOwner: this.storageScope };
|
|
224
|
+
await this.profiles.put(ownership, markerKey);
|
|
225
|
+
await this.profiles.put(legacy, this.key(appId, profileId));
|
|
226
|
+
await this.profiles.delete([appId, profileId]);
|
|
227
|
+
return legacy;
|
|
228
|
+
}
|
|
208
229
|
async load(appId, profileId) {
|
|
209
|
-
return this.transaction("r", () => this.
|
|
230
|
+
return this.transaction(this.migrateLegacy ? "rw" : "r", () => this.loadCurrent(appId, profileId));
|
|
210
231
|
}
|
|
211
232
|
async update(appId, profileId, transform) {
|
|
212
233
|
return this.transaction("rw", async () => {
|
|
213
|
-
const previous = await this.
|
|
234
|
+
const previous = await this.loadCurrent(appId, profileId);
|
|
214
235
|
const next = transform(previous ?? createReadIndex(appId, profileId));
|
|
215
236
|
if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
|
|
216
237
|
throw new Error("READ_IDENTITY_MISMATCH");
|
|
@@ -19,7 +19,7 @@ export class ReadingClient {
|
|
|
19
19
|
generation = 0;
|
|
20
20
|
constructor(options) {
|
|
21
21
|
this.options = options;
|
|
22
|
-
const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope));
|
|
22
|
+
const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope, options.migrateLegacy));
|
|
23
23
|
this.persistence = new BatchedReadIndex(storage);
|
|
24
24
|
this.tasks = new WindowedTasks(1000, error => this.failed(error));
|
|
25
25
|
this.sync = new ReadSyncClient({ ...options, persistence: this.persistence, onChange: state => this.publish(state) });
|
package/package.json
CHANGED