@pmate/chat-sdk 0.12.0-next.66 → 0.12.0-next.67

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.
@@ -20,7 +20,7 @@ export function asChatError(error) {
20
20
  : /CLOSED/.test(message) ? "closed"
21
21
  : status === 409 || /REVISION_CONFLICT|PROJECTION_CHANGED/.test(message) ? "conflict"
22
22
  : /BACKPRESSURE|TOO_LARGE/.test(message) ? "busy"
23
- : status === 400 || /INVALID_|CONVERSATION_MISMATCH|NOT_OWNED/.test(message) ? "invalid-input"
23
+ : status === 400 || /INVALID_|CONVERSATION_MISMATCH|CONVERSATION_UNKNOWN|NOT_OWNED/.test(message) ? "invalid-input"
24
24
  : /QuotaExceeded|DatabaseClosed|UnknownError|AbortError/.test(value?.name ?? "") || /STORAGE_|DATABASE_|CACHE_WRITE/.test(message) ? "storage-unavailable" : "unavailable";
25
25
  return new ChatError(code, message, { cause: error, status });
26
26
  }
@@ -444,21 +444,32 @@ export class ChatSession {
444
444
  /** Validated target identity survives content redaction; no roster is exposed. */
445
445
  getConversationTarget(threadId) {
446
446
  const scope = this.options.messages.scope;
447
- const group = this.getSnapshot(threadId).group;
447
+ const snapshot = this.getSnapshot(threadId), group = snapshot.group;
448
448
  if (group)
449
449
  return { id: threadId, kind: "group", targetId: group.groupId, teamId: scope.teamId };
450
- for (const value of this.getSnapshot(threadId).messages) {
451
- const row = value;
450
+ const infer = (row) => {
452
451
  const kind = row.threadKind ?? row.body?.threadKind;
453
452
  if ((kind !== "group" && kind !== "dm") || !row.to || !row.from)
454
- continue;
453
+ return undefined;
455
454
  const thread = { id: threadId, kind, targetId: kind === "group" || row.from === scope.profileId ? row.to : row.from, teamId: scope.teamId };
456
- // A stored row is only a hint; validate the canonical scoped identifier.
457
455
  try {
458
456
  validateConversation(scope, thread);
459
457
  return thread;
460
458
  }
461
- catch { }
459
+ catch {
460
+ return undefined;
461
+ }
462
+ };
463
+ for (const value of snapshot.messages) {
464
+ const thread = infer(value);
465
+ if (thread)
466
+ return thread;
467
+ }
468
+ // Older integrations may have a durable send but no registered target or indexed history yet.
469
+ for (const value of snapshot.outgoing) {
470
+ const thread = infer(value.message);
471
+ if (thread)
472
+ return thread;
462
473
  }
463
474
  return undefined;
464
475
  }
@@ -30,6 +30,7 @@ export declare class ConversationDirectory {
30
30
  put(thread: ChatConversation): Promise<ChatConversation>;
31
31
  private persist;
32
32
  get(id: string): Promise<ChatConversation | undefined>;
33
+ private lookup;
33
34
  private flush;
34
35
  retire(version: number): Promise<void>;
35
36
  close(): Promise<void>;
@@ -19,7 +19,7 @@ export class ConversationDirectory {
19
19
  constructor(scope, access) {
20
20
  this.scope = scope;
21
21
  this.access = access;
22
- this.cache = new IndexedDbResponseCache(`conversations:${messageScopeKey(scope)}`, { access: () => this.authorize(), entries: 256, recordBytes: 4096 });
22
+ this.cache = new IndexedDbResponseCache(`conversations:${messageScopeKey(scope)}`, { access: () => this.authorize(), entries: 256, recordBytes: 4096, retainAll: true });
23
23
  }
24
24
  authorize() { return this.authority ??= this.access().finally(() => { this.authority = undefined; }); }
25
25
  assertOpen() { if (this.retired)
@@ -56,18 +56,18 @@ export class ConversationDirectory {
56
56
  this.assertOpen();
57
57
  validateConversation(this.scope, thread);
58
58
  const value = { id: thread.id, kind: thread.kind, targetId: thread.targetId, teamId: this.scope.teamId };
59
- const previous = await this.get(thread.id);
59
+ const row = await this.lookup(thread.id), previous = row.value;
60
60
  if (previous) {
61
61
  if (previous.kind !== value.kind || previous.targetId !== value.targetId)
62
62
  throw new Error("MESSAGE_CONVERSATION_MISMATCH");
63
63
  return previous;
64
64
  }
65
- const row = await this.cache.read(thread.id);
66
65
  await this.cache.write(thread.id, value, row.epoch);
67
66
  this.assertOpen();
68
67
  return this.remember(value);
69
68
  }
70
- async get(id) {
69
+ async get(id) { return (await this.lookup(id)).value; }
70
+ async lookup(id) {
71
71
  this.assertOpen();
72
72
  const authority = await this.authorize();
73
73
  this.assertOpen();
@@ -78,7 +78,7 @@ export class ConversationDirectory {
78
78
  this.version = authority.version;
79
79
  const known = this.memory.get(id);
80
80
  if (known)
81
- return this.remember(known);
81
+ return { value: this.remember(known), epoch: 0 };
82
82
  const existing = this.pending.get(id);
83
83
  if (existing)
84
84
  return existing;
@@ -98,13 +98,13 @@ export class ConversationDirectory {
98
98
  const rows = await this.cache.readMany(batch.map(item => item.id));
99
99
  this.assertOpen();
100
100
  for (const item of batch) {
101
- const value = rows.get(item.id)?.cached?.value;
101
+ const row = rows.get(item.id), value = row.cached?.value;
102
102
  if (value) {
103
103
  if (value.id !== item.id)
104
104
  throw new Error("MESSAGE_CONVERSATION_MISMATCH");
105
105
  validateConversation(this.scope, value);
106
106
  }
107
- item.resolve(value ? this.remember(value) : undefined);
107
+ item.resolve({ value: value ? this.remember(value) : undefined, epoch: row.epoch });
108
108
  }
109
109
  }
110
110
  catch (error) {
@@ -8,6 +8,8 @@ export type ResponseCacheOptions<T> = {
8
8
  indexedDB?: IDBFactory;
9
9
  name?: string;
10
10
  access?: ResponseCacheAccess;
11
+ /** Immutable identity metadata must survive ordinary response-window eviction. */
12
+ retainAll?: boolean;
11
13
  recordBytes?: number;
12
14
  entries?: number;
13
15
  batchRecords?: number;
@@ -59,7 +59,7 @@ export class IndexedDbResponseCache {
59
59
  throw new Error("RESPONSE_CACHE_SCOPE_CHANGED");
60
60
  if (access.version > version) {
61
61
  await this.epochs.put({ scope: this.scope, epoch: (current?.epoch ?? 0) + 1, accessVersion: access.version });
62
- await this.rows.where("scope").equals(this.scope).delete();
62
+ await this.rows.where("[scope+key]").between([this.scope, Dexie.minKey], [this.scope, Dexie.maxKey], true, true).delete();
63
63
  }
64
64
  });
65
65
  if (!access.allowed)
@@ -114,7 +114,7 @@ export class IndexedDbResponseCache {
114
114
  if ((current?.accessVersion ?? 0) >= version)
115
115
  return;
116
116
  await this.epochs.put({ scope: this.scope, epoch: (current?.epoch ?? 0) + 1, accessVersion: version });
117
- await this.rows.where("scope").equals(this.scope).delete();
117
+ await this.rows.where("[scope+key]").between([this.scope, Dexie.minKey], [this.scope, Dexie.maxKey], true, true).delete();
118
118
  });
119
119
  this.invalidations.add(task);
120
120
  return task.finally(() => this.invalidations.delete(task));
@@ -127,7 +127,7 @@ export class IndexedDbResponseCache {
127
127
  const current = await this.epochs.get(this.scope);
128
128
  const epoch = (current?.epoch ?? 0) + 1;
129
129
  await this.epochs.put({ scope: this.scope, epoch, accessVersion: current?.accessVersion });
130
- await this.rows.where("scope").equals(this.scope).delete();
130
+ await this.rows.where("[scope+key]").between([this.scope, Dexie.minKey], [this.scope, Dexie.maxKey], true, true).delete();
131
131
  });
132
132
  })();
133
133
  this.invalidations.add(task);
@@ -178,14 +178,16 @@ export class IndexedDbResponseCache {
178
178
  }
179
179
  if (unique.size) {
180
180
  await this.rows.bulkPut([...unique.values()]);
181
- const stored = await this.rows.where("scope").equals(this.scope).toArray();
182
- stored.sort((a, b) => b.time - a.time || a.key.localeCompare(b.key));
183
- let bytes = 0;
184
- const evicted = [];
185
- stored.forEach((row, index) => { bytes += row.bytes; if (index >= (this.options.entries ?? 256) || bytes > 1048576)
186
- evicted.push([this.scope, row.key]); });
187
- if (evicted.length)
188
- await this.rows.bulkDelete(evicted);
181
+ if (!this.options.retainAll) {
182
+ const stored = await this.rows.where("scope").equals(this.scope).toArray();
183
+ stored.sort((a, b) => b.time - a.time || a.key.localeCompare(b.key));
184
+ let bytes = 0;
185
+ const evicted = [];
186
+ stored.forEach((row, index) => { bytes += row.bytes; if (index >= (this.options.entries ?? 256) || bytes > 1048576)
187
+ evicted.push([this.scope, row.key]); });
188
+ if (evicted.length)
189
+ await this.rows.bulkDelete(evicted);
190
+ }
189
191
  }
190
192
  return { accepted, changed };
191
193
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.66",
3
+ "version": "0.12.0-next.67",
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",