@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.
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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