@pmate/chat-sdk 0.12.0-next.78 → 0.12.0-next.79

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.
@@ -304,12 +304,11 @@ export class ChatSession {
304
304
  throw new Error("INVALID_MESSAGE_QUERY");
305
305
  // Explicit authoritative changes (for example history assignment) can invalidate
306
306
  // even a recently cached empty window. Keep this on the existing read surface.
307
- if (options.refresh)
308
- await this.messages.read(threadId, options.limit ?? 100, true);
307
+ const refreshed = options.refresh ? await this.messages.read(threadId, options.limit ?? 100, true) : undefined;
309
308
  if (options.all) {
310
309
  return this.readAll(threadId);
311
310
  }
312
- const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
311
+ const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : refreshed ?? this.messages.read(threadId, options.limit));
313
312
  this.assertOpen();
314
313
  await this.confirmMessages(page.messages);
315
314
  if (this.initializing && !this.readingStarted && page.complete)
@@ -57,6 +57,7 @@ export declare class IndexedDbMessageStore<T extends StoredMessage> {
57
57
  hasEarlier: boolean;
58
58
  coverage: MessageCoverage | undefined;
59
59
  }>;
60
+ private readRows;
60
61
  /** Bounded identifier reads for SDK-owned view handoffs; no transport fallback. */
61
62
  readSelected(thread: string, ids: readonly string[]): Promise<{
62
63
  messages: T[];
@@ -42,12 +42,24 @@ export class IndexedDbMessageStore {
42
42
  const access = await this.scopeAccess();
43
43
  if (access.revoked)
44
44
  return { messages: [], hasEarlier: false, coverage: { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } };
45
- 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);
46
- const records = await range.reverse().limit(limit + 1).toArray();
45
+ const records = await this.readRows(this.rows, thread, limit, before);
47
46
  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) });
48
47
  });
49
48
  return this.finishPage(thread, page);
50
49
  }
50
+ readRows(table, thread, limit, before) {
51
+ const lower = [this.key, thread, Dexie.minKey, Dexie.minKey], upper = [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey];
52
+ const index = Dexie.currentTransaction.idbtrans.objectStore(table.name).index("[scope+thread+time+id]");
53
+ // Bounded batches avoid a browser round trip per history row. Older engines
54
+ // retain the same indexed cursor path and pagination boundaries.
55
+ if (typeof index.getAllRecords === "function")
56
+ return new Dexie.Promise((resolve, reject) => {
57
+ const request = index.getAllRecords({ query: IDBKeyRange.bound(lower, upper, false, Boolean(before)), count: limit + 1, direction: "prev" });
58
+ request.onsuccess = () => resolve(request.result.map(record => record.value));
59
+ request.onerror = () => reject(request.error);
60
+ });
61
+ return table.where("[scope+thread+time+id]").between(lower, upper, true, !before).reverse().limit(limit + 1).toArray();
62
+ }
51
63
  /** Bounded identifier reads for SDK-owned view handoffs; no transport fallback. */
52
64
  async readSelected(thread, ids) {
53
65
  if (ids.length > 100 || ids.some(id => !id || id.length > 256))
@@ -118,7 +130,7 @@ export class IndexedDbMessageStore {
118
130
  const coverage = access.revoked ? { complete: false, revoked: true, resetVersion: 0, scopeVersion: access.version } : this.scopedCoverage(await this.coverage.get([this.key, thread]), access);
119
131
  if (coverage?.revoked)
120
132
  return { messages: [], hasEarlier: false, coverage };
121
- 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();
133
+ const read = (table) => this.readRows(table, thread, limit, before);
122
134
  const [canonical, legacy] = await Promise.all([read(this.rows), read(this.legacy)]);
123
135
  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));
124
136
  return this.controlPage({ messages: rows.slice(-limit).map(row => row.value), hasEarlier: rows.length > limit, coverage });
@@ -197,7 +209,8 @@ export class IndexedDbMessageStore {
197
209
  const thread = page.messages[0]?.threadId;
198
210
  const targets = thread && references.length ? (await this.rows.bulkGet(references.map(id => [this.key, thread, id]))).flatMap(row => row ? [row.value] : []) : [];
199
211
  const selected = [...page.messages, ...targets];
200
- const facts = await this.controls.where("[scope+thread+target]").anyOf(selected.map(message => [this.key, message.threadId, message.id])).toArray();
212
+ // Exact bounded index reads avoid serial cursor seeks across a message page.
213
+ const facts = (await Promise.all(selected.map(message => this.controls.where("[scope+thread+target]").equals([this.key, message.threadId, message.id]).toArray()))).flat();
201
214
  const latest = new Map();
202
215
  for (const event of [...facts, ...events]) {
203
216
  const key = JSON.stringify(controlKey(event));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.78",
3
+ "version": "0.12.0-next.79",
4
4
  "description": "Local-first chat SDK, message subscriptions, reading state, and shared chat UI primitives",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",