@fabriktor/fx 0.0.101 → 0.0.102

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.
@@ -53,6 +53,8 @@ export declare class ChatSendQueue {
53
53
  private listeners;
54
54
  private readonly queue;
55
55
  private uploads;
56
+ private sends;
57
+ private cancellations;
56
58
  private writes;
57
59
  private pendingSaves;
58
60
  private error?;
@@ -26,6 +26,8 @@ export class ChatSendQueue {
26
26
  listeners = new Set();
27
27
  queue;
28
28
  uploads = new Set();
29
+ sends = new Map();
30
+ cancellations = new Set();
29
31
  writes = Promise.resolve();
30
32
  pendingSaves = new Map();
31
33
  error;
@@ -213,38 +215,55 @@ export class ChatSendQueue {
213
215
  await this.refresh();
214
216
  }
215
217
  async remove(id) {
216
- const entry = this.entries.find((value) => value.id === id);
218
+ const owner = this.owner, signal = this.controller.signal;
219
+ let entry = this.entries.find((value) => value.id === id);
217
220
  if (!entry ||
218
- !this.enabled ||
219
- entry.status === ChatSendStatus.Saving ||
220
- entry.status === ChatSendStatus.Sending ||
221
- entry.status === ChatSendStatus.Uploading) {
221
+ !this.current(owner, signal) ||
222
+ !chatSendCanRemove(entry) ||
223
+ this.cancellations.has(id)) {
222
224
  return;
223
225
  }
224
- if (entry.sequence === undefined) {
225
- this.pendingSaves.delete(id);
226
- this.entries = this.entries.filter((value) => value.id !== id);
227
- this.publish();
228
- this.persist();
229
- return;
230
- }
231
- const signal = this.controller.signal, owner = this.owner;
232
- const removeStored = async () => {
233
- if (!this.current(owner, signal))
226
+ this.cancellations.add(id);
227
+ try {
228
+ // Finish a pending local save before removing its durable record. The
229
+ // cancellation flag prevents this tab from starting that message meanwhile.
230
+ if (entry.status === ChatSendStatus.Saving) {
231
+ await this.writes;
232
+ entry = this.entries.find((value) => value.id === id);
233
+ }
234
+ if (!entry || !this.current(owner, signal) || !chatSendCanRemove(entry)) {
234
235
  return;
235
- const stored = (await this.opts.store.list(owner)).find((value) => value.id === id);
236
- if (stored && !chatSendCanRemove(stored))
236
+ }
237
+ // Cancel only this request. A server response that already arrived wins;
238
+ // the shared room lock still protects removal from another tab's sender.
239
+ this.sends.get(id)?.abort();
240
+ if (entry.sequence === undefined) {
241
+ this.pendingSaves.delete(id);
242
+ this.entries = this.entries.filter((value) => value.id !== id);
243
+ this.publish();
244
+ this.persist();
237
245
  return;
238
- await this.opts.store.remove(id);
239
- this.entries = this.entries.filter((value) => value.id !== id);
240
- this.publish();
241
- this.opts.changed?.();
242
- };
243
- // Upload retries use their own lock so a slow multipart never holds the
244
- // message lane. Discard must coordinate with that same lock.
245
- await this.opts.lock(entry.chat ? this.uploadLock(entry) : this.roomLock(entry), signal, removeStored);
246
- this.persist();
247
- await this.refresh();
246
+ }
247
+ const removeStored = async () => {
248
+ if (!this.current(owner, signal))
249
+ return;
250
+ const stored = (await this.opts.store.list(owner)).find((value) => value.id === id);
251
+ if (stored && !chatSendCanRemove(stored))
252
+ return;
253
+ await this.opts.store.remove(id);
254
+ this.entries = this.entries.filter((value) => value.id !== id);
255
+ this.publish();
256
+ this.opts.changed?.();
257
+ };
258
+ // Upload retries use their own lock so a slow multipart never holds the
259
+ // message lane. Discard must coordinate with that same lock.
260
+ await this.opts.lock(entry.chat ? this.uploadLock(entry) : this.roomLock(entry), signal, removeStored);
261
+ }
262
+ finally {
263
+ this.cancellations.delete(id);
264
+ this.persist();
265
+ await this.refresh();
266
+ }
248
267
  }
249
268
  work(room, owner, signal) {
250
269
  this.queue.process({
@@ -264,7 +283,7 @@ export class ChatSendQueue {
264
283
  }
265
284
  },
266
285
  run: async (entry) => {
267
- if (!this.current(owner, signal)) {
286
+ if (!this.current(owner, signal) || this.cancellations.has(entry.id)) {
268
287
  return false;
269
288
  }
270
289
  if (entry.status === ChatSendStatus.Sending ||
@@ -278,6 +297,9 @@ export class ChatSendQueue {
278
297
  return false;
279
298
  }
280
299
  let current = entry;
300
+ const request = new AbortController();
301
+ const requestSignal = AbortSignal.any([signal, request.signal]);
302
+ this.sends.set(entry.id, request);
281
303
  try {
282
304
  current = {
283
305
  ...entry,
@@ -289,9 +311,10 @@ export class ChatSendQueue {
289
311
  return false;
290
312
  }
291
313
  if (!current.server_id) {
314
+ requestSignal.throwIfAborted();
292
315
  const out = await this.opts.chat.insertOneChat({
293
316
  in: current.input,
294
- signal,
317
+ signal: requestSignal,
295
318
  idempotency_key: current.id,
296
319
  });
297
320
  current = {
@@ -339,7 +362,7 @@ export class ChatSendQueue {
339
362
  try {
340
363
  await this.save({
341
364
  ...current,
342
- status: signal.aborted
365
+ status: requestSignal.aborted
343
366
  ? ChatSendStatus.Interrupted
344
367
  : ChatSendStatus.Failed,
345
368
  });
@@ -347,12 +370,15 @@ export class ChatSendQueue {
347
370
  catch (storageError) {
348
371
  this.error = storageError;
349
372
  }
350
- if (this.current(owner, signal)) {
373
+ if (this.current(owner, signal) && !request.signal.aborted) {
351
374
  this.error = err;
352
375
  this.publish();
353
376
  }
354
377
  return false;
355
378
  }
379
+ finally {
380
+ this.sends.delete(entry.id);
381
+ }
356
382
  return true;
357
383
  },
358
384
  });
@@ -518,5 +544,11 @@ export function chatSendNeedsRetry(entry) {
518
544
  entry.status === ChatSendStatus.UploadInterrupted);
519
545
  }
520
546
  export function chatSendCanRemove(entry) {
521
- return entry.status === ChatSendStatus.Queued || chatSendNeedsRetry(entry);
547
+ if (!entry.chat && entry.server_id) {
548
+ return false;
549
+ }
550
+ return (entry.status === ChatSendStatus.Saving ||
551
+ entry.status === ChatSendStatus.Queued ||
552
+ entry.status === ChatSendStatus.Sending ||
553
+ chatSendNeedsRetry(entry));
522
554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {