@pmate/chat-sdk 0.11.4 → 0.11.5

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.
@@ -82,14 +82,12 @@ export interface ReadIndexPersistence {
82
82
  /** Transform and persist atomically; UI publishes only the committed result. */
83
83
  update(appId: string, profileId: string, transform: (current: ReadIndexState) => ReadIndexState): Promise<ReadIndexState>;
84
84
  }
85
- /** IndexedDB transactions serialize tabs, and retain offline outbox across process restarts. */
85
+ /** Dexie preserves the legacy database/store and outbound [appId, profileId] keys. */
86
86
  export declare class IndexedDbReadIndex implements ReadIndexPersistence {
87
- private factory;
88
- private name;
89
- private database?;
87
+ private database;
88
+ private profiles;
90
89
  private closed;
91
90
  constructor(factory?: IDBFactory, name?: string);
92
- private getDatabase;
93
91
  private transaction;
94
92
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
95
93
  update(appId: string, profileId: string, transform: (current: ReadIndexState) => ReadIndexState): Promise<ReadIndexState>;
@@ -1,3 +1,4 @@
1
+ import { Dexie } from "dexie";
1
2
  import { isValidChatTeamId } from "./thread.js";
2
3
  export class ReadSyncGap extends Error {
3
4
  cursor;
@@ -162,107 +163,57 @@ export function unreadInThread(state, threadId) {
162
163
  const unreadMentionCount = messages.filter(message => message.mentions.includes(state.profileId)).length;
163
164
  return { unreadMessageCount, unreadMentionCount, badgeCount: thread.kind === "group" ? unreadMentionCount : unreadMessageCount };
164
165
  }
165
- /** IndexedDB transactions serialize tabs, and retain offline outbox across process restarts. */
166
+ /** Dexie preserves the legacy database/store and outbound [appId, profileId] keys. */
166
167
  export class IndexedDbReadIndex {
167
- factory;
168
- name;
169
168
  database;
169
+ profiles;
170
170
  closed = false;
171
171
  constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index") {
172
- this.factory = factory;
173
- this.name = name;
174
172
  if (!factory)
175
173
  throw new Error("READ_STORAGE_UNAVAILABLE");
176
- }
177
- getDatabase() {
178
- if (this.closed)
179
- return Promise.reject(new Error("READ_STORAGE_CLOSED"));
180
- if (this.database)
181
- return this.database;
182
- let abandoned = false;
183
- const pending = new Promise((resolve, reject) => {
184
- const request = this.factory.open(this.name);
185
- request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains("profiles"))
186
- request.result.createObjectStore("profiles"); };
187
- request.onsuccess = () => {
188
- const db = request.result;
189
- if (abandoned || this.closed) {
190
- db.close();
191
- reject(new Error("READ_STORAGE_CLOSED"));
192
- return;
193
- }
194
- const invalidate = () => { if (this.database === pending)
195
- this.database = undefined; };
196
- db.onversionchange = () => { db.close(); invalidate(); };
197
- db.onclose = invalidate;
198
- resolve(db);
199
- };
200
- request.onerror = () => { abandoned = true; reject(request.error); };
201
- request.onblocked = () => { abandoned = true; reject(new Error("READ_STORAGE_BLOCKED")); };
202
- });
203
- this.database = pending;
204
- void pending.catch(() => { if (this.database === pending)
205
- this.database = undefined; });
206
- return pending;
174
+ this.database = new Dexie(name, { indexedDB: factory, IDBKeyRange: globalThis.IDBKeyRange });
175
+ // The empty schema retains the legacy out-of-line primary key and records.
176
+ this.database.version(1).stores({ profiles: "" });
177
+ this.profiles = this.database.table("profiles");
178
+ this.database.on("versionchange", () => { this.database.close({ disableAutoOpen: false }); return false; });
207
179
  }
208
180
  async transaction(mode, execute) {
209
181
  for (let attempt = 0; attempt < 2; attempt++) {
210
- const pending = this.getDatabase(), db = await pending;
211
182
  if (this.closed)
212
183
  throw new Error("READ_STORAGE_CLOSED");
213
- let transaction;
184
+ let started = false;
214
185
  try {
215
- transaction = db.transaction("profiles", mode);
186
+ return await this.database.transaction(mode, this.profiles, async () => {
187
+ if (this.closed)
188
+ throw new Error("READ_STORAGE_CLOSED");
189
+ started = true;
190
+ return execute();
191
+ });
216
192
  }
217
193
  catch (error) {
218
- if (attempt || error?.name !== "InvalidStateError")
194
+ if (this.closed)
195
+ throw new Error("READ_STORAGE_CLOSED");
196
+ // Retry only failed transaction creation, never a transform or write.
197
+ if (attempt || started || this.closed || error?.name !== "InvalidStateError")
219
198
  throw error;
220
- // Retry only transaction creation: no transform or write has run yet.
221
- db.close();
222
- if (this.database === pending)
223
- this.database = undefined;
224
- continue;
199
+ this.database.close({ disableAutoOpen: false });
225
200
  }
226
- // Start requests synchronously after creation, including on WebKit.
227
- return execute(transaction);
228
201
  }
229
202
  throw new Error("READ_STORAGE_UNAVAILABLE");
230
203
  }
