@pmate/chat-sdk 0.12.0-next.10 → 0.12.0-next.11

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";
@@ -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, scope.teamId]) });
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
- await this.connection.connect();
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 = {}) {
@@ -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>;
@@ -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.profiles.get(this.key(appId, profileId)));
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.profiles.get(this.key(appId, profileId));
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");
@@ -6,6 +6,7 @@ export type ReadingClientOptions = {
6
6
  hubHttpUrl: string;
7
7
  persistence?: ReadIndexPersistence;
8
8
  storageScope?: string;
9
+ migrateLegacy?: boolean;
9
10
  fetch?: typeof fetch;
10
11
  onError?: (error: unknown) => void;
11
12
  onRecovered?: () => void;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.10",
3
+ "version": "0.12.0-next.11",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",