@pmate/chat-sdk 0.12.0-next.7 → 0.12.0-next.9

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.
package/README.md CHANGED
@@ -142,7 +142,9 @@ The browser entry point is `createChat` from `@pmate/chat-sdk/runtime`:
142
142
  ```ts
143
143
  const chat = createChat({ appId, accountId, profileId, teamId, token, onError })
144
144
  await chat.start()
145
- const page = await chat.getMessages(threadId, { limit: 50 })
145
+ const page = await chat.getMessages(threadId)
146
+ // SDK owns the page size and history boundary; hosts need not construct a cursor.
147
+ const earlier = await chat.getMessages(threadId, { earlier: true })
146
148
  // After an authoritative mutation (e.g. history assignment), await reconciliation.
147
149
  // Concurrent refreshes share transport; errors do not masquerade as fresh cache.
148
150
  const reconciled = await chat.getMessages(threadId, { refresh: true })
@@ -43,6 +43,8 @@ export declare class ChatSession<T extends StoredMessage> {
43
43
  private sender;
44
44
  private listeners;
45
45
  private snapshots;
46
+ private historyEdges;
47
+ private earlierReads;
46
48
  private off;
47
49
  private globalListeners;
48
50
  private globalSnapshot?;
@@ -61,9 +63,12 @@ export declare class ChatSession<T extends StoredMessage> {
61
63
  timestamp: number;
62
64
  id: string;
63
65
  };
66
+ earlier?: boolean;
64
67
  all?: boolean;
65
68
  refresh?: boolean;
66
69
  }): Promise<MessageSnapshot<T>>;
70
+ private rememberHistoryEdge;
71
+ private readEarlier;
67
72
  private readAll;
68
73
  sendMessage(threadId: string, input: {
69
74
  to: string;
@@ -15,6 +15,8 @@ export class ChatSession {
15
15
  sender;
16
16
  listeners = new Map();
17
17
  snapshots = new Map();
18
+ historyEdges = new Map();
19
+ earlierReads = new Map();
18
20
  off = [];
19
21
  globalListeners = new Set();
20
22
  globalSnapshot;
@@ -37,7 +39,7 @@ export class ChatSession {
37
39
  this.messages = new MessageClient(options.messages);
38
40
  this.connection = options.sharedConnection ?? new ChatClient(options.connection);
39
41
  this.reading = new ReadingClient({ ...options.reading, storageScope: JSON.stringify([scope.accountId, scope.teamId]) });
40
- this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
42
+ this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
41
43
  this.sender = new DurableChatSender(this.connection, scope, options.onError);
42
44
  this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
43
45
  options.onError(error); });
@@ -81,6 +83,11 @@ export class ChatSession {
81
83
  throw new Error("CHAT_SESSION_CLOSED"); }
82
84
  async getMessages(threadId, options = {}) {
83
85
  this.assertOpen();
86
+ if (options.earlier) {
87
+ if (options.before || options.all || options.limit !== undefined || options.refresh)
88
+ throw new Error("INVALID_MESSAGE_QUERY");
89
+ return this.readEarlier(threadId);
90
+ }
84
91
  if (options.all && (options.before || options.limit !== undefined))
85
92
  throw new Error("INVALID_MESSAGE_QUERY");
86
93
  // Explicit authoritative changes (for example history assignment) can invalidate
@@ -92,8 +99,37 @@ export class ChatSession {
92
99
  }
93
100
  const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
94
101
  await this.sender.confirm(page.messages.map(message => message.id));
102
+ const edge = this.historyEdges.get(threadId);
103
+ if (!options.before && page.complete && (!edge || edge.version !== page.resetVersion))
104
+ this.rememberHistoryEdge(threadId, page);
95
105
  return page;
96
106
  }
107
+ rememberHistoryEdge(threadId, page) {
108
+ const first = page.messages[0];
109
+ this.historyEdges.set(threadId, { before: first ? { timestamp: first.timestamp, id: first.id } : undefined, version: page.resetVersion, hasEarlier: page.hasEarlier });
110
+ }
111
+ readEarlier(threadId) {
112
+ const running = this.earlierReads.get(threadId);
113
+ if (running)
114
+ return running;
115
+ const task = (async () => {
116
+ const recent = await this.getMessages(threadId);
117
+ if (!recent.complete)
118
+ throw new Error("MESSAGE_WINDOW_INCOMPLETE");
119
+ const edge = this.historyEdges.get(threadId);
120
+ if (!edge.hasEarlier)
121
+ return { ...recent, messages: [], hasEarlier: false };
122
+ if (!edge.before)
123
+ throw new Error("MESSAGE_SYNC_NO_PROGRESS");
124
+ const page = await this.getMessages(threadId, { before: edge.before });
125
+ this.assertOpen();
126
+ this.rememberHistoryEdge(threadId, page);
127
+ return page;
128
+ })().finally(() => { if (this.earlierReads.get(threadId) === task)
129
+ this.earlierReads.delete(threadId); });
130
+ this.earlierReads.set(threadId, task);
131
+ return task;
132
+ }
97
133
  async readAll(threadId) {
98
134
  for (let attempt = 0; attempt < 3; attempt++) {
99
135
  const pages = [];
@@ -231,6 +267,8 @@ export class ChatSession {
231
267
  this.listeners.clear();
232
268
  this.globalListeners.clear();
233
269
  this.snapshots.clear();
270
+ this.historyEdges.clear();
271
+ this.earlierReads.clear();
234
272
  this.globalSnapshot = undefined;
235
273
  })();
236
274
  }
@@ -1,3 +1,4 @@
1
+ import { type MessageScope } from "./messageStore.js";
1
2
  export type MessageReadersPage = {
2
3
  appId: string;
3
4
  threadId: string;
@@ -21,6 +22,8 @@ export type MessageReadersOptions = {
21
22
  token: string;
22
23
  hubHttpUrl: string;
23
24
  fetch?: typeof fetch;
25
+ scope?: MessageScope;
26
+ onError?: (error: unknown) => void;
24
27
  };
25
28
  export declare class MessageReadersError extends Error {
26
29
  readonly status: number;
@@ -32,8 +35,12 @@ export declare class MessageReadersClient {
32
35
  private options;
33
36
  private pending;
34
37
  private lifetime;
38
+ private cache?;
39
+ private revoked;
40
+ private generation;
35
41
  constructor(options: MessageReadersOptions);
36
42
  getReaders(query: MessageReadersQuery, signal?: AbortSignal): Promise<MessageReadersPage>;
43
+ private cachedRequest;
37
44
  private request;
38
45
  close(): void;
39
46
  }
@@ -1,3 +1,5 @@
1
+ import { IndexedDbResponseCache } from "./responseCache.js";
2
+ import { messageScopeKey } from "./messageStore.js";
1
3
  export class MessageReadersError extends Error {
2
4
  status;
3
5
  forbidden;
@@ -12,7 +14,17 @@ export class MessageReadersClient {
12
14
  options;
13
15
  pending = new Map();
14
16
  lifetime = new AbortController();
15
- constructor(options) { this.options = { ...options }; }
17
+ cache;
18
+ revoked = false;
19
+ generation = 0;
20
+ constructor(options) {
21
+ this.options = { ...options };
22
+ if (options.scope) {
23
+ if (options.scope.appId !== options.appId || options.scope.profileId !== options.profileId)
24
+ throw new Error("MESSAGE_READERS_SCOPE_MISMATCH");
25
+ this.cache = new IndexedDbResponseCache(messageScopeKey(options.scope));
26
+ }
27
+ }
16
28
  getReaders(query, signal) {
17
29
  if (this.lifetime.signal.aborted)
18
30
  return Promise.reject(new Error("MESSAGE_READERS_CLOSED"));
@@ -24,7 +36,7 @@ export class MessageReadersClient {
24
36
  const key = JSON.stringify([input.threadId, input.messageId, input.afterProfileId, input.revision, input.limit]);
25
37
  let task = this.pending.get(key);
26
38
  if (!task) {
27
- task = this.request(input).finally(() => { if (this.pending.get(key) === task)
39
+ task = this.cachedRequest(input, key).finally(() => { if (this.pending.get(key) === task)
28
40
  this.pending.delete(key); });
29
41
  this.pending.set(key, task);
30
42
  }
@@ -37,6 +49,47 @@ export class MessageReadersClient {
37
49
  task.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
38
50
  });
39
51
  }
52
+ async cachedRequest(input, key) {
53
+ // Keep only the standard SDK window. Arbitrary page sizes must not multiply
54
+ // durable cache entries. Revision is part of the key, so changed counts do
55
+ // not reuse a page from a previous membership/read projection.
56
+ const cache = input.limit === 50 ? this.cache : undefined;
57
+ if (!cache)
58
+ return this.request(input);
59
+ const stored = await cache.read(key).catch(error => { this.options.onError?.(error); return undefined; });
60
+ if (this.lifetime.signal.aborted)
61
+ throw new Error("MESSAGE_READERS_CLOSED");
62
+ const generation = this.generation;
63
+ const cached = this.revoked ? undefined : stored?.cached;
64
+ if (cached && (Date.now() - cached.time < 5000 || (typeof navigator !== "undefined" && navigator.onLine === false)))
65
+ return structuredClone(cached.value);
66
+ let value;
67
+ try {
68
+ value = await this.request(input);
69
+ }
70
+ catch (error) {
71
+ if (error instanceof MessageReadersError && error.forbidden) {
72
+ this.revoked = true;
73
+ this.generation++;
74
+ await this.cache?.invalidate().catch(failure => this.options.onError?.(failure));
75
+ throw error;
76
+ }
77
+ if (cached && !this.revoked && generation === this.generation && !this.lifetime.signal.aborted && (error instanceof TypeError || (error instanceof MessageReadersError && error.status >= 500)))
78
+ return structuredClone(cached.value);
79
+ throw error;
80
+ }
81
+ if (generation !== this.generation)
82
+ throw new Error("MESSAGE_READERS_INVALIDATED");
83
+ this.revoked = false;
84
+ if (value.resetRequired)
85
+ await cache?.invalidate().catch(error => this.options.onError?.(error));
86
+ else if (stored)
87
+ await cache?.write(key, value, stored.epoch).catch(error => { if (String(error).includes("RESPONSE_CACHE_INVALIDATED"))
88
+ throw error; this.options.onError?.(error); });
89
+ if (this.lifetime.signal.aborted)
90
+ throw new Error("MESSAGE_READERS_CLOSED");
91
+ return value;
92
+ }
40
93
  async request(input) {
41
94
  const { token, fetch: fetcher, hubHttpUrl, appId, profileId } = this.options;
42
95
  const controller = new AbortController();
@@ -67,5 +120,5 @@ export class MessageReadersClient {
67
120
  this.lifetime.signal.removeEventListener("abort", cancel);
68
121
  }
69
122
  }
70
- close() { this.lifetime.abort(); this.pending.clear(); }
123
+ close() { this.lifetime.abort(); this.pending.clear(); void this.cache?.close().catch(error => this.options.onError?.(error)); }
71
124
  }
@@ -1,3 +1,4 @@
1
+ import { type MessageScope } from "./messageStore.js";
1
2
  export type ReadCountsResult = {
2
3
  appId: string;
3
4
  threadId: string;
@@ -11,6 +12,7 @@ export type ReadCountsOptions = {
11
12
  clientInstanceId: string;
12
13
  hubHttpUrl: string;
13
14
  fetch?: typeof fetch;
15
+ scope?: MessageScope;
14
16
  onChange: (value: ReadCountsResult) => void;
15
17
  onError?: (error: unknown) => void;
16
18
  };
@@ -31,7 +33,15 @@ export declare class ReadCountsClient {
31
33
  private subscribed;
32
34
  private subscriptionDenied;
33
35
  private subscriptionPending?;
36
+ private cache?;
37
+ private lastResult?;
38
+ private publish;
39
+ private revoked;
40
+ private accessVersion;
41
+ private requestedRevision;
42
+ private force;
34
43
  constructor(options: ReadCountsOptions);
44
+ private denyCache;
35
45
  private post;
36
46
  private subscription;
37
47
  private renew;
@@ -1,3 +1,5 @@
1
+ import { IndexedDbResponseCache } from "./responseCache.js";
2
+ import { messageScopeKey } from "./messageStore.js";
1
3
  let subscriptionSequence = 0;
2
4
  function nextSubscriptionSequence() { subscriptionSequence = Math.max(Date.now() * 1000, subscriptionSequence + 1); return subscriptionSequence; }
3
5
  /** One active conversation; fixed event window, bounded batches, no count polling. */
@@ -17,8 +19,31 @@ export class ReadCountsClient {
17
19
  subscribed = false;
18
20
  subscriptionDenied = false;
19
21
  subscriptionPending;
22
+ cache;
23
+ lastResult;
24
+ publish(value) {
25
+ if (JSON.stringify(value) === JSON.stringify(this.lastResult))
26
+ return;
27
+ this.lastResult = value;
28
+ this.options.onChange(value);
29
+ }
30
+ revoked = false;
31
+ accessVersion = 0;
32
+ requestedRevision = -1;
33
+ force = false;
20
34
  constructor(options) {
21
35
  this.options = options;
36
+ if (options.scope) {
37
+ if (options.scope.appId !== options.appId || options.scope.profileId !== options.profileId)
38
+ throw new Error("READ_COUNTS_SCOPE_MISMATCH");
39
+ this.cache = new IndexedDbResponseCache(messageScopeKey(options.scope));
40
+ }
41
+ }
42
+ denyCache() {
43
+ this.revoked = true;
44
+ this.accessVersion++;
45
+ this.lastResult = undefined;
46
+ void this.cache?.invalidate().catch(error => this.options.onError?.(error));
22
47
  }
23
48
  async post(path, body, signal) {
24
49
  const response = await (this.options.fetch ?? fetch)(`${this.options.hubHttpUrl.replace(/\/$/, "")}${path}`, { method: "POST", headers: { authorization: `Bearer ${this.options.token}`, "content-type": "application/json" }, body: JSON.stringify(body), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(8000)]) : AbortSignal.timeout(8000), redirect: "error" });
@@ -33,7 +58,7 @@ export class ReadCountsClient {
33
58
  return this.post("/api/social/reading/subscription", { appId: this.options.appId, profileId: this.options.profileId, threadId, clientInstanceId: this.options.clientInstanceId, sequence: nextSubscriptionSequence(), release });
34
59
  }
35
60
  renew() {
36
- if (!this.thread || this.closed || this.subscriptionDenied)
61
+ if (!this.thread || this.closed || this.subscriptionDenied || (typeof navigator !== "undefined" && navigator.onLine === false))
37
62
  return;
38
63
  const threadId = this.thread, generation = this.generation;
39
64
  if (this.subscriptionPending?.generation === generation)
@@ -56,6 +81,7 @@ export class ReadCountsClient {
56
81
  this.subscribed = false;
57
82
  if (/READ_COUNTS_HTTP_(401|403)/.test(String(error))) {
58
83
  this.subscriptionDenied = true;
84
+ this.denyCache();
59
85
  clearInterval(this.heartbeat);
60
86
  this.heartbeat = undefined;
61
87
  }
@@ -83,9 +109,12 @@ export class ReadCountsClient {
83
109
  clearTimeout(this.timer);
84
110
  clearInterval(this.heartbeat);
85
111
  this.timer = undefined;
112
+ this.lastResult = undefined;
86
113
  this.thread = threadId;
87
114
  this.ids = ids;
88
115
  this.revision = -1;
116
+ this.requestedRevision = -1;
117
+ this.force = false;
89
118
  this.dirty = false;
90
119
  this.retries = 0;
91
120
  this.subscribed = false;
@@ -104,11 +133,13 @@ export class ReadCountsClient {
104
133
  throw new Error("INVALID_READ_COUNTS_EVENT");
105
134
  if (threadId !== this.thread || revision <= this.revision || this.closed)
106
135
  return;
136
+ this.requestedRevision = Math.max(this.requestedRevision, revision);
107
137
  this.schedule(3000);
108
138
  }
109
139
  reconnect() {
110
140
  if (!this.thread || this.closed)
111
141
  return;
142
+ this.force = true;
112
143
  this.subscribed = false;
113
144
  this.subscriptionDenied = false;
114
145
  this.renew();
@@ -137,20 +168,44 @@ export class ReadCountsClient {
137
168
  let retryDelay;
138
169
  const controller = new AbortController();
139
170
  this.controller = controller;
171
+ const accessVersion = this.accessVersion, force = this.force;
172
+ this.force = false;
173
+ const key = JSON.stringify(["counts", threadId, [...ids].sort()]);
140
174
  try {
175
+ const stored = this.cache ? await this.cache.read(key).catch(error => { this.options.onError?.(error); return undefined; }) : undefined;
176
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
177
+ return;
178
+ const cached = this.revoked || (stored?.cached && stored.cached.value.revision < this.revision) ? undefined : stored?.cached;
179
+ if (cached) {
180
+ this.revision = cached.value.revision;
181
+ this.publish(cached.value);
182
+ const offline = typeof navigator !== "undefined" && navigator.onLine === false;
183
+ if (offline || (!force && this.requestedRevision <= this.revision && Date.now() - cached.time < 5000))
184
+ return;
185
+ }
141
186
  const value = await this.post("/api/social/reading/counts", { appId: this.options.appId, profileId: this.options.profileId, threadId, messageIds: ids }, controller.signal);
142
- if (generation !== this.generation || this.closed)
187
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
143
188
  return;
144
189
  if (value?.appId !== this.options.appId || value.threadId !== threadId || !Number.isSafeInteger(value.revision) || value.revision < 0 || !value.counts || typeof value.counts !== "object" || ids.some(id => !Number.isSafeInteger(value.counts[id]) || value.counts[id] < 0) || Object.keys(value.counts).some(id => !ids.includes(id)))
145
190
  throw new Error("INVALID_READ_COUNTS_RESULT");
191
+ if (value.revision < this.revision)
192
+ return;
146
193
  this.retries = 0;
194
+ this.revoked = false;
195
+ if (stored)
196
+ await this.cache?.write(key, value, stored.epoch).catch(error => { if (String(error).includes("RESPONSE_CACHE_INVALIDATED"))
197
+ throw error; this.options.onError?.(error); });
198
+ if (generation !== this.generation || this.closed || accessVersion !== this.accessVersion)
199
+ return;
147
200
  this.revision = value.revision;
148
- this.options.onChange(value);
201
+ this.publish(value);
149
202
  if (!this.subscribed)
150
203
  this.renew();
151
204
  }
152
205
  catch (error) {
153
206
  if (!controller.signal.aborted && generation === this.generation && !this.closed) {
207
+ if (/READ_COUNTS_HTTP_(401|403)/.test(String(error)))
208
+ this.denyCache();
154
209
  this.options.onError?.(error);
155
210
  if (!/READ_COUNTS_HTTP_4\d\d|INVALID_READ_COUNTS/.test(String(error)) && this.retries < 3) {
156
211
  retryDelay = 1000 * 2 ** this.retries++;
@@ -178,5 +233,6 @@ export class ReadCountsClient {
178
233
  if (old)
179
234
  void this.subscription(old, true).catch(() => { });
180
235
  this.thread = undefined;
236
+ void this.cache?.close().catch(error => this.options.onError?.(error));
181
237
  }
182
238
  }
@@ -0,0 +1,27 @@
1
+ /** Small response windows, isolated by identity. Epochs fence late writes after
2
+ * access withdrawal. No localStorage or synchronous writes on the read path. */
3
+ export declare class IndexedDbResponseCache<T> {
4
+ private scope;
5
+ private db;
6
+ private rows;
7
+ private epochs;
8
+ private pending;
9
+ private tasks;
10
+ private running?;
11
+ private closed;
12
+ constructor(scope: string, options?: {
13
+ indexedDB?: IDBFactory;
14
+ name?: string;
15
+ });
16
+ read(key: string): Promise<{
17
+ epoch: number;
18
+ cached: {
19
+ value: T;
20
+ time: number;
21
+ } | undefined;
22
+ }>;
23
+ write(key: string, value: T, epoch: number): Promise<void>;
24
+ invalidate(): Promise<void>;
25
+ flush(): Promise<void>;
26
+ close(): Promise<void>;
27
+ }
@@ -0,0 +1,103 @@
1
+ import { Dexie } from "dexie";
2
+ import { WindowedTasks } from "./coalescer.js";
3
+ /** Small response windows, isolated by identity. Epochs fence late writes after
4
+ * access withdrawal. No localStorage or synchronous writes on the read path. */
5
+ export class IndexedDbResponseCache {
6
+ scope;
7
+ db;
8
+ rows;
9
+ epochs;
10
+ pending = [];
11
+ tasks = new WindowedTasks(16, () => { });
12
+ running;
13
+ closed = false;
14
+ constructor(scope, options = {}) {
15
+ this.scope = scope;
16
+ this.db = new Dexie(options.name ?? "pmate-chat-response-windows", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
17
+ this.db.version(1).stores({ windows: "[scope+key],scope", epochs: "scope" });
18
+ this.rows = this.db.table("windows");
19
+ this.epochs = this.db.table("epochs");
20
+ }
21
+ async read(key) {
22
+ if (this.closed)
23
+ throw new Error("RESPONSE_CACHE_CLOSED");
24
+ return this.db.transaction("r", this.rows, this.epochs, async () => {
25
+ const epoch = (await this.epochs.get(this.scope))?.epoch ?? 0;
26
+ const row = await this.rows.get([this.scope, key]);
27
+ return { epoch, cached: row?.epoch === epoch ? { value: row.value, time: row.time } : undefined };
28
+ });
29
+ }
30
+ write(key, value, epoch) {
31
+ if (this.closed)
32
+ return Promise.reject(new Error("RESPONSE_CACHE_CLOSED"));
33
+ const copy = structuredClone(value), bytes = new TextEncoder().encode(JSON.stringify(copy)).byteLength;
34
+ // Count in-flight data as well as queued data; requests cannot create an
35
+ // unbounded backlog while IndexedDB is blocked by another tab.
36
+ if (bytes > 65536 || this.pending.length >= 128 || this.pending.reduce((n, w) => n + w.row.bytes, 0) + bytes > 1048576)
37
+ return Promise.reject(new Error("RESPONSE_CACHE_BACKPRESSURE"));
38
+ const promise = new Promise((resolve, reject) => this.pending.push({ row: { scope: this.scope, key, epoch, time: Date.now(), bytes, value: copy }, resolve, reject }));
39
+ this.tasks.schedule("write", () => this.flush());
40
+ return promise;
41
+ }
42
+ async invalidate() {
43
+ if (this.closed)
44
+ throw new Error("RESPONSE_CACHE_CLOSED");
45
+ await this.db.transaction("rw", this.rows, this.epochs, async () => {
46
+ const epoch = ((await this.epochs.get(this.scope))?.epoch ?? 0) + 1;
47
+ await this.epochs.put({ scope: this.scope, epoch });
48
+ await this.rows.where("scope").equals(this.scope).delete();
49
+ });
50
+ }
51
+ async flush() {
52
+ if (this.running) {
53
+ await this.running;
54
+ if (this.pending.length)
55
+ return this.flush();
56
+ return;
57
+ }
58
+ const batch = [...this.pending];
59
+ if (!batch.length)
60
+ return;
61
+ const task = (async () => {
62
+ try {
63
+ const accepted = await this.db.transaction("rw", this.rows, this.epochs, async () => {
64
+ const epoch = (await this.epochs.get(this.scope))?.epoch ?? 0;
65
+ const valid = batch.filter(write => write.row.epoch === epoch);
66
+ const unique = new Map(valid.map(write => [write.row.key, write.row]));
67
+ await this.rows.bulkPut([...unique.values()]);
68
+ const stored = await this.rows.where("scope").equals(this.scope).toArray();
69
+ stored.sort((a, b) => b.time - a.time || a.key.localeCompare(b.key));
70
+ let bytes = 0;
71
+ const evicted = [];
72
+ stored.forEach((row, index) => { bytes += row.bytes; if (index >= 256 || bytes > 1048576)
73
+ evicted.push([this.scope, row.key]); });
74
+ await this.rows.bulkDelete(evicted);
75
+ return new Set(valid);
76
+ });
77
+ batch.forEach(write => accepted.has(write) ? write.resolve() : write.reject(new Error("RESPONSE_CACHE_INVALIDATED")));
78
+ }
79
+ catch (error) {
80
+ batch.forEach(write => write.reject(error));
81
+ throw error;
82
+ }
83
+ finally {
84
+ this.pending.splice(0, batch.length);
85
+ }
86
+ })().finally(() => { if (this.running === task)
87
+ this.running = undefined; });
88
+ this.running = task;
89
+ return task;
90
+ }
91
+ async close() {
92
+ if (this.closed)
93
+ return;
94
+ this.closed = true;
95
+ this.tasks.close();
96
+ try {
97
+ await this.flush();
98
+ }
99
+ finally {
100
+ this.db.close();
101
+ }
102
+ }
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.7",
3
+ "version": "0.12.0-next.9",
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",