231
204
  async load(appId, profileId) {
232
- return this.transaction("readonly", transaction => new Promise((resolve, reject) => {
233
- const request = transaction.objectStore("profiles").get([appId, profileId]);
234
- transaction.oncomplete = () => resolve(request.result);
235
- transaction.onerror = () => reject(transaction.error);
236
- transaction.onabort = () => reject(transaction.error ?? new Error("READ_STORAGE_ABORTED"));
237
- }));
205
+ return this.transaction("r", () => this.profiles.get([appId, profileId]));
238
206
  }
239
207
  async update(appId, profileId, transform) {
240
- return this.transaction("readwrite", transaction => new Promise((resolve, reject) => {
241
- const store = transaction.objectStore("profiles");
242
- const request = store.get([appId, profileId]);
243
- let next, failure;
244
- request.onsuccess = () => {
245
- try {
246
- next = transform(request.result ?? createReadIndex(appId, profileId));
247
- if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
248
- throw new Error("READ_IDENTITY_MISMATCH");
249
- store.put(next, [appId, profileId]);
250
- }
251
- catch (error) {
252
- failure = error;
253
- transaction.abort();
254
- }
255
- };
256
- transaction.oncomplete = () => resolve(next);
257
- transaction.onerror = () => reject(failure ?? transaction.error);
258
- transaction.onabort = () => reject(failure ?? transaction.error ?? new Error("READ_STORAGE_ABORTED"));
259
- }));
208
+ return this.transaction("rw", async () => {
209
+ const next = transform(await this.profiles.get([appId, profileId]) ?? createReadIndex(appId, profileId));
210
+ if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
211
+ throw new Error("READ_IDENTITY_MISMATCH");
212
+ await this.profiles.put(next, [appId, profileId]);
213
+ return next;
214
+ });
260
215
  }
261
- async close() { this.closed = true; const pending = this.database; this.database = undefined; if (pending) {
262
- const db = await pending.catch(error => { if (error?.message !== "READ_STORAGE_CLOSED")
263
- throw error; return undefined; });
264
- db?.close();
265
- } }
216
+ async close() { this.closed = true; this.database.close(); }
266
217
  }
267
218
  export function sourcesInConversation(state, conversationId) {
268
219
  return state.threads.filter(thread => (thread.conversationId ?? thread.id) === conversationId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
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",
@@ -28,11 +28,21 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
+ "scripts": {
32
+ "clean": "rimraf lib",
33
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
34
+ "prepack": "pnpm run build",
35
+ "npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
36
+ "test": "vitest run",
37
+ "storybook": "storybook dev -p 6009",
38
+ "build-storybook": "storybook build --output-dir dist"
39
+ },
31
40
  "dependencies": {
32
41
  "@emoji-mart/data": "^1.2.1",
33
42
  "@emoji-mart/react": "^1.1.1",
34
43
  "@msgpack/msgpack": "^3.1.2",
35
44
  "@noble/hashes": "^2.4.0",
45
+ "dexie": "^4.0.4",
36
46
  "jotai": "^2.16.1"
37
47
  },
38
48
  "peerDependencies": {
@@ -42,24 +52,16 @@
42
52
  "devDependencies": {
43
53
  "@storybook/addon-docs": "^9.0.17",
44
54
  "@storybook/react-vite": "9.0.17",
45
- "@tailwindcss/vite": "^4.1.3",
46
- "@types/react": "^19.2.7",
47
- "@types/react-dom": "^19.2.3",
55
+ "@tailwindcss/vite": "catalog:",
56
+ "@types/react": "catalog:",
57
+ "@types/react-dom": "catalog:",
48
58
  "fake-indexeddb": "^6.2.5",
49
- "react": "^19.1.1",
50
- "react-dom": "^19.1.1",
59
+ "react": "catalog:",
60
+ "react-dom": "catalog:",
51
61
  "rimraf": "^6.1.2",
52
62
  "storybook": "^9.0.17",
53
- "tailwindcss": "^4.0.14",
63
+ "tailwindcss": "catalog:",
54
64
  "typescript": "5.9.2",
55
65
  "vitest": "^4.0.17"
56
- },
57
- "scripts": {
58
- "clean": "rimraf lib",
59
- "build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
60
- "npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
61
- "test": "vitest run",
62
- "storybook": "storybook dev -p 6009",
63
- "build-storybook": "storybook build --output-dir dist"
64
66
  }
65
- }
67
+ }