@pmate/chat-sdk 0.11.1 → 0.11.3

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.
@@ -84,8 +84,13 @@ export interface ReadIndexPersistence {
84
84
  }
85
85
  /** IndexedDB transactions serialize tabs, and retain offline outbox across process restarts. */
86
86
  export declare class IndexedDbReadIndex implements ReadIndexPersistence {
87
- private readonly database;
87
+ private factory;
88
+ private name;
89
+ private database?;
90
+ private closed;
88
91
  constructor(factory?: IDBFactory, name?: string);
92
+ private getDatabase;
93
+ private transaction;
89
94
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
90
95
  update(appId: string, profileId: string, transform: (current: ReadIndexState) => ReadIndexState): Promise<ReadIndexState>;
91
96
  close(): Promise<void>;
@@ -164,32 +164,80 @@ export function unreadInThread(state, threadId) {
164
164
  }
165
165
  /** IndexedDB transactions serialize tabs, and retain offline outbox across process restarts. */
166
166
  export class IndexedDbReadIndex {
167
+ factory;
168
+ name;
167
169
  database;
170
+ closed = false;
168
171
  constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index") {
172
+ this.factory = factory;
173
+ this.name = name;
169
174
  if (!factory)
170
175
  throw new Error("READ_STORAGE_UNAVAILABLE");
171
- this.database = new Promise((resolve, reject) => {
172
- const request = factory.open(name, 1);
173
- request.onupgradeneeded = () => { request.result.createObjectStore("profiles"); };
174
- request.onsuccess = () => { request.result.onversionchange = () => request.result.close(); resolve(request.result); };
175
- request.onerror = () => reject(request.error);
176
- request.onblocked = () => reject(new Error("READ_STORAGE_BLOCKED"));
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")); };
177
202
  });
203
+ this.database = pending;
204
+ void pending.catch(() => { if (this.database === pending)
205
+ this.database = undefined; });
206
+ return pending;
207
+ }
208
+ async transaction(mode, execute) {
209
+ for (let attempt = 0; attempt < 2; attempt++) {
210
+ const pending = this.getDatabase(), db = await pending;
211
+ if (this.closed)
212
+ throw new Error("READ_STORAGE_CLOSED");
213
+ let transaction;
214
+ try {
215
+ transaction = db.transaction("profiles", mode);
216
+ }
217
+ catch (error) {
218
+ if (attempt || error?.name !== "InvalidStateError")
219
+ 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;
225
+ }
226
+ // Start requests synchronously after creation, including on WebKit.
227
+ return execute(transaction);
228
+ }
229
+ throw new Error("READ_STORAGE_UNAVAILABLE");
178
230
  }
179
231
  async load(appId, profileId) {
180
- const database = await this.database;
181
- return new Promise((resolve, reject) => {
182
- const transaction = database.transaction("profiles", "readonly");
232
+ return this.transaction("readonly", transaction => new Promise((resolve, reject) => {
183
233
  const request = transaction.objectStore("profiles").get([appId, profileId]);
184
234
  transaction.oncomplete = () => resolve(request.result);
185
235
  transaction.onerror = () => reject(transaction.error);
186
236
  transaction.onabort = () => reject(transaction.error ?? new Error("READ_STORAGE_ABORTED"));
187
- });
237
+ }));
188
238
  }
189
239
  async update(appId, profileId, transform) {
190
- const database = await this.database;
191
- return new Promise((resolve, reject) => {
192
- const transaction = database.transaction("profiles", "readwrite");
240
+ return this.transaction("readwrite", transaction => new Promise((resolve, reject) => {
193
241
  const store = transaction.objectStore("profiles");
194
242
  const request = store.get([appId, profileId]);
195
243
  let next, failure;
@@ -208,9 +256,13 @@ export class IndexedDbReadIndex {
208
256
  transaction.oncomplete = () => resolve(next);
209
257
  transaction.onerror = () => reject(failure ?? transaction.error);
210
258
  transaction.onabort = () => reject(failure ?? transaction.error ?? new Error("READ_STORAGE_ABORTED"));
211
- });
259
+ }));
212
260
  }
213
- async close() { (await this.database).close(); }
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
+ } }
214
266
  }
