@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 (!(input.voiceBlob instanceof Blob) || input.voiceBlob.size === 0 || input.voiceBlob.size > 16 * 1024 * 1024 || body.kind !== "message" || !validVoice(body.content.voice))
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 this.withEpoch("rw", async () => {
268
- if (command) {
269
- const previous = await this.outbox.get([this.scope, message.hash]);
270
- if (previous && (previous.accessVersion ?? 0) === this.version) {
271
- if (previous.message.from !== this.identity.profileId || previous.message.hash !== message.hash || commandId(this.scope, this.version, threadId, previous.message) !== message.hash)
272
- throw new Error("OUTGOING_COMMAND_CONFLICT");
273
- return previous;
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
- await this.outbox.put(record);
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
- else {
288
- const current = this.values.get(durable.hash);
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
- return durable.message;
295
- })().finally(() => this.accepted.delete(pending));
296
- this.accepted.add(pending);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.54",
3
+ "version": "0.12.0-next.55",
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",