@pmate/chat-sdk 0.12.0-next.54 → 0.12.0-next.55
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.
|
@@ -46,6 +46,11 @@ export declare class DurableChatSender {
|
|
|
46
46
|
private uploads;
|
|
47
47
|
private reconciliations;
|
|
48
48
|
private firstDelivery;
|
|
49
|
+
private acceptanceQueue;
|
|
50
|
+
private acceptanceTimer?;
|
|
51
|
+
private acceptanceRunning?;
|
|
52
|
+
private acceptanceCount;
|
|
53
|
+
private acceptanceBytes;
|
|
49
54
|
private uploadTail;
|
|
50
55
|
private uploadWaiting;
|
|
51
56
|
private confirmationQueue;
|
|
@@ -88,6 +93,9 @@ export declare class DurableChatSender {
|
|
|
88
93
|
/** Compatibility API: resolves only after transport delivery. */
|
|
89
94
|
send(threadId: string, input: SendMessageInput): Promise<ChatRuntimeMessage>;
|
|
90
95
|
private persistOutgoing;
|
|
96
|
+
private scheduleAcceptances;
|
|
97
|
+
private failAcceptances;
|
|
98
|
+
private drainAcceptances;
|
|
91
99
|
retry(hash: string): Promise<ChatRuntimeMessage>;
|
|
92
100
|
private transmit;
|
|
93
101
|
private reconcile;
|
|
@@ -16,6 +16,11 @@ export class DurableChatSender {
|
|
|
16
16
|
uploads = new Set();
|
|
17
17
|
reconciliations = new Set();
|
|
18
18
|
firstDelivery = new Set();
|
|
19
|
+
acceptanceQueue = [];
|
|
20
|
+
acceptanceTimer;
|
|
21
|
+
acceptanceRunning;
|
|
22
|
+
acceptanceCount = 0;
|
|
23
|
+
acceptanceBytes = 0;
|
|
19
24
|
uploadTail = Promise.resolve();
|
|
20
25
|
uploadWaiting = 0;
|
|
21
26
|
confirmationQueue = [];
|
|
@@ -189,6 +194,7 @@ export class DurableChatSender {
|
|
|
189
194
|
/** Stop disclosure immediately; durable invalidation is coordinated separately. */
|
|
190
195
|
withdraw() {
|
|
191
196
|
this.withdrawn = true;
|
|
197
|
+
this.failAcceptances(new Error("OUTBOX_ACCESS_REVOKED"));
|
|
192
198
|
this.failConfirmations(new Error("OUTBOX_ACCESS_REVOKED"));
|
|
193
199
|
this.uploads.forEach(controller => controller.abort());
|
|
194
200
|
this.reconciliations.forEach(controller => controller.abort());
|
|
@@ -249,6 +255,13 @@ export class DurableChatSender {
|
|
|
249
255
|
persistOutgoing(threadId, input) {
|
|
250
256
|
if (this.closed || this.closing || this.withdrawn)
|
|
251
257
|
return Promise.reject(new Error("OUTBOX_ACCESS_REVOKED"));
|
|
258
|
+
if (input.voiceBlob && (!(input.voiceBlob instanceof Blob) || input.voiceBlob.size === 0 || input.voiceBlob.size > 16 * 1024 * 1024))
|
|
259
|
+
return Promise.reject(new Error("INVALID_VOICE_ATTACHMENT"));
|
|
260
|
+
const bytes = input.voiceBlob?.size ?? 0;
|
|
261
|
+
if (this.acceptanceCount >= 4096 || this.acceptanceBytes + bytes > 64 * 1024 * 1024)
|
|
262
|
+
return Promise.reject(new Error("OUTBOX_ACCEPTANCE_BACKPRESSURE"));
|
|
263
|
+
this.acceptanceCount++;
|
|
264
|
+
this.acceptanceBytes += bytes;
|
|
252
265
|
const pending = (async () => {
|
|
253
266
|
await this.ready;
|
|
254
267
|
await this.checkAccess();
|
|
@@ -260,41 +273,93 @@ export class DurableChatSender {
|
|
|
260
273
|
message.hash = commandId(this.scope, this.version, threadId, message);
|
|
261
274
|
if (input.voiceBlob) {
|
|
262
275
|
const body = message.body;
|
|
263
|
-
if (
|
|
276
|
+
if (body.kind !== "message" || !validVoice(body.content.voice))
|
|
264
277
|
throw new Error("INVALID_VOICE_ATTACHMENT");
|
|
265
278
|
}
|
|
266
279
|
const record = { ...(input.voiceBlob ? { voiceData: { bytes: await input.voiceBlob.arrayBuffer(), type: input.voiceBlob.type } } : {}), scope: this.scope, threadId, hash: message.hash, message, status: "sending", accessVersion: this.version };
|
|
267
|
-
const durable = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
const durable = await new Promise((resolve, reject) => {
|
|
281
|
+
this.acceptanceQueue.push({ record, resolve, reject });
|
|
282
|
+
this.scheduleAcceptances();
|
|
283
|
+
});
|
|
284
|
+
return durable.message;
|
|
285
|
+
})().finally(() => { this.accepted.delete(pending); this.acceptanceCount--; this.acceptanceBytes -= bytes; });
|
|
286
|
+
this.accepted.add(pending);
|
|
287
|
+
return pending;
|
|
288
|
+
}
|
|
289
|
+
scheduleAcceptances() {
|
|
290
|
+
if (this.acceptanceTimer || this.acceptanceRunning || !this.acceptanceQueue.length)
|
|
291
|
+
return;
|
|
292
|
+
this.acceptanceTimer = setTimeout(() => { this.acceptanceTimer = undefined; void this.drainAcceptances(); }, 16);
|
|
293
|
+
}
|
|
294
|
+
failAcceptances(error) {
|
|
295
|
+
clearTimeout(this.acceptanceTimer);
|
|
296
|
+
this.acceptanceTimer = undefined;
|
|
297
|
+
for (const item of this.acceptanceQueue.splice(0))
|
|
298
|
+
item.reject(error);
|
|
299
|
+
}
|
|
300
|
+
drainAcceptances() {
|
|
301
|
+
const task = (async () => {
|
|
302
|
+
let bytes = 0, count = 0;
|
|
303
|
+
for (const item of this.acceptanceQueue) {
|
|
304
|
+
const size = item.record.voiceData?.bytes.byteLength ?? 0;
|
|
305
|
+
if (count >= 128 || (count > 0 && bytes + size > 16 * 1024 * 1024))
|
|
306
|
+
break;
|
|
307
|
+
bytes += size;
|
|
308
|
+
count++;
|
|
309
|
+
}
|
|
310
|
+
const batch = this.acceptanceQueue.splice(0, count);
|
|
311
|
+
try {
|
|
312
|
+
const result = await this.withEpoch("rw", async () => {
|
|
313
|
+
const commands = batch.filter(item => statefulGroup(item.record.message));
|
|
314
|
+
const previous = await this.outbox.bulkGet(commands.map(item => [this.scope, item.record.hash]));
|
|
315
|
+
const known = new Map(previous.filter((row) => Boolean(row && (row.accessVersion ?? 0) === this.version)).map(row => [row.hash, row]));
|
|
316
|
+
const writes = [], records = [];
|
|
317
|
+
for (const { record } of batch) {
|
|
318
|
+
const command = statefulGroup(record.message), old = command ? known.get(record.hash) : undefined;
|
|
319
|
+
if (old) {
|
|
320
|
+
if (old.message.from !== this.identity.profileId || old.message.hash !== record.hash || commandId(this.scope, this.version, record.threadId, old.message) !== record.hash)
|
|
321
|
+
throw new Error("OUTGOING_COMMAND_CONFLICT");
|
|
322
|
+
records.push(old);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
writes.push(record);
|
|
326
|
+
records.push(record);
|
|
327
|
+
if (command)
|
|
328
|
+
known.set(record.hash, record);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (writes.length)
|
|
332
|
+
await this.outbox.bulkPut(writes);
|
|
333
|
+
return { writes, records };
|
|
334
|
+
});
|
|
335
|
+
await this.checkAccess();
|
|
336
|
+
for (const record of result.writes)
|
|
337
|
+
if (statefulGroup(record.message))
|
|
338
|
+
this.firstDelivery.add(record.hash);
|
|
339
|
+
let changed = false;
|
|
340
|
+
for (const record of result.records) {
|
|
341
|
+
if (record.confirmed) {
|
|
342
|
+
this.firstDelivery.delete(record.hash);
|
|
343
|
+
changed = this.values.delete(record.hash) || changed;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const current = this.values.get(record.hash);
|
|
347
|
+
if (!current || current.status !== record.status || current.accessVersion !== record.accessVersion) {
|
|
348
|
+
this.values.set(record.hash, record);
|
|
349
|
+
changed = true;
|
|
350
|
+
}
|
|
274
351
|
}
|
|
275
352
|
}
|
|
276
|
-
|
|
277
|
-
return record;
|
|
278
|
-
});
|
|
279
|
-
await this.checkAccess();
|
|
280
|
-
if (command && durable === record)
|
|
281
|
-
this.firstDelivery.add(record.hash);
|
|
282
|
-
if (durable.confirmed) {
|
|
283
|
-
this.firstDelivery.delete(durable.hash);
|
|
284
|
-
if (this.values.delete(durable.hash))
|
|
353
|
+
if (changed)
|
|
285
354
|
this.emit();
|
|
355
|
+
batch.forEach((item, index) => item.resolve(result.records[index]));
|
|
286
356
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (!current || current.status !== durable.status || current.accessVersion !== durable.accessVersion) {
|
|
290
|
-
this.values.set(durable.hash, durable);
|
|
291
|
-
this.emit();
|
|
292
|
-
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
batch.forEach(item => item.reject(error));
|
|
293
359
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return pending;
|
|
360
|
+
})().finally(() => { this.acceptanceRunning = undefined; this.scheduleAcceptances(); });
|
|
361
|
+
this.acceptanceRunning = task;
|
|
362
|
+
return task;
|
|
298
363
|
}
|
|
299
364
|
retry(hash) {
|
|
300
365
|
if (this.closed || this.closing || this.withdrawn)
|
package/package.json
CHANGED