@pmate/chat-sdk 0.12.0-next.33 → 0.12.0-next.35

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.
@@ -261,7 +261,8 @@ export class ChatSession {
261
261
  return { ...recent, messages: [], hasEarlier: false };
262
262
  if (!edge.before)
263
263
  throw new Error("MESSAGE_SYNC_NO_PROGRESS");
264
- const page = await this.getMessages(threadId, { before: edge.before });
264
+ const page = await this.messages.earlier(threadId, edge.before, 100, true);
265
+ await this.sender.confirm(page.messages.map(message => message.id));
265
266
  this.assertOpen();
266
267
  this.rememberHistoryEdge(threadId, page);
267
268
  return page;
@@ -36,6 +36,7 @@ export declare class MessageClient<T extends StoredMessage> {
36
36
  private options;
37
37
  private storage;
38
38
  private watches;
39
+ private windowEdges;
39
40
  private accessListeners;
40
41
  private allListeners;
41
42
  private reads;
@@ -93,7 +94,7 @@ export declare class MessageClient<T extends StoredMessage> {
93
94
  earlier(thread: string, before: {
94
95
  timestamp: number;
95
96
  id: string;
96
- }, limit?: number): Promise<MessageSnapshot<T>>;
97
+ }, limit?: number, expand?: boolean): Promise<MessageSnapshot<T>>;
97
98
  private fillWindow;
98
99
  private denyView;
99
100
  private denyScope;
@@ -6,6 +6,7 @@ export class MessageClient {
6
6
  options;
7
7
  storage;
8
8
  watches = new Map();
9
+ windowEdges = new Map();
9
10
  accessListeners = new Set();
10
11
  allListeners = new Set();
11
12
  reads = new Map();
@@ -252,7 +253,27 @@ export class MessageClient {
252
253
  async reload(thread, fallback = false) {
253
254
  await this.ensureScope();
254
255
  const limit = 100;
255
- const page = await (fallback ? this.storage.readFallback(thread, limit) : this.storage.read(thread, limit));
256
+ let page = await (fallback ? this.storage.readFallback(thread, limit) : this.storage.read(thread, limit));
257
+ const edge = this.windowEdges.get(thread);
258
+ if (edge && edge.version !== (page.coverage?.resetVersion ?? 0))
259
+ this.windowEdges.delete(thread);
260
+ if (edge && edge.version === (page.coverage?.resetVersion ?? 0)) {
261
+ const pages = [page.messages];
262
+ const beforeEdge = (message) => message.timestamp < edge.timestamp || message.timestamp === edge.timestamp && message.id < edge.id;
263
+ let oldest = page.messages[0];
264
+ while (oldest && !beforeEdge(oldest) && (oldest.timestamp !== edge.timestamp || oldest.id !== edge.id) && page.hasEarlier) {
265
+ const next = await (fallback ? this.storage.readFallback(thread, limit, { timestamp: oldest.timestamp, id: oldest.id }) : this.storage.read(thread, limit, { timestamp: oldest.timestamp, id: oldest.id }));
266
+ if ((next.coverage?.resetVersion ?? 0) !== edge.version || (next.coverage?.scopeVersion ?? 0) !== (page.coverage?.scopeVersion ?? 0)) {
267
+ this.windowEdges.delete(thread);
268
+ return this.reload(thread, fallback);
269
+ }
270
+ pages.push(next.messages);
271
+ page = next;
272
+ oldest = next.messages[0];
273
+ }
274
+ const messages = pages.reverse().flat();
275
+ page = { ...page, messages: messages.filter(message => !beforeEdge(message)), hasEarlier: page.hasEarlier || messages.some(beforeEdge) };
276
+ }
256
277
  if (this.closed)
257
278
  throw new Error("MESSAGE_CLIENT_CLOSED");
258
279
  if ((page.coverage?.scopeVersion ?? 0) !== this.scopeVersion) {
@@ -464,7 +485,7 @@ export class MessageClient {
464
485
  this.syncing.set(thread, task);
465
486
  return task;
466
487
  }
467
- async earlier(thread, before, limit = 100) {
488
+ async earlier(thread, before, limit = 100, expand = false) {
468
489
  await this.ensureScope();
469
490
  if (this.scopeDenied)
470
491
  throw new Error("MESSAGE_ACCESS_REVOKED");
@@ -478,7 +499,14 @@ export class MessageClient {
478
499
  throw new Error("MESSAGE_OFFLINE_UNAVAILABLE");
479
500
  await this.synchronize(thread);
480
501
  }
481
- return await this.fillWindow(thread, limit, before);
502
+ const page = await this.fillWindow(thread, limit, before);
503
+ if (expand && page.complete && page.messages.length) {
504
+ const first = page.messages[0], old = this.windowEdges.get(thread);
505
+ if (!old || old.version !== page.resetVersion || first.timestamp < old.timestamp || first.timestamp === old.timestamp && first.id < old.id)
506
+ this.windowEdges.set(thread, { timestamp: first.timestamp, id: first.id, version: page.resetVersion });
507
+ await this.reload(thread);
508
+ }
509
+ return page;
482
510
  }
483
511
  catch (error) {
484
512
  if (String(error).includes("MESSAGE_SCOPE_CHANGED")) {
@@ -538,6 +566,7 @@ export class MessageClient {
538
566
  return { messages: page.messages, hasEarlier: page.hasEarlier || Boolean(page.coverage?.nextBefore), resetVersion: page.coverage?.resetVersion ?? 0, complete: page.coverage?.complete ?? false };
539
567
  }
540
568
  denyView(thread, version) {
569
+ this.windowEdges.delete(thread);
541
570
  this.revoked.add(thread);
542
571
  const watch = this.watches.get(thread);
543
572
  const snapshot = { messages: [], hasEarlier: false, complete: false, resetVersion: Math.max(watch?.snapshot.resetVersion ?? 0, version ?? (watch?.snapshot.resetVersion ?? 0) + 1) };
@@ -619,6 +648,7 @@ export class MessageClient {
619
648
  this.abort.abort();
620
649
  this.channel?.close();
621
650
  this.watches.clear();
651
+ this.windowEdges.clear();
622
652
  this.allListeners.clear();
623
653
  this.accessListeners.clear();
624
654
  this.storage.close();
@@ -0,0 +1,19 @@
1
+ import type { StoredMessage, MessageScope } from "./messageStore.js";
2
+ export type MessageControl = {
3
+ scope: string;
4
+ thread: string;
5
+ target: string;
6
+ kind: "recall" | "reaction";
7
+ actor: string;
8
+ emoji: string;
9
+ id: string;
10
+ timestamp: number;
11
+ revision: number;
12
+ active: boolean;
13
+ };
14
+ export type ControlKey = [string, string, string, string, string, string];
15
+ export declare const controlKey: (event: MessageControl) => ControlKey;
16
+ export declare function newerControl(old: MessageControl | undefined, next: MessageControl): MessageControl;
17
+ /** Body-free proofs retained independently of the visible history window. */
18
+ export declare function messageControls(message: StoredMessage, scope: MessageScope, key: string): MessageControl[];
19
+ export declare function applyMessageControls<T extends StoredMessage>(messages: T[], controls: readonly MessageControl[]): T[];
@@ -0,0 +1,44 @@
1
+ export const controlKey = (event) => [event.scope, event.thread, event.target, event.kind, event.actor, event.emoji];
2
+ export function newerControl(old, next) {
3
+ return !old || next.revision > old.revision || next.revision === old.revision && (next.timestamp > old.timestamp || next.timestamp === old.timestamp && next.id > old.id) ? next : old;
4
+ }
5
+ const identifier = (value) => typeof value === "string" && value.length > 0 && value.length <= 256;
6
+ /** Body-free proofs retained independently of the visible history window. */
7
+ export function messageControls(message, scope, key) {
8
+ const row = message;
9
+ if (row.appId !== undefined && row.appId !== scope.appId || row.teamId !== undefined && row.teamId !== scope.teamId || !identifier(row.from))
10
+ return [];
11
+ const apps = row.body;
12
+ const app = apps?.content?.appId ?? apps?.payload?.appId;
13
+ if (app !== undefined && app !== scope.appId)
14
+ return [];
15
+ if (row.recalled || row.legacyDisplay?.recalled)
16
+ return [{ scope: key, thread: message.threadId, target: message.id, kind: "recall", actor: row.from, emoji: "", id: message.id, timestamp: message.timestamp, revision: 0, active: true }];
17
+ const body = row.body;
18
+ const content = body?.kind === "message" && body.content?.appId === scope.appId ? body.content : undefined;
19
+ const target = body?.kind === "recall" && row.appId === scope.appId ? body.targetMessageId : content?.recall?.messageId;
20
+ const base = { scope: key, thread: message.threadId, actor: row.from, id: message.id, timestamp: message.timestamp };
21
+ if (identifier(target))
22
+ return [{ ...base, target, kind: "recall", emoji: "", revision: 0, active: true }];
23
+ const reaction = content?.reaction;
24
+ if (body?.contentKind === "text" && reaction?.version === 1 && identifier(reaction.messageId) && identifier(reaction.emojiId) && Number.isSafeInteger(reaction.revision) && (reaction.revision ?? 0) >= 1 && typeof reaction.active === "boolean")
25
+ return [{ ...base, target: reaction.messageId, kind: "reaction", emoji: reaction.emojiId, revision: reaction.revision, active: reaction.active }];
26
+ return [];
27
+ }
28
+ export function applyMessageControls(messages, controls) {
29
+ const grouped = new Map();
30
+ for (const event of controls) {
31
+ const list = grouped.get(event.target) ?? [];
32
+ list.push(event);
33
+ grouped.set(event.target, list);
34
+ }
35
+ return messages.map(message => {
36
+ const facts = grouped.get(message.id);
37
+ if (!facts?.length)
38
+ return message;
39
+ const row = message;
40
+ const recalled = message.recalled || facts.some(event => event.kind === "recall" && event.actor === row.from);
41
+ const reactionEvents = facts.filter(event => event.kind === "reaction").map(event => ({ id: event.id, messageId: event.target, threadId: event.thread, from: event.actor, emojiId: event.emoji, active: event.active, revision: event.revision, timestamp: event.timestamp }));
42
+ return { ...message, ...(recalled ? { recalled: true } : {}), ...(reactionEvents.length ? { reactionEvents: [...(row.reactionEvents ?? []), ...reactionEvents] } : {}) };
43
+ });
44
+ }
@@ -33,12 +33,14 @@ export declare const messageScopeKey: (scope: MessageScope) => string;
33
33
  /** Normalized browser cache. Data and coverage advance together in each commit.
34
34
  * Merge runs inside the write transaction against the latest cross-tab state. */
35
35
  export declare class IndexedDbMessageStore<T extends StoredMessage> {
36
+ private scope;
36
37
  private merge;
37
38
  private db;
38
39
  private rows;
39
40
  private legacy;
40
41
  private coverage;
41
42
  private accessRows;
43
+ private controls;
42
44
  private key;
43
45
  constructor(scope: MessageScope, merge: (previous: T | undefined, incoming: T) => T, options?: {
44
46
  indexedDB?: IDBFactory;
@@ -69,6 +71,9 @@ export declare class IndexedDbMessageStore<T extends StoredMessage> {
69
71
  authoritative?: boolean;
70
72
  scopeVersion?: number;
71
73
  }): Promise<number>;
74
+ private rememberControls;
75
+ private controlPage;
76
+ private finishPage;
72
77
  windows(): Promise<CoverageRow[]>;
73
78
  scopeAccess(): Promise<MessageScopeAccess>;
74
79
  private scopedCoverage;
@@ -1,22 +1,28 @@
1
1
  import { Dexie } from "dexie";
2
+ import { messageControls, controlKey, newerControl, applyMessageControls } from "./messageControls.js";
2
3
  export const messageScopeKey = (scope) => JSON.stringify([scope.appId, scope.accountId, scope.profileId, scope.teamId]);
3
4
  /** Normalized browser cache. Data and coverage advance together in each commit.
4
5
  * Merge runs inside the write transaction against the latest cross-tab state. */
5
6
  export class IndexedDbMessageStore {
7
+ scope;
6
8
  merge;
7
9
  db;
8
10
  rows;
9
11
  legacy;
10
12
  coverage;
11
13
  accessRows;
14
+ controls;
12
15
  key;
13
16
  constructor(scope, merge, options = {}) {
17
+ this.scope = scope;
14
18
  this.merge = merge;
15
19
  this.key = messageScopeKey(scope);
16
20
  this.db = new Dexie(options.name ?? "pmate-chat-messages", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
17
21
  this.db.version(1).stores({ messages: "[scope+thread+id],[scope+thread+time+id],[scope+thread],scope", coverage: "[scope+thread],scope" });
18
22
  this.db.version(2).stores({ legacyMessages: "[scope+thread+id],[scope+thread+time+id],[scope+thread],scope" });
19
23
  this.db.version(3).stores({ scopeAccess: "scope" });
24
+ this.db.version(4).stores({ messageControls: "[scope+thread+target+kind+actor+emoji],[scope+thread+target],[scope+thread],scope" });
25
+ this.controls = this.db.table("messageControls");
20
26
  this.accessRows = this.db.table("scopeAccess");
21
27
  this.legacy = this.db.table("legacyMessages");
22
28
  this.rows = this.db.table("messages");
@@ -27,14 +33,15 @@ export class IndexedDbMessageStore {
27
33
  async read(thread, limit = 100, before) {
28
34
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
29
35
  throw new Error("INVALID_MESSAGE_LIMIT");
30
- return this.db.transaction("r", this.rows, this.coverage, this.accessRows, async () => {
36
+ const page = await this.db.transaction("r", this.rows, this.coverage, this.accessRows, this.controls, async () => {
31
37
  const access = await this.scopeAccess();
32
38
  if (access.revoked)
33
39
  return { messages: [], hasEarlier: false, coverage: { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } };
34
40
  const range = this.rows.where("[scope+thread+time+id]").between([this.key, thread, Dexie.minKey, Dexie.minKey], [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey], true, !before);
35
41
  const records = await range.reverse().limit(limit + 1).toArray();
36
- return { messages: records.slice(0, limit).reverse().map(row => row.value), hasEarlier: records.length > limit, coverage: this.scopedCoverage(await this.coverage.get([this.key, thread]), access) };
42
+ return this.controlPage({ messages: records.slice(0, limit).reverse().map(row => row.value), hasEarlier: records.length > limit, coverage: this.scopedCoverage(await this.coverage.get([this.key, thread]), access) });
37
43
  });
44
+ return this.finishPage(thread, page);
38
45
  }
39
46
  /** Migration records never claim coverage or overwrite authoritative rows. */
40
47
  async importLegacy(thread, messages) {
@@ -48,13 +55,19 @@ export class IndexedDbMessageStore {
48
55
  if (bytes > 1_048_576)
49
56
  throw new Error("MESSAGE_BATCH_TOO_LARGE");
50
57
  }
51
- return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
58
+ return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
52
59
  const access = await this.scopeAccess();
53
60
  const coverage = await this.coverage.get([this.key, thread]);
54
61
  if (access.revoked || coverage?.revoked)
55
62
  throw new Error("MESSAGE_ACCESS_REVOKED");
56
63
  if (access.version > 0)
57
64
  throw new Error("MESSAGE_PROJECTION_CHANGED");
65
+ const proofs = messages.map(message => messageControls(message, this.scope, this.key));
66
+ const controlsOnly = messages.length > 0 && proofs.every((events, index) => events.length > 0 && !messages[index].recalled && !messages[index].legacyDisplay?.recalled);
67
+ if (controlsOnly) {
68
+ await this.rememberControls(proofs.flat());
69
+ return 0;
70
+ }
58
71
  if ((coverage?.resetVersion ?? 0) > 0)
59
72
  throw new Error("MESSAGE_PROJECTION_CHANGED");
60
73
  const ids = [...new Set(messages.map(message => message.id))], keys = ids.map(id => [this.key, thread, id]);
@@ -71,6 +84,7 @@ export class IndexedDbMessageStore {
71
84
  if (JSON.stringify(old) !== JSON.stringify(value))
72
85
  changed.set(message.id, { scope: this.key, thread, id: value.id, time: value.timestamp, value });
73
86
  }
87
+ await this.rememberControls([...changed.values()].flatMap(row => messageControls(row.value, this.scope, this.key)));
74
88
  if (changed.size)
75
89
  await this.legacy.bulkPut([...changed.values()]);
76
90
  return changed.size;
@@ -80,7 +94,7 @@ export class IndexedDbMessageStore {
80
94
  async readFallback(thread, limit = 100, before) {
81
95
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
82
96
  throw new Error("INVALID_MESSAGE_LIMIT");
83
- return this.db.transaction("r", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
97
+ const page = await this.db.transaction("r", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
84
98
  const access = await this.scopeAccess();
85
99
  const coverage = access.revoked ? { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } : this.scopedCoverage(await this.coverage.get([this.key, thread]), access);
86
100
  if (coverage?.revoked)
@@ -88,8 +102,9 @@ export class IndexedDbMessageStore {
88
102
  const read = (table) => table.where("[scope+thread+time+id]").between([this.key, thread, Dexie.minKey, Dexie.minKey], [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey], true, !before).reverse().limit(limit + 1).toArray();
89
103
  const [canonical, legacy] = await Promise.all([read(this.rows), read(this.legacy)]);
90
104
  const rows = [...new Map([...legacy, ...canonical].map(row => [row.id, row])).values()].sort((a, b) => a.time - b.time || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
91
- return { messages: rows.slice(-limit).map(row => row.value), hasEarlier: rows.length > limit, coverage };
105
+ return this.controlPage({ messages: rows.slice(-limit).map(row => row.value), hasEarlier: rows.length > limit, coverage });
92
106
  });
107
+ return this.finishPage(thread, page);
93
108
  }
94
109
  async commit(thread, messages, coverage, guard) {
95
110
  for (const message of messages)
@@ -97,7 +112,7 @@ export class IndexedDbMessageStore {
97
112
  throw new Error("MESSAGE_SCOPE_MISMATCH");
98
113
  if (messages.length > 1000)
99
114
  throw new Error("MESSAGE_BATCH_TOO_LARGE");
100
- return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
115
+ return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
101
116
  const access = await this.scopeAccess();
102
117
  if ((guard?.scopeVersion ?? 0) !== access.version)
103
118
  throw new Error("MESSAGE_SCOPE_CHANGED");
@@ -126,6 +141,7 @@ export class IndexedDbMessageStore {
126
141
  if (!canonicalIds.has(message.id) || JSON.stringify(old) !== JSON.stringify(value))
127
142
  changed.set(message.id, { scope: this.key, thread, id: value.id, time: value.timestamp, value });
128
143
  }
144
+ await this.rememberControls(messages.flatMap(message => messageControls(message, this.scope, this.key)));
129
145
  if (changed.size)
130
146
  await this.rows.bulkPut([...changed.values()]);
131
147
  const replaced = legacy.flatMap(row => row ? [[this.key, thread, row.id]] : []);
@@ -139,6 +155,47 @@ export class IndexedDbMessageStore {
139
155
  return changed.size;
140
156
  });
141
157
  }
158
+ async rememberControls(events) {
159
+ const latest = new Map();
160
+ for (const event of events) {
161
+ const key = JSON.stringify(controlKey(event));
162
+ latest.set(key, newerControl(latest.get(key), event));
163
+ }
164
+ const values = [...latest.values()];
165
+ for (let offset = 0; offset < values.length; offset += 128) {
166
+ const batch = values.slice(offset, offset + 128), previous = await this.controls.bulkGet(batch.map(controlKey));
167
+ const changed = batch.filter((event, index) => newerControl(previous[index], event) !== previous[index]);
168
+ if (changed.length)
169
+ await this.controls.bulkPut(changed);
170
+ }
171
+ }
172
+ async controlPage(page) {
173
+ const events = page.messages.flatMap(message => messageControls(message, this.scope, this.key));
174
+ const old = events.length ? await this.controls.bulkGet(events.map(controlKey)) : [];
175
+ const pendingControls = events.filter((event, index) => newerControl(old[index], event) !== old[index]);
176
+ const facts = await this.controls.where("[scope+thread+target]").anyOf(page.messages.map(message => [this.key, message.threadId, message.id])).toArray();
177
+ const latest = new Map();
178
+ for (const event of [...facts, ...events]) {
179
+ const key = JSON.stringify(controlKey(event));
180
+ latest.set(key, newerControl(latest.get(key), event));
181
+ }
182
+ return { ...page, messages: applyMessageControls(page.messages, [...latest.values()]), pendingControls };
183
+ }
184
+ async finishPage(thread, page) {
185
+ const { pendingControls, ...result } = page;
186
+ // v3 caches migrate only source proofs from already-read pages, with no body
187
+ // rewrite. Steady reads remain a single read-only IndexedDB transaction.
188
+ if (pendingControls?.length)
189
+ await this.db.transaction("rw", this.controls, this.coverage, this.accessRows, async () => {
190
+ const access = await this.scopeAccess(), current = await this.coverage.get([this.key, thread]);
191
+ if (access.revoked || access.version !== (page.coverage?.scopeVersion ?? 0))
192
+ throw new Error("MESSAGE_SCOPE_CHANGED");
193
+ if ((current?.resetVersion ?? 0) !== (page.coverage?.resetVersion ?? 0))
194
+ throw new Error("MESSAGE_PROJECTION_CHANGED");
195
+ await this.rememberControls(pendingControls);
196
+ });
197
+ return result;
198
+ }
142
199
  async windows() {
143
200
  return this.db.transaction("r", this.coverage, this.accessRows, async () => {
144
201
  if ((await this.scopeAccess()).revoked)
@@ -156,6 +213,7 @@ export class IndexedDbMessageStore {
156
213
  return access.version ? { ...coverage, complete: coverage?.complete ?? false, scopeVersion: access.version } : coverage;
157
214
  }
158
215
  async clearScopeRows() {
216
+ await this.controls.where("scope").equals(this.key).delete();
159
217
  await this.rows.where("[scope+thread+id]").between([this.key, Dexie.minKey, Dexie.minKey], [this.key, Dexie.maxKey, Dexie.maxKey], true, true).delete();
160
218
  await this.legacy.where("[scope+thread+id]").between([this.key, Dexie.minKey, Dexie.minKey], [this.key, Dexie.maxKey, Dexie.maxKey], true, true).delete();
161
219
  await this.coverage.where("[scope+thread]").between([this.key, Dexie.minKey], [this.key, Dexie.maxKey], true, true).delete();
@@ -170,7 +228,7 @@ export class IndexedDbMessageStore {
170
228
  await this.accessRows.put(next);
171
229
  return next;
172
230
  });
173
- await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
231
+ await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
174
232
  const current = await this.scopeAccess();
175
233
  if (current.version === access.version && current.revoked)
176
234
  await this.clearScopeRows();
@@ -179,7 +237,7 @@ export class IndexedDbMessageStore {
179
237
  }
180
238
  /** A durable epoch prevents another tab's pre-reset request from restoring rows. */
181
239
  async resetThread(thread, revoked = false, scopeVersion = 0) {
182
- return this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
240
+ return this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
183
241
  const access = await this.scopeAccess();
184
242
  if (access.version !== scopeVersion)
185
243
  throw new Error("MESSAGE_SCOPE_CHANGED");
@@ -187,6 +245,8 @@ export class IndexedDbMessageStore {
187
245
  throw new Error("MESSAGE_ACCESS_REVOKED");
188
246
  const old = await this.coverage.get([this.key, thread]);
189
247
  const resetVersion = (old?.resetVersion ?? 0) + 1;
248
+ if (revoked)
249
+ await this.controls.where("[scope+thread]").equals([this.key, thread]).delete();
190
250
  await this.rows.where("[scope+thread]").equals([this.key, thread]).delete();
191
251
  await this.legacy.where("[scope+thread]").equals([this.key, thread]).delete();
192
252
  await this.coverage.put({ scope: this.key, thread, complete: false, resetVersion, revoked });
@@ -194,14 +254,16 @@ export class IndexedDbMessageStore {
194
254
  });
195
255
  }
196
256
  async removeThread(thread) {
197
- await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
257
+ await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
258
+ await this.controls.where("[scope+thread]").equals([this.key, thread]).delete();
198
259
  await this.rows.where("[scope+thread]").equals([this.key, thread]).delete();
199
260
  await this.legacy.where("[scope+thread]").equals([this.key, thread]).delete();
200
261
  await this.coverage.delete([this.key, thread]);
201
262
  });
202
263
  }
203
264
  async clear() {
204
- await this.db.transaction("rw", this.rows, this.legacy, this.coverage, this.accessRows, async () => {
265
+ await this.db.transaction("rw", [this.rows, this.legacy, this.coverage, this.accessRows, this.controls], async () => {
266
+ await this.controls.where("scope").equals(this.key).delete();
205
267
  await this.legacy.where("scope").equals(this.key).delete();
206
268
  await this.rows.where("scope").equals(this.key).delete();
207
269
  await this.coverage.where("scope").equals(this.key).delete();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.33",
3
+ "version": "0.12.0-next.35",
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",