@pmate/chat-sdk 0.12.0-next.50 → 0.12.0-next.52

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.
@@ -115,6 +115,7 @@ export declare class ChatSession<T extends StoredMessage> {
115
115
  all?: boolean;
116
116
  refresh?: boolean;
117
117
  }): Promise<MessageSnapshot<T>>;
118
+ private confirmMessages;
118
119
  private rememberHistoryEdge;
119
120
  private readEarlier;
120
121
  private readAll;
@@ -104,7 +104,8 @@ export class ChatSession {
104
104
  void this.messages.receive(decoded.threadId, [decoded]).then(async () => {
105
105
  if (!this.closed && this.readingStarted)
106
106
  this.reading.refresh();
107
- await this.sender.confirm([message.hash]);
107
+ if (message.from === scope.profileId)
108
+ await this.sender.confirm([message.hash]);
108
109
  }).catch(error => { if (String(error).includes("MESSAGE_PROJECTION_CHANGED"))
109
110
  this.messages.invalidate(decoded.threadId);
110
111
  else if (!this.closed)
@@ -254,7 +255,7 @@ export class ChatSession {
254
255
  }
255
256
  const page = await (options.before ? this.messages.earlier(threadId, options.before, options.limit) : this.messages.read(threadId, options.limit));
256
257
  this.assertOpen();
257
- await this.sender.confirm(page.messages.map(message => message.id));
258
+ await this.confirmMessages(page.messages);
258
259
  if (this.initializing && !this.readingStarted && page.complete)
259
260
  await this.startReading();
260
261
  this.assertOpen();
@@ -263,6 +264,16 @@ export class ChatSession {
263
264
  this.rememberHistoryEdge(threadId, page);
264
265
  return page;
265
266
  }
267
+ async confirmMessages(messages) {
268
+ await this.sender.initialize();
269
+ // Only this profile can create its outgoing wire hashes. Keep the legacy
270
+ // fallback when a generic host has not supplied an author on stored rows.
271
+ const hashes = messages.filter(message => {
272
+ const from = message.from;
273
+ return typeof from !== "string" || !from || from === this.options.messages.scope.profileId;
274
+ }).map(message => message.id);
275
+ return hashes.length ? this.sender.confirm(hashes) : Promise.resolve();
276
+ }
266
277
  rememberHistoryEdge(threadId, page) {
267
278
  const first = page.messages[0];
268
279
  this.historyEdges.set(threadId, { before: first ? { timestamp: first.timestamp, id: first.id } : undefined, version: page.resetVersion, hasEarlier: page.hasEarlier });
@@ -281,7 +292,7 @@ export class ChatSession {
281
292
  if (!edge.before)
282
293
  throw new Error("MESSAGE_SYNC_NO_PROGRESS");
283
294
  const page = await this.messages.earlier(threadId, edge.before, 100, true);
284
- await this.sender.confirm(page.messages.map(message => message.id));
295
+ await this.confirmMessages(page.messages);
285
296
  this.assertOpen();
286
297
  this.rememberHistoryEdge(threadId, page);
287
298
  return page;
@@ -45,6 +45,10 @@ export declare class DurableChatSender {
45
45
  private uploads;
46
46
  private uploadTail;
47
47
  private uploadWaiting;
48
+ private confirmationQueue;
49
+ private confirmationTimer?;
50
+ private confirmationRunning?;
51
+ private confirmationSize;
48
52
  private version;
49
53
  private withdrawn;
50
54
  private revoking?;
@@ -91,6 +95,11 @@ export declare class DurableChatSender {
91
95
  /** Confirm only after the authoritative message has been durably merged.
92
96
  * A tombstone prevents another tab's late failure from recreating the outbox. */
93
97
  confirm(hashes: string[]): Promise<void>;
98
+ private scheduleConfirmations;
99
+ private drainConfirmations;
100
+ private failConfirmations;
101
+ private flushConfirmations;
102
+ private confirmNow;
94
103
  private resume;
95
104
  close(): Promise<void>;
96
105
  }
@@ -14,6 +14,10 @@ export class DurableChatSender {
14
14
  uploads = new Set();
15
15
  uploadTail = Promise.resolve();
16
16
  uploadWaiting = 0;
17
+ confirmationQueue = [];
18
+ confirmationTimer;
19
+ confirmationRunning;
20
+ confirmationSize = 0;
17
21
  version = 0;
18
22
  withdrawn = false;
19
23
  revoking;
@@ -181,6 +185,7 @@ export class DurableChatSender {
181
185
  /** Stop disclosure immediately; durable invalidation is coordinated separately. */
182
186
  withdraw() {
183
187
  this.withdrawn = true;
188
+ this.failConfirmations(new Error("OUTBOX_ACCESS_REVOKED"));
184
189
  this.uploads.forEach(controller => controller.abort());
185
190
  this.releaseUrls();
186
191
  this.values.clear();
@@ -419,10 +424,67 @@ export class DurableChatSender {
419
424
  }
420
425
  /** Confirm only after the authoritative message has been durably merged.
421
426
  * A tombstone prevents another tab's late failure from recreating the outbox. */
422
- async confirm(hashes) {
427
+ confirm(hashes) {
423
428
  hashes = [...new Set(hashes)];
424
429
  if (!hashes.length)
430
+ return Promise.resolve();
431
+ if (this.closed || this.closing || this.withdrawn)
432
+ return Promise.reject(new Error("OUTBOX_ACCESS_REVOKED"));
433
+ if (this.confirmationSize + hashes.length > 4096)
434
+ return Promise.reject(new Error("OUTBOX_CONFIRM_BACKPRESSURE"));
435
+ const pending = new Promise((resolve, reject) => this.confirmationQueue.push({ hashes, resolve, reject }));
436
+ this.confirmationSize += hashes.length;
437
+ this.scheduleConfirmations();
438
+ return pending;
439
+ }
440
+ scheduleConfirmations() {
441
+ if (this.confirmationTimer !== undefined || this.confirmationRunning || !this.confirmationQueue.length)
425
442
  return;
443
+ this.confirmationTimer = setTimeout(() => { this.confirmationTimer = undefined; void this.drainConfirmations(); }, 16);
444
+ }
445
+ drainConfirmations() {
446
+ if (this.confirmationRunning)
447
+ return this.confirmationRunning;
448
+ const batch = this.confirmationQueue.splice(0);
449
+ if (!batch.length)
450
+ return Promise.resolve();
451
+ const size = batch.reduce((sum, item) => sum + item.hashes.length, 0);
452
+ return this.confirmationRunning = (async () => {
453
+ let failed = false, error;
454
+ try {
455
+ await this.confirmNow([...new Set(batch.flatMap(item => item.hashes))]);
456
+ }
457
+ catch (reason) {
458
+ failed = true;
459
+ error = reason;
460
+ }
461
+ this.confirmationSize -= size;
462
+ for (const item of batch)
463
+ if (failed)
464
+ item.reject(error);
465
+ else
466
+ item.resolve();
467
+ })().finally(() => {
468
+ this.confirmationRunning = undefined;
469
+ this.scheduleConfirmations();
470
+ });
471
+ }
472
+ failConfirmations(error) {
473
+ clearTimeout(this.confirmationTimer);
474
+ this.confirmationTimer = undefined;
475
+ for (const item of this.confirmationQueue.splice(0)) {
476
+ this.confirmationSize -= item.hashes.length;
477
+ item.reject(error);
478
+ }
479
+ }
480
+ async flushConfirmations() {
481
+ while (this.confirmationRunning || this.confirmationQueue.length) {
482
+ clearTimeout(this.confirmationTimer);
483
+ this.confirmationTimer = undefined;
484
+ await (this.confirmationRunning ?? this.drainConfirmations());
485
+ }
486
+ }
487
+ async confirmNow(hashes) {
426
488
  await this.ready;
427
489
  let changed = false;
428
490
  try {
@@ -477,10 +539,12 @@ export class DurableChatSender {
477
539
  this.channel = undefined;
478
540
  try {
479
541
  await this.ready;
542
+ await this.flushConfirmations();
480
543
  await Promise.allSettled([...this.accepted, ...this.running.values(), ...(this.revoking ? [this.revoking] : [])]);
481
544
  }
482
545
  finally {
483
546
  this.closed = true;
547
+ this.failConfirmations(new Error("CHAT_SENDER_CLOSED"));
484
548
  this.releaseUrls();
485
549
  this.values.clear();
486
550
  this.snapshots.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.50",
3
+ "version": "0.12.0-next.52",
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",