@fabriktor/fx 0.0.101 → 0.0.103
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.
|
@@ -55,9 +55,22 @@ export function projectQueuedChats(chats, snapshot, ownerID, roomID) {
|
|
|
55
55
|
}
|
|
56
56
|
const result = [...confirmed.values()].sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at) ||
|
|
57
57
|
a.id.localeCompare(b.id));
|
|
58
|
+
const sequences = new Map(entries
|
|
59
|
+
.filter((entry) => entry.server_id && entry.sequence !== undefined)
|
|
60
|
+
.map((entry) => [entry.server_id, entry.sequence]));
|
|
58
61
|
for (const entry of entries) {
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
// A room event may arrive before this entry's independent confirmation
|
|
63
|
+
// read. Match the server ID so identical messages remain distinct.
|
|
64
|
+
if (!entry.chat && (!entry.server_id || !confirmed.has(entry.server_id))) {
|
|
65
|
+
// Out-of-order reads must not move a later outgoing bubble ahead of an
|
|
66
|
+
// earlier pending one. Canonical messages keep their server ordering.
|
|
67
|
+
const before = result.findIndex((chat) => {
|
|
68
|
+
const sequence = sequences.get(chat.id) ?? snapshot.confirmed_sequences?.[chat.id];
|
|
69
|
+
return (sequence !== undefined &&
|
|
70
|
+
entry.sequence !== undefined &&
|
|
71
|
+
sequence > entry.sequence);
|
|
72
|
+
});
|
|
73
|
+
result.splice(before < 0 ? result.length : before, 0, entry.optimistic);
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
76
|
return result.length === chats.length &&
|
|
@@ -68,6 +81,7 @@ export function projectQueuedChats(chats, snapshot, ownerID, roomID) {
|
|
|
68
81
|
export function activeQueuedChat(snapshot, ownerID, roomID) {
|
|
69
82
|
return (snapshot.entries.find((entry) => entry.owner_id === ownerID &&
|
|
70
83
|
entry.input.chat_room_id === roomID &&
|
|
84
|
+
!entry.server_id &&
|
|
71
85
|
entry.status === ChatSendStatus.Sending)?.optimistic ?? null);
|
|
72
86
|
}
|
|
73
87
|
export function queuedChatsByID(snapshot) {
|
|
@@ -5,6 +5,7 @@ export declare const ChatSendStatus: {
|
|
|
5
5
|
readonly Saving: "saving";
|
|
6
6
|
readonly Queued: "queued";
|
|
7
7
|
readonly Sending: "sending";
|
|
8
|
+
readonly Confirming: "confirming";
|
|
8
9
|
readonly Failed: "failed";
|
|
9
10
|
readonly Interrupted: "interrupted";
|
|
10
11
|
readonly Uploading: "uploading";
|
|
@@ -35,6 +36,7 @@ export type ChatSendLock = QueueLock;
|
|
|
35
36
|
export type ChatSendSnapshot = {
|
|
36
37
|
entries: readonly QueuedChat[];
|
|
37
38
|
confirmed: readonly Chat[];
|
|
39
|
+
confirmed_sequences?: Readonly<Record<string, number>>;
|
|
38
40
|
error?: unknown;
|
|
39
41
|
};
|
|
40
42
|
export type ChatSendQueueOptions = {
|
|
@@ -50,9 +52,13 @@ export declare class ChatSendQueue {
|
|
|
50
52
|
private controller;
|
|
51
53
|
private entries;
|
|
52
54
|
private confirmed;
|
|
55
|
+
private confirmedSequences;
|
|
53
56
|
private listeners;
|
|
54
57
|
private readonly queue;
|
|
55
58
|
private uploads;
|
|
59
|
+
private confirmations;
|
|
60
|
+
private sends;
|
|
61
|
+
private cancellations;
|
|
56
62
|
private writes;
|
|
57
63
|
private pendingSaves;
|
|
58
64
|
private error?;
|
|
@@ -71,9 +77,11 @@ export declare class ChatSendQueue {
|
|
|
71
77
|
retry(id: string): Promise<void>;
|
|
72
78
|
remove(id: string): Promise<void>;
|
|
73
79
|
private work;
|
|
80
|
+
private startConfirmation;
|
|
74
81
|
private startUpload;
|
|
75
82
|
private recoverUpload;
|
|
76
83
|
private uploadLock;
|
|
84
|
+
private confirmationLock;
|
|
77
85
|
private roomLock;
|
|
78
86
|
private current;
|
|
79
87
|
private save;
|
|
@@ -10,22 +10,27 @@ export const ChatSendStatus = {
|
|
|
10
10
|
Saving: "saving",
|
|
11
11
|
Queued: "queued",
|
|
12
12
|
Sending: "sending",
|
|
13
|
+
Confirming: "confirming",
|
|
13
14
|
Failed: "failed",
|
|
14
15
|
Interrupted: "interrupted",
|
|
15
16
|
Uploading: "uploading",
|
|
16
17
|
UploadInterrupted: "upload_interrupted",
|
|
17
18
|
};
|
|
18
|
-
// Persistent FIFO per account and room.
|
|
19
|
-
// failed/uncertain head.
|
|
19
|
+
// Persistent creation FIFO per account and room. Creation never advances beyond
|
|
20
|
+
// a failed/uncertain head. Retrieval, other rooms and other users are independent.
|
|
20
21
|
export class ChatSendQueue {
|
|
21
22
|
opts;
|
|
22
23
|
owner = "";
|
|
23
24
|
controller = new AbortController();
|
|
24
25
|
entries = [];
|
|
25
26
|
confirmed = new Map();
|
|
27
|
+
confirmedSequences = new Map();
|
|
26
28
|
listeners = new Set();
|
|
27
29
|
queue;
|
|
28
30
|
uploads = new Set();
|
|
31
|
+
confirmations = new Set();
|
|
32
|
+
sends = new Map();
|
|
33
|
+
cancellations = new Set();
|
|
29
34
|
writes = Promise.resolve();
|
|
30
35
|
pendingSaves = new Map();
|
|
31
36
|
error;
|
|
@@ -42,6 +47,7 @@ export class ChatSendQueue {
|
|
|
42
47
|
return {
|
|
43
48
|
entries: this.entries,
|
|
44
49
|
confirmed: [...this.confirmed.values()],
|
|
50
|
+
confirmed_sequences: Object.fromEntries(this.confirmedSequences),
|
|
45
51
|
error: this.error,
|
|
46
52
|
};
|
|
47
53
|
}
|
|
@@ -59,6 +65,7 @@ export class ChatSendQueue {
|
|
|
59
65
|
if (this.owner !== ownerID) {
|
|
60
66
|
this.entries = [...this.pendingSaves.values()].filter((entry) => entry.owner_id === ownerID);
|
|
61
67
|
this.confirmed.clear();
|
|
68
|
+
this.confirmedSequences.clear();
|
|
62
69
|
this.error = undefined;
|
|
63
70
|
}
|
|
64
71
|
this.owner = ownerID;
|
|
@@ -155,17 +162,20 @@ export class ChatSendQueue {
|
|
|
155
162
|
];
|
|
156
163
|
for (const entry of stored) {
|
|
157
164
|
if (entry.chat) {
|
|
158
|
-
this.remember(entry.chat);
|
|
165
|
+
this.remember(entry.chat, entry.sequence);
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
this.error = undefined;
|
|
162
169
|
this.publish();
|
|
163
170
|
for (const room of new Set(stored
|
|
164
|
-
.filter((entry) => !entry.chat)
|
|
171
|
+
.filter((entry) => !entry.chat && !entry.server_id)
|
|
165
172
|
.map((entry) => entry.input.chat_room_id))) {
|
|
166
173
|
this.work(room, owner, signal);
|
|
167
174
|
}
|
|
168
175
|
for (const entry of stored) {
|
|
176
|
+
if (chatSendCanConfirm(entry)) {
|
|
177
|
+
this.startConfirmation(entry, owner, signal);
|
|
178
|
+
}
|
|
169
179
|
if (entry.chat && entry.status === ChatSendStatus.Uploading) {
|
|
170
180
|
this.recoverUpload(entry, owner, signal);
|
|
171
181
|
}
|
|
@@ -195,7 +205,9 @@ export class ChatSendQueue {
|
|
|
195
205
|
return;
|
|
196
206
|
}
|
|
197
207
|
const signal = this.controller.signal, owner = this.owner;
|
|
198
|
-
await this.opts.lock(
|
|
208
|
+
await this.opts.lock(entry.server_id && !entry.chat
|
|
209
|
+
? this.confirmationLock(entry)
|
|
210
|
+
: this.roomLock(entry), signal, async () => {
|
|
199
211
|
if (!this.current(owner, signal)) {
|
|
200
212
|
return;
|
|
201
213
|
}
|
|
@@ -207,51 +219,73 @@ export class ChatSendQueue {
|
|
|
207
219
|
this.startUpload(current, owner, signal, true);
|
|
208
220
|
}
|
|
209
221
|
else {
|
|
210
|
-
await this.save({
|
|
222
|
+
await this.save({
|
|
223
|
+
...current,
|
|
224
|
+
status: current.server_id
|
|
225
|
+
? ChatSendStatus.Confirming
|
|
226
|
+
: ChatSendStatus.Queued,
|
|
227
|
+
});
|
|
211
228
|
}
|
|
212
229
|
});
|
|
213
230
|
await this.refresh();
|
|
214
231
|
}
|
|
215
232
|
async remove(id) {
|
|
216
|
-
const
|
|
233
|
+
const owner = this.owner, signal = this.controller.signal;
|
|
234
|
+
let entry = this.entries.find((value) => value.id === id);
|
|
217
235
|
if (!entry ||
|
|
218
|
-
!this.
|
|
219
|
-
entry
|
|
220
|
-
|
|
221
|
-
entry.status === ChatSendStatus.Uploading) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
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();
|
|
236
|
+
!this.current(owner, signal) ||
|
|
237
|
+
!chatSendCanRemove(entry) ||
|
|
238
|
+
this.cancellations.has(id)) {
|
|
229
239
|
return;
|
|
230
240
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
this.cancellations.add(id);
|
|
242
|
+
try {
|
|
243
|
+
// Finish a pending local save before removing its durable record. The
|
|
244
|
+
// cancellation flag prevents this tab from starting that message meanwhile.
|
|
245
|
+
if (entry.status === ChatSendStatus.Saving) {
|
|
246
|
+
await this.writes;
|
|
247
|
+
entry = this.entries.find((value) => value.id === id);
|
|
248
|
+
}
|
|
249
|
+
if (!entry || !this.current(owner, signal) || !chatSendCanRemove(entry)) {
|
|
234
250
|
return;
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
}
|
|
252
|
+
// Cancel only this request. A server response that already arrived wins;
|
|
253
|
+
// the shared room lock still protects removal from another tab's sender.
|
|
254
|
+
this.sends.get(id)?.abort();
|
|
255
|
+
if (entry.sequence === undefined) {
|
|
256
|
+
this.pendingSaves.delete(id);
|
|
257
|
+
this.entries = this.entries.filter((value) => value.id !== id);
|
|
258
|
+
this.publish();
|
|
259
|
+
this.persist();
|
|
237
260
|
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
261
|
+
}
|
|
262
|
+
const removeStored = async () => {
|
|
263
|
+
if (!this.current(owner, signal))
|
|
264
|
+
return;
|
|
265
|
+
const stored = (await this.opts.store.list(owner)).find((value) => value.id === id);
|
|
266
|
+
if (stored && !chatSendCanRemove(stored))
|
|
267
|
+
return;
|
|
268
|
+
await this.opts.store.remove(id);
|
|
269
|
+
this.entries = this.entries.filter((value) => value.id !== id);
|
|
270
|
+
this.publish();
|
|
271
|
+
this.opts.changed?.();
|
|
272
|
+
};
|
|
273
|
+
// Upload retries use their own lock so a slow multipart never holds the
|
|
274
|
+
// message lane. Discard must coordinate with that same lock.
|
|
275
|
+
await this.opts.lock(entry.chat ? this.uploadLock(entry) : this.roomLock(entry), signal, removeStored);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
this.cancellations.delete(id);
|
|
279
|
+
this.persist();
|
|
280
|
+
await this.refresh();
|
|
281
|
+
}
|
|
248
282
|
}
|
|
249
283
|
work(room, owner, signal) {
|
|
250
284
|
this.queue.process({
|
|
251
285
|
owner_id: owner,
|
|
252
286
|
scope: `chat-send:${room}`,
|
|
253
287
|
signal,
|
|
254
|
-
select: (entry) => entry.input.chat_room_id === room && !entry.chat,
|
|
288
|
+
select: (entry) => entry.input.chat_room_id === room && !entry.chat && !entry.server_id,
|
|
255
289
|
on_error: (err) => {
|
|
256
290
|
if (this.current(owner, signal)) {
|
|
257
291
|
this.error = err;
|
|
@@ -264,20 +298,20 @@ export class ChatSendQueue {
|
|
|
264
298
|
}
|
|
265
299
|
},
|
|
266
300
|
run: async (entry) => {
|
|
267
|
-
if (!this.current(owner, signal)) {
|
|
301
|
+
if (!this.current(owner, signal) || this.cancellations.has(entry.id)) {
|
|
268
302
|
return false;
|
|
269
303
|
}
|
|
270
|
-
if (entry.status === ChatSendStatus.Sending
|
|
271
|
-
(entry
|
|
272
|
-
|
|
273
|
-
await this.save({ ...entry, status: ChatSendStatus.Interrupted });
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
304
|
+
if (entry.status === ChatSendStatus.Sending) {
|
|
305
|
+
await this.save({ ...entry, status: ChatSendStatus.Interrupted });
|
|
306
|
+
return false;
|
|
276
307
|
}
|
|
277
308
|
else if (entry.status !== ChatSendStatus.Queued) {
|
|
278
309
|
return false;
|
|
279
310
|
}
|
|
280
311
|
let current = entry;
|
|
312
|
+
const request = new AbortController();
|
|
313
|
+
const requestSignal = AbortSignal.any([signal, request.signal]);
|
|
314
|
+
this.sends.set(entry.id, request);
|
|
281
315
|
try {
|
|
282
316
|
current = {
|
|
283
317
|
...entry,
|
|
@@ -288,58 +322,30 @@ export class ChatSendQueue {
|
|
|
288
322
|
if (!this.current(owner, signal)) {
|
|
289
323
|
return false;
|
|
290
324
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
});
|
|
297
|
-
current = {
|
|
298
|
-
...current,
|
|
299
|
-
server_id: requiredOperationID(out, "Created chat response did not contain an ID."),
|
|
300
|
-
};
|
|
301
|
-
// Retain the returned ID even if the page/account changed meanwhile.
|
|
302
|
-
await this.save(current);
|
|
303
|
-
}
|
|
304
|
-
if (!this.current(owner, signal)) {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
const out = await this.opts.chat.findOneChat({
|
|
308
|
-
id: current.server_id,
|
|
309
|
-
signal,
|
|
325
|
+
requestSignal.throwIfAborted();
|
|
326
|
+
const out = await this.opts.chat.insertOneChat({
|
|
327
|
+
in: current.input,
|
|
328
|
+
signal: requestSignal,
|
|
329
|
+
idempotency_key: current.id,
|
|
310
330
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
chat
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
current = {
|
|
332
|
+
...current,
|
|
333
|
+
status: ChatSendStatus.Confirming,
|
|
334
|
+
server_id: requiredOperationID(out, "Created chat response did not contain an ID."),
|
|
335
|
+
};
|
|
336
|
+
// Persist successful creation before advancing the room's FIFO. A
|
|
337
|
+
// later retrieval failure must never cause this POST to be repeated.
|
|
338
|
+
await this.save(current);
|
|
318
339
|
if (!this.current(owner, signal)) {
|
|
319
340
|
return false;
|
|
320
341
|
}
|
|
321
|
-
this.
|
|
322
|
-
if (current.attachments.files.length) {
|
|
323
|
-
current = {
|
|
324
|
-
...current,
|
|
325
|
-
chat,
|
|
326
|
-
status: ChatSendStatus.UploadInterrupted,
|
|
327
|
-
};
|
|
328
|
-
await this.save(current);
|
|
329
|
-
this.startUpload(current, owner, signal);
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
await this.opts.store.remove(current.id);
|
|
333
|
-
this.entries = this.entries.filter((value) => value.id !== current.id);
|
|
334
|
-
this.publish();
|
|
335
|
-
this.opts.changed?.(chat);
|
|
336
|
-
}
|
|
342
|
+
this.startConfirmation(current, owner, signal);
|
|
337
343
|
}
|
|
338
344
|
catch (err) {
|
|
339
345
|
try {
|
|
340
346
|
await this.save({
|
|
341
347
|
...current,
|
|
342
|
-
status:
|
|
348
|
+
status: requestSignal.aborted
|
|
343
349
|
? ChatSendStatus.Interrupted
|
|
344
350
|
: ChatSendStatus.Failed,
|
|
345
351
|
});
|
|
@@ -347,16 +353,99 @@ export class ChatSendQueue {
|
|
|
347
353
|
catch (storageError) {
|
|
348
354
|
this.error = storageError;
|
|
349
355
|
}
|
|
350
|
-
if (this.current(owner, signal)) {
|
|
356
|
+
if (this.current(owner, signal) && !request.signal.aborted) {
|
|
351
357
|
this.error = err;
|
|
352
358
|
this.publish();
|
|
353
359
|
}
|
|
354
360
|
return false;
|
|
355
361
|
}
|
|
362
|
+
finally {
|
|
363
|
+
this.sends.delete(entry.id);
|
|
364
|
+
}
|
|
356
365
|
return true;
|
|
357
366
|
},
|
|
358
367
|
});
|
|
359
368
|
}
|
|
369
|
+
// Retrieval is independent of the room's creation lock. Its per-message
|
|
370
|
+
// lock coordinates retries/recovery across tabs without holding later POSTs.
|
|
371
|
+
startConfirmation(entry, owner, signal) {
|
|
372
|
+
if (this.confirmations.has(entry.id)) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.confirmations.add(entry.id);
|
|
376
|
+
void this.opts
|
|
377
|
+
.lock(this.confirmationLock(entry), signal, async () => {
|
|
378
|
+
if (!this.current(owner, signal)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const stored = (await this.opts.store.list(owner)).find((value) => value.id === entry.id);
|
|
382
|
+
if (!stored || !chatSendCanConfirm(stored)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
let current = stored;
|
|
386
|
+
try {
|
|
387
|
+
const out = await this.opts.chat.findOneChat({
|
|
388
|
+
id: current.server_id,
|
|
389
|
+
signal,
|
|
390
|
+
});
|
|
391
|
+
const chat = out.value;
|
|
392
|
+
if (!chat ||
|
|
393
|
+
chat.id !== current.server_id ||
|
|
394
|
+
chat.owner_id !== owner ||
|
|
395
|
+
chat.chat_room_id !== current.input.chat_room_id) {
|
|
396
|
+
throw new Error("Chat confirmation could not be resolved.");
|
|
397
|
+
}
|
|
398
|
+
if (!this.current(owner, signal)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.remember(chat, current.sequence);
|
|
402
|
+
if (current.attachments.files.length &&
|
|
403
|
+
chat.has_objects &&
|
|
404
|
+
!chat.objects_uploaded) {
|
|
405
|
+
current = {
|
|
406
|
+
...current,
|
|
407
|
+
chat,
|
|
408
|
+
status: ChatSendStatus.UploadInterrupted,
|
|
409
|
+
};
|
|
410
|
+
await this.save(current);
|
|
411
|
+
this.startUpload(current, owner, signal);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
await this.opts.store.remove(current.id);
|
|
415
|
+
this.entries = this.entries.filter((value) => value.id !== current.id);
|
|
416
|
+
this.publish();
|
|
417
|
+
this.opts.changed?.(chat);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
await this.save({
|
|
422
|
+
...current,
|
|
423
|
+
status: current.chat
|
|
424
|
+
? ChatSendStatus.UploadInterrupted
|
|
425
|
+
: signal.aborted
|
|
426
|
+
? ChatSendStatus.Interrupted
|
|
427
|
+
: ChatSendStatus.Failed,
|
|
428
|
+
});
|
|
429
|
+
if (this.current(owner, signal)) {
|
|
430
|
+
this.error = err;
|
|
431
|
+
this.publish();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
.catch((err) => {
|
|
436
|
+
if (this.current(owner, signal)) {
|
|
437
|
+
this.error = err;
|
|
438
|
+
this.publish();
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
.finally(() => {
|
|
442
|
+
this.confirmations.delete(entry.id);
|
|
443
|
+
// A pause/resume can occur before the aborted worker releases its lock.
|
|
444
|
+
if (this.enabled && signal.aborted) {
|
|
445
|
+
void this.refresh();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
360
449
|
startUpload(entry, owner, signal, reconcile = false) {
|
|
361
450
|
if (this.uploads.has(entry.id)) {
|
|
362
451
|
return;
|
|
@@ -477,6 +566,9 @@ export class ChatSendQueue {
|
|
|
477
566
|
uploadLock(entry) {
|
|
478
567
|
return `chat-upload:${entry.owner_id}:${entry.id}`;
|
|
479
568
|
}
|
|
569
|
+
confirmationLock(entry) {
|
|
570
|
+
return `chat-confirm:${entry.owner_id}:${entry.id}`;
|
|
571
|
+
}
|
|
480
572
|
roomLock(entry) {
|
|
481
573
|
return `${entry.owner_id}:chat-send:${entry.input.chat_room_id}`;
|
|
482
574
|
}
|
|
@@ -499,7 +591,11 @@ export class ChatSendQueue {
|
|
|
499
591
|
: this.entries.map((value) => (value.id === entry.id ? entry : value));
|
|
500
592
|
this.publish();
|
|
501
593
|
}
|
|
502
|
-
remember(chat)
|
|
594
|
+
remember(chat, sequence = this.entries.find((entry) => entry.server_id === chat.id)
|
|
595
|
+
?.sequence) {
|
|
596
|
+
if (sequence !== undefined) {
|
|
597
|
+
this.confirmedSequences.set(chat.id, sequence);
|
|
598
|
+
}
|
|
503
599
|
const old = this.confirmed.get(chat.id);
|
|
504
600
|
if (!old || old.updated_at <= chat.updated_at) {
|
|
505
601
|
this.confirmed.set(chat.id, chat);
|
|
@@ -512,11 +608,25 @@ export class ChatSendQueue {
|
|
|
512
608
|
}
|
|
513
609
|
}
|
|
514
610
|
}
|
|
611
|
+
function chatSendCanConfirm(entry) {
|
|
612
|
+
return (!!entry.server_id &&
|
|
613
|
+
!entry.chat &&
|
|
614
|
+
(entry.status === ChatSendStatus.Confirming ||
|
|
615
|
+
entry.status === ChatSendStatus.Sending ||
|
|
616
|
+
entry.status === ChatSendStatus.Queued ||
|
|
617
|
+
entry.status === ChatSendStatus.Interrupted));
|
|
618
|
+
}
|
|
515
619
|
export function chatSendNeedsRetry(entry) {
|
|
516
620
|
return (entry.status === ChatSendStatus.Failed ||
|
|
517
621
|
entry.status === ChatSendStatus.Interrupted ||
|
|
518
622
|
entry.status === ChatSendStatus.UploadInterrupted);
|
|
519
623
|
}
|
|
520
624
|
export function chatSendCanRemove(entry) {
|
|
521
|
-
|
|
625
|
+
if (!entry.chat && entry.server_id) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
return (entry.status === ChatSendStatus.Saving ||
|
|
629
|
+
entry.status === ChatSendStatus.Queued ||
|
|
630
|
+
entry.status === ChatSendStatus.Sending ||
|
|
631
|
+
chatSendNeedsRetry(entry));
|
|
522
632
|
}
|