215
267
  export function sourcesInConversation(state, conversationId) {
216
268
  return state.threads.filter(thread => (thread.conversationId ?? thread.id) === conversationId);
@@ -1,3 +1,4 @@
1
+ import type { SocialGroup } from "./socialReadClient.js";
1
2
  export type TeamAssignment = {
2
3
  appId: string;
3
4
  sourceThreadId: string;
@@ -6,6 +7,8 @@ export type TeamAssignment = {
6
7
  actorId: string;
7
8
  assignmentId: string;
8
9
  createdAt: number;
10
+ groupId?: string;
11
+ groupRevision?: string;
9
12
  };
10
13
  export type TeamMigrationPlan = {
11
14
  status: "assigned" | "noHistory" | "noCommonTeam" | "ready" | "needsSelection";
@@ -17,6 +20,12 @@ export type TeamMigrationPlan = {
17
20
  messageCount?: number;
18
21
  assignment?: TeamAssignment;
19
22
  };
23
+ export type GroupTeamMigrationPlan = Omit<TeamMigrationPlan, "status"> & {
24
+ status: "assigned" | "noCommonTeam" | "ready" | "needsSelection";
25
+ group: SocialGroup;
26
+ canAssign: boolean;
27
+ legacyWritesClosed: boolean;
28
+ };
20
29
  export type TeamMigrationResult = {
21
30
  status: "assigned";
22
31
  assignment: TeamAssignment;
@@ -51,6 +60,9 @@ export declare class TeamMigrationClient {
51
60
  });
52
61
  plan(peerId: string): Promise<TeamMigrationPlan>;
53
62
  assign(peerId: string, teamId: string): Promise<TeamMigrationResult>;
63
+ planGroup(groupId: string): Promise<GroupTeamMigrationPlan>;
64
+ /** Only the current owner can migrate; expectedRevision fences roster changes. */
65
+ assignGroup(groupId: string, teamId: string, expectedRevision: string): Promise<TeamMigrationResult>;
54
66
  reconcileDelivery(input: {
55
67
  peerId: string;
56
68
  teamId: string;
@@ -20,6 +20,14 @@ export class TeamMigrationClient {
20
20
  async assign(peerId, teamId) {
21
21
  return this.request("", { method: "POST", body: JSON.stringify({ appId: this.options.appId, profileId: this.options.profileId, peerId, teamId }) });
22
22
  }
23
+ async planGroup(groupId) {
24
+ const query = new URLSearchParams({ appId: this.options.appId, profileId: this.options.profileId, groupId });
25
+ return this.request(`/groups/plan?${query}`);
26
+ }
27
+ /** Only the current owner can migrate; expectedRevision fences roster changes. */
28
+ async assignGroup(groupId, teamId, expectedRevision) {
29
+ return this.request("/groups", { method: "POST", body: JSON.stringify({ appId: this.options.appId, profileId: this.options.profileId, groupId, teamId, expectedRevision }) });
30
+ }
23
31
  async reconcileDelivery(input) {
24
32
  const query = new URLSearchParams({ ...input, appId: this.options.appId, profileId: this.options.profileId });
25
33
  return this.request(`/delivery?${query}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
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,6 +28,15 @@
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",
@@ -42,24 +51,16 @@
42
51
  "devDependencies": {
43
52
  "@storybook/addon-docs": "^9.0.17",
44
53
  "@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",
54
+ "@tailwindcss/vite": "catalog:",
55
+ "@types/react": "catalog:",
56
+ "@types/react-dom": "catalog:",
48
57
  "fake-indexeddb": "^6.2.5",
49
- "react": "^19.1.1",
50
- "react-dom": "^19.1.1",
58
+ "react": "catalog:",
59
+ "react-dom": "catalog:",
51
60
  "rimraf": "^6.1.2",
52
61
  "storybook": "^9.0.17",
53
- "tailwindcss": "^4.0.14",
62
+ "tailwindcss": "catalog:",
54
63
  "typescript": "5.9.2",
55
64
  "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
65
  }
65
- }
66
+ }