@pmate/chat-sdk 0.12.0-next.3 → 0.12.0-next.4

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.
@@ -71,6 +71,7 @@ export class ChatSession {
71
71
  async getMessages(threadId, options = {}) {
72
72
  this.assertOpen();
73
73
  const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
74
+ await this.sender.confirm(page.messages.map(message => message.id));
74
75
  if (!options.before)
75
76
  this.publish(threadId);
76
77
  return page;
@@ -143,10 +143,13 @@ export class DurableChatSender {
143
143
  /** Confirm only after the authoritative message has been durably merged.
144
144
  * A tombstone prevents another tab's late failure from recreating the outbox. */
145
145
  async confirm(hashes) {
146
+ hashes = [...new Set(hashes)];
147
+ if (!hashes.length)
148
+ return;
146
149
  await this.ready;
147
150
  await this.db.transaction("rw", this.outbox, async () => {
148
151
  const previous = await this.outbox.bulkGet(hashes.map(hash => [this.scope, hash]));
149
- const confirmed = previous.filter((record) => Boolean(record)).map(record => ({ ...record, status: "sent", confirmed: true }));
152
+ const confirmed = previous.filter((record) => Boolean(record && !record.confirmed)).map(record => ({ ...record, status: "sent", confirmed: true }));
150
153
  if (confirmed.length)
151
154
  await this.outbox.bulkPut(confirmed);
152
155
  });
@@ -27,6 +27,7 @@ export type MessageClientOptions<T extends StoredMessage> = {
27
27
  syncWindowMs?: number;
28
28
  transactionBytes?: number;
29
29
  queueBytes?: number;
30
+ freshnessMs?: number;
30
31
  };
31
32
  /** Local-first message facade; hosts never select a cache or network source. */
32
33
  export declare class MessageClient<T extends StoredMessage> {
@@ -66,6 +66,9 @@ export class MessageClient {
66
66
  throw new Error("MESSAGE_CLIENT_CLOSED");
67
67
  const result = await this.fillWindow(thread, limit);
68
68
  const snapshot = await this.reload(thread);
69
+ if (local.coverage?.complete && Date.now() - (local.coverage.syncedAt ?? 0) > (this.options.freshnessMs ?? 30_000)
70
+ && (typeof navigator === "undefined" || navigator.onLine !== false))
71
+ this.invalidate(thread);
69
72
  return limit === 100 ? snapshot : result;
70
73
  })().finally(() => { if (this.reads.get(key) === task)
71
74
  this.reads.delete(key); });
@@ -105,17 +108,20 @@ export class MessageClient {
105
108
  if (this.closed || this.closing || this.revoked.has(thread))
106
109
  return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
107
110
  const queue = this.writes.get(thread) ?? [];
111
+ if (messages.length > 1000 || this.pendingRecords + messages.length > 4096)
112
+ return Promise.reject(new Error("MESSAGE_STORAGE_BACKPRESSURE"));
108
113
  let bytes = 0;
109
114
  try {
110
- for (const message of messages)
115
+ for (const message of messages) {
111
116
  bytes += this.messageBytes(message);
117
+ if (this.pendingBytes + bytes > this.queueBytes)
118
+ throw new Error("MESSAGE_STORAGE_BACKPRESSURE");
119
+ }
112
120
  messages = structuredClone(messages);
113
121
  }
114
122
  catch (error) {
115
123
  return Promise.reject(error);
116
124
  }
117
- if (messages.length > 1000 || this.pendingRecords + messages.length > 4096 || this.pendingBytes + bytes > this.queueBytes)
118
- return Promise.reject(new Error("MESSAGE_STORAGE_BACKPRESSURE"));
119
125
  this.pendingBytes += bytes;
120
126
  this.pendingRecords += messages.length;
121
127
  const promise = new Promise((resolve, reject) => queue.push({ messages, bytes, resolve, reject }));
@@ -123,8 +129,9 @@ export class MessageClient {
123
129
  this.writeTasks.schedule(thread, () => this.flushWrites(thread));
124
130
  return promise;
125
131
  }
126
- async persist(thread, messages, checkpoint, complete, nextBefore) {
132
+ async persist(thread, messages, checkpoint, complete, nextBefore, syncedAt) {
127
133
  let changed = 0;
134
+ let navigationChanged = false;
128
135
  // Coverage advances only after all bounded batches have durably committed.
129
136
  let batch = [], bytes = 0;
130
137
  const commit = async () => {
@@ -148,9 +155,10 @@ export class MessageClient {
148
155
  const old = (await this.storage.read(thread, 1)).coverage;
149
156
  if (this.closed || this.revoked.has(thread))
150
157
  throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
151
- await this.storage.commit(thread, [], { complete: complete ?? old?.complete ?? false, checkpoint: checkpoint ?? old?.checkpoint, nextBefore: nextBefore === undefined ? old?.nextBefore : nextBefore });
158
+ navigationChanged = nextBefore !== undefined && nextBefore !== old?.nextBefore;
159
+ await this.storage.commit(thread, [], { syncedAt: syncedAt ?? old?.syncedAt, complete: complete ?? old?.complete ?? false, checkpoint: checkpoint ?? old?.checkpoint, nextBefore: nextBefore === undefined ? old?.nextBefore : nextBefore });
152
160
  }
153
- if (changed) {
161
+ if (changed || navigationChanged) {
154
162
  await this.reload(thread);
155
163
  this.channel?.postMessage(thread);
156
164
  }
@@ -213,7 +221,7 @@ export class MessageClient {
213
221
  throw new Error("MESSAGE_ACCESS_REVOKED");
214
222
  if (page.resetRequired)
215
223
  throw new Error("MESSAGE_SYNC_RESET_REQUIRED");
216
- await this.persist(thread, page.messages, page.checkpoint, true, after ? undefined : page.nextBefore);
224
+ await this.persist(thread, page.messages, page.checkpoint, true, after ? undefined : page.nextBefore, Date.now());
217
225
  if (!page.hasMoreChanges)
218
226
  break;
219
227
  if (!page.checkpoint || page.checkpoint === after || !this.options.transport.changes)
@@ -15,6 +15,7 @@ export type MessageCoverage = {
15
15
  checkpoint?: string;
16
16
  complete: boolean;
17
17
  nextBefore?: string | null;
18
+ syncedAt?: number;
18
19
  };
19
20
  type CoverageRow = MessageCoverage & {
20
21
  scope: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.3",
3
+ "version": "0.12.0-next.4",
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",