@bobfrankston/rmfmail 1.0.708 → 1.1.3
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.
- package/bin/mailx.js +9 -1
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +10 -1
- package/client/android-bootstrap.bundle.js +1 -1
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +58 -1
- package/client/app.bundle.js.map +2 -2
- package/client/compose/compose.bundle.js +41 -1
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +41 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +29 -0
- package/client/lib/message-state.js +23 -0
- package/client/lib/message-state.js.map +1 -1
- package/client/lib/message-state.ts +21 -0
- package/package.json +4 -3
- package/packages/mailx-bus/index.d.ts +122 -0
- package/packages/mailx-bus/index.d.ts.map +1 -0
- package/packages/mailx-bus/index.js +247 -0
- package/packages/mailx-bus/index.js.map +1 -0
- package/packages/mailx-bus/index.ts +275 -0
- package/packages/mailx-bus/package.json +19 -0
- package/packages/mailx-bus/store-events.d.ts +79 -0
- package/packages/mailx-bus/store-events.d.ts.map +1 -0
- package/packages/mailx-bus/store-events.js +155 -0
- package/packages/mailx-bus/store-events.js.map +1 -0
- package/packages/mailx-bus/store-events.ts +165 -0
- package/packages/mailx-bus/tsconfig.json +9 -0
- package/packages/mailx-core/index.d.ts.map +1 -1
- package/packages/mailx-core/index.js +25 -3
- package/packages/mailx-core/index.js.map +1 -1
- package/packages/mailx-core/index.ts +23 -3
- package/packages/mailx-host/package.json +1 -1
- package/packages/mailx-imap/index.d.ts +0 -37
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +17 -172
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +17 -179
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/db-worker-client.d.ts +32 -0
- package/packages/mailx-service/db-worker-client.d.ts.map +1 -0
- package/packages/mailx-service/db-worker-client.js +66 -0
- package/packages/mailx-service/db-worker-client.js.map +1 -0
- package/packages/mailx-service/db-worker-client.ts +75 -0
- package/packages/mailx-service/db-worker.d.ts +39 -0
- package/packages/mailx-service/db-worker.d.ts.map +1 -0
- package/packages/mailx-service/db-worker.js +104 -0
- package/packages/mailx-service/db-worker.js.map +1 -0
- package/packages/mailx-service/db-worker.ts +153 -0
- package/packages/mailx-service/index.d.ts +16 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +89 -77
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +83 -75
- package/packages/mailx-service/local-store.d.ts +54 -2
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +147 -2
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +147 -3
- package/packages/mailx-service/package.json +1 -0
- package/packages/mailx-service/reconciler.d.ts.map +1 -1
- package/packages/mailx-service/reconciler.js +28 -8
- package/packages/mailx-service/reconciler.js.map +1 -1
- package/packages/mailx-service/reconciler.ts +28 -8
- package/packages/mailx-service/sync-queue.d.ts +16 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -1
- package/packages/mailx-service/sync-queue.js +33 -3
- package/packages/mailx-service/sync-queue.js.map +1 -1
- package/packages/mailx-service/sync-queue.ts +33 -3
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/bus.d.ts +79 -0
- package/packages/mailx-store/bus.d.ts.map +1 -0
- package/packages/mailx-store/bus.js +155 -0
- package/packages/mailx-store/bus.js.map +1 -0
- package/packages/mailx-store/index.d.ts +2 -0
- package/packages/mailx-store/index.d.ts.map +1 -1
- package/packages/mailx-store/index.js +5 -0
- package/packages/mailx-store/index.js.map +1 -1
- package/packages/mailx-store/index.ts +7 -0
- package/packages/mailx-store/package.json +2 -1
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +9 -1
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +9 -1
- package/packages/mailx-store-web/package.json +2 -1
- package/packages/mailx-store-web/sync-manager.d.ts +4 -0
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
- package/packages/mailx-store-web/sync-manager.js +32 -4
- package/packages/mailx-store-web/sync-manager.js.map +1 -1
- package/packages/mailx-store-web/sync-manager.ts +28 -4
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-24548 → node_modules.npmglobalize-stash-444}/.package-lock.json +0 -0
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import * as fs from "node:fs";
|
|
20
|
-
import { parseSerial } from "@bobfrankston/mailx-store";
|
|
21
|
-
import type { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
|
|
20
|
+
import { parseSerial, storeBus } from "@bobfrankston/mailx-store";
|
|
21
|
+
import type { MailxDB, FileMessageStore, StoreBus } from "@bobfrankston/mailx-store";
|
|
22
22
|
import type {
|
|
23
23
|
MessageEnvelope, Message, MessageQuery, PagedResult,
|
|
24
24
|
} from "@bobfrankston/mailx-types";
|
|
@@ -138,6 +138,11 @@ export class LocalStore {
|
|
|
138
138
|
constructor(
|
|
139
139
|
private db: MailxDB,
|
|
140
140
|
private bodyStore: FileMessageStore,
|
|
141
|
+
/** Event bus for Store mutations. Defaults to the process-singleton
|
|
142
|
+
* so cross-package subscribers (bin/mailx.ts forwarder → WebView,
|
|
143
|
+
* in-process reconciler triggers) see all writes without explicit
|
|
144
|
+
* wiring. Tests can pass a fresh StoreBus for isolation. */
|
|
145
|
+
public readonly bus: StoreBus = storeBus,
|
|
141
146
|
) {}
|
|
142
147
|
|
|
143
148
|
// ── Account list (read-only here; mutations go through MailxService
|
|
@@ -157,6 +162,19 @@ export class LocalStore {
|
|
|
157
162
|
return this.db.getFolders(accountId);
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
/** Look up a folder by RFC 6154 specialUse tag (`trash`, `drafts`, `sent`,
|
|
166
|
+
* `junk`, etc.) for the given account. Falls back to a case-insensitive
|
|
167
|
+
* path match for legacy rows where specialUse never got tagged.
|
|
168
|
+
* Returns null when the account has no such folder configured. */
|
|
169
|
+
findSpecialFolder(accountId: string, specialUse: string): { id: number; path: string } | null {
|
|
170
|
+
const folders = this.db.getFolders(accountId);
|
|
171
|
+
const f = folders.find(x =>
|
|
172
|
+
x.specialUse === specialUse ||
|
|
173
|
+
x.path.toLowerCase() === specialUse.toLowerCase()
|
|
174
|
+
);
|
|
175
|
+
return f ? { id: f.id, path: f.path } : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
160
178
|
// ── Message envelopes ──
|
|
161
179
|
|
|
162
180
|
/** Single envelope by (account, uid, folder). Null when the row isn't
|
|
@@ -405,5 +423,131 @@ export class LocalStore {
|
|
|
405
423
|
return this.db.listContacts(query, page, pageSize);
|
|
406
424
|
}
|
|
407
425
|
|
|
408
|
-
// Write paths
|
|
426
|
+
// ── Write paths (local-only; mirror to server is queued separately) ──
|
|
427
|
+
|
|
428
|
+
/** Update a message's flag set. Local DB write completes synchronously;
|
|
429
|
+
* the server-mirror enqueue is the caller's responsibility (typically
|
|
430
|
+
* via SyncQueue.enqueueFlag) so callers that don't want a server push
|
|
431
|
+
* — pure-local UI state like "pin in pane" — can skip it.
|
|
432
|
+
*
|
|
433
|
+
* Publishes:
|
|
434
|
+
* `message:<uuid>` { kind: "flagsChanged" }
|
|
435
|
+
* `folder:<id>` (auto fan-out)
|
|
436
|
+
*/
|
|
437
|
+
updateFlags(accountId: string, uid: number, folderId: number, flags: string[]): void {
|
|
438
|
+
this.db.updateMessageFlags(accountId, uid, flags);
|
|
439
|
+
const env: any = this.db.getMessageByUid(accountId, uid, folderId);
|
|
440
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
441
|
+
if (msgUuid) {
|
|
442
|
+
this.bus.publish({
|
|
443
|
+
topic: `message:${msgUuid}`,
|
|
444
|
+
kind: "flagsChanged",
|
|
445
|
+
accountId, folderId, uid, msgUuid, flags,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Move a message between folders in the same account. Adds a tombstone
|
|
451
|
+
* on the Message-ID so the next sync doesn't re-import the pre-move row
|
|
452
|
+
* in the source folder before the server-side MOVE completes; tombstone
|
|
453
|
+
* is cleared on terminal IMAP failure (see processSyncActions).
|
|
454
|
+
*
|
|
455
|
+
* Returns true if a local row existed and was moved, false otherwise.
|
|
456
|
+
*
|
|
457
|
+
* Publishes:
|
|
458
|
+
* `message:<uuid>` { kind: "messageMoved", folderId: source, targetFolderId }
|
|
459
|
+
* `folder:<source>` and `folder:<target>` (auto fan-out + explicit count)
|
|
460
|
+
*/
|
|
461
|
+
moveMessage(accountId: string, uid: number, fromFolderId: number, targetFolderId: number): boolean {
|
|
462
|
+
const env: any = this.db.getMessageByUid(accountId, uid, fromFolderId);
|
|
463
|
+
if (!env) return false;
|
|
464
|
+
if (env.messageId) this.db.addTombstone(accountId, env.messageId, env.subject || "");
|
|
465
|
+
const moved = this.db.moveMessageLocal(accountId, uid, fromFolderId, targetFolderId);
|
|
466
|
+
if (!moved) return false;
|
|
467
|
+
this.db.recalcFolderCounts(fromFolderId);
|
|
468
|
+
this.db.recalcFolderCounts(targetFolderId);
|
|
469
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
470
|
+
if (msgUuid) {
|
|
471
|
+
this.bus.publish({
|
|
472
|
+
topic: `message:${msgUuid}`,
|
|
473
|
+
kind: "messageMoved",
|
|
474
|
+
accountId, folderId: fromFolderId, targetFolderId, uid, msgUuid,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// Folder count change isn't tied to a specific message uuid — publish
|
|
478
|
+
// to both folder topics directly. (The fan-out only covers the source
|
|
479
|
+
// via the message event's folderId; target needs its own publish.)
|
|
480
|
+
this.bus.publish({
|
|
481
|
+
topic: `folder:${targetFolderId}`,
|
|
482
|
+
kind: "folderCountsChanged",
|
|
483
|
+
accountId, folderId: targetFolderId,
|
|
484
|
+
});
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Trash a message. If a trash folder is configured and the message is
|
|
489
|
+
* not already in it, this is a move-to-trash. If the message is already
|
|
490
|
+
* in trash (or no trash exists), it's a hard delete + body unlink.
|
|
491
|
+
*
|
|
492
|
+
* Returns "moved-to-trash" or "expunged" so the caller knows whether
|
|
493
|
+
* to enqueue an IMAP MOVE or a DELETE+EXPUNGE on the queue.
|
|
494
|
+
*/
|
|
495
|
+
trashMessage(accountId: string, uid: number, folderId: number, trashFolderId: number | null): "moved-to-trash" | "expunged" {
|
|
496
|
+
const env: any = this.db.getMessageByUid(accountId, uid, folderId);
|
|
497
|
+
if (env?.messageId) this.db.addTombstone(accountId, env.messageId, env.subject || "");
|
|
498
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
499
|
+
if (trashFolderId != null && trashFolderId !== folderId) {
|
|
500
|
+
this.db.moveMessageLocal(accountId, uid, folderId, trashFolderId);
|
|
501
|
+
this.db.recalcFolderCounts(folderId);
|
|
502
|
+
this.db.recalcFolderCounts(trashFolderId);
|
|
503
|
+
if (msgUuid) {
|
|
504
|
+
this.bus.publish({
|
|
505
|
+
topic: `message:${msgUuid}`,
|
|
506
|
+
kind: "messageMoved",
|
|
507
|
+
accountId, folderId, targetFolderId: trashFolderId, uid, msgUuid,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
this.bus.publish({
|
|
511
|
+
topic: `folder:${trashFolderId}`,
|
|
512
|
+
kind: "folderCountsChanged",
|
|
513
|
+
accountId, folderId: trashFolderId,
|
|
514
|
+
});
|
|
515
|
+
return "moved-to-trash";
|
|
516
|
+
}
|
|
517
|
+
this.db.deleteMessage(accountId, uid, "user-initiated trash (already in trash → expunge)", "LocalStore.trashMessage");
|
|
518
|
+
this.db.recalcFolderCounts(folderId);
|
|
519
|
+
if (msgUuid) {
|
|
520
|
+
this.bus.publish({
|
|
521
|
+
topic: `message:${msgUuid}`,
|
|
522
|
+
kind: "messageRemoved",
|
|
523
|
+
accountId, folderId, uid, msgUuid,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return "expunged";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Restore a message from trash back to its original folder. Local-only;
|
|
530
|
+
* caller handles the queue (cancel-pending-MOVE vs queue-counter-MOVE).
|
|
531
|
+
* Returns true if a local row was moved. */
|
|
532
|
+
undeleteMessage(accountId: string, uid: number, trashFolderId: number, originalFolderId: number): boolean {
|
|
533
|
+
const moved = this.db.moveMessageLocal(accountId, uid, trashFolderId, originalFolderId);
|
|
534
|
+
if (!moved) return false;
|
|
535
|
+
this.db.recalcFolderCounts(trashFolderId);
|
|
536
|
+
this.db.recalcFolderCounts(originalFolderId);
|
|
537
|
+
const env: any = this.db.getMessageByUid(accountId, uid, originalFolderId);
|
|
538
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
539
|
+
if (msgUuid) {
|
|
540
|
+
this.bus.publish({
|
|
541
|
+
topic: `message:${msgUuid}`,
|
|
542
|
+
kind: "messageMoved",
|
|
543
|
+
accountId, folderId: trashFolderId, targetFolderId: originalFolderId, uid, msgUuid,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
this.bus.publish({
|
|
547
|
+
topic: `folder:${originalFolderId}`,
|
|
548
|
+
kind: "folderCountsChanged",
|
|
549
|
+
accountId, folderId: originalFolderId,
|
|
550
|
+
});
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
409
553
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@bobfrankston/mailx-bus": "file:../mailx-bus",
|
|
12
13
|
"@bobfrankston/mailx-imap": "file:../mailx-imap",
|
|
13
14
|
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
14
15
|
"@bobfrankston/mailx-store": "file:../mailx-store",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAC9B;;sDAEkD;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;8DAC0D;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;0EACsE;IACtE,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,mEAAmE;IACnE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;yCACqC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACnC;AAYD,qBAAa,UAAU;IAcf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IAfjB,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAK;IAC5B;;;wEAGoE;IACpE,OAAO,CAAC,UAAU,CAAgE;gBAGtE,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,SAAS,EACxB,IAAI,GAAE,iBAAsB;IAKhC,KAAK,IAAI,IAAI;IAyCb,IAAI,IAAI,IAAI;IAOZ;oDACgD;IAChD,OAAO,CAAC,iBAAiB;IAMzB;;;gDAG4C;IAC5C,OAAO,CAAC,eAAe;IAcvB;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;2DAEuD;IACvD,OAAO,CAAC,eAAe;IAqFvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Part of docs/local-first-plan.md (step 4).
|
|
29
29
|
*/
|
|
30
|
+
import { storeBus } from "@bobfrankston/mailx-store";
|
|
30
31
|
const DEFAULTS = {
|
|
31
32
|
bodyFetchConcurrency: 2,
|
|
32
33
|
statusEmitIntervalMs: 5_000,
|
|
@@ -156,8 +157,15 @@ export class Reconciler {
|
|
|
156
157
|
try {
|
|
157
158
|
const raw = await this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid);
|
|
158
159
|
if (raw) {
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// Look up uuid for the message topic; the body has just
|
|
161
|
+
// landed, so the DB row's body_path was updated by
|
|
162
|
+
// fetchMessageBody and the envelope is fresh.
|
|
163
|
+
const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
164
|
+
const msgUuid = env?.uuid;
|
|
165
|
+
storeBus.publish({
|
|
166
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
167
|
+
kind: "bodyAvailable",
|
|
168
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
161
169
|
});
|
|
162
170
|
}
|
|
163
171
|
else {
|
|
@@ -170,8 +178,12 @@ export class Reconciler {
|
|
|
170
178
|
// (non-transient) bodyFetchError so the message-
|
|
171
179
|
// viewer's listener can show a clear failure banner
|
|
172
180
|
// instead of a silent permanent blank.
|
|
173
|
-
this.
|
|
174
|
-
|
|
181
|
+
const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
182
|
+
const msgUuid = env?.uuid;
|
|
183
|
+
storeBus.publish({
|
|
184
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
185
|
+
kind: "bodyFetchError",
|
|
186
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
175
187
|
error: "body fetch returned no content (message too large for the API or stub)",
|
|
176
188
|
transient: false,
|
|
177
189
|
});
|
|
@@ -180,10 +192,14 @@ export class Reconciler {
|
|
|
180
192
|
catch (err) {
|
|
181
193
|
if (err?.isNotFound) {
|
|
182
194
|
try {
|
|
195
|
+
const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
196
|
+
const msgUuid = env?.uuid;
|
|
183
197
|
this.db.deleteMessage(next.accountId, next.uid);
|
|
184
198
|
this.db.recalcFolderCounts(next.folderId);
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
storeBus.publish({
|
|
200
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
201
|
+
kind: "messageRemoved",
|
|
202
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
187
203
|
});
|
|
188
204
|
}
|
|
189
205
|
catch { /* */ }
|
|
@@ -203,8 +219,12 @@ export class Reconciler {
|
|
|
203
219
|
setTimeout(() => this.pumpBodyFetches(), backoffMs);
|
|
204
220
|
}
|
|
205
221
|
else {
|
|
206
|
-
this.
|
|
207
|
-
|
|
222
|
+
const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
223
|
+
const msgUuid = env?.uuid;
|
|
224
|
+
storeBus.publish({
|
|
225
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
226
|
+
kind: "bodyFetchError",
|
|
227
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
208
228
|
error: msg, transient,
|
|
209
229
|
});
|
|
210
230
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import type { ImapManager } from "@bobfrankston/mailx-imap";
|
|
32
32
|
import type { MailxDB } from "@bobfrankston/mailx-store";
|
|
33
|
+
import { storeBus } from "@bobfrankston/mailx-store";
|
|
33
34
|
import type { SyncQueue } from "./sync-queue.js";
|
|
34
35
|
|
|
35
36
|
export interface ReconcilerOptions {
|
|
@@ -182,8 +183,15 @@ export class Reconciler {
|
|
|
182
183
|
try {
|
|
183
184
|
const raw = await this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid);
|
|
184
185
|
if (raw) {
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// Look up uuid for the message topic; the body has just
|
|
187
|
+
// landed, so the DB row's body_path was updated by
|
|
188
|
+
// fetchMessageBody and the envelope is fresh.
|
|
189
|
+
const env: any = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
190
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
191
|
+
storeBus.publish({
|
|
192
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
193
|
+
kind: "bodyAvailable",
|
|
194
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
187
195
|
});
|
|
188
196
|
} else {
|
|
189
197
|
// fetchMessageBody returned null without throwing.
|
|
@@ -195,8 +203,12 @@ export class Reconciler {
|
|
|
195
203
|
// (non-transient) bodyFetchError so the message-
|
|
196
204
|
// viewer's listener can show a clear failure banner
|
|
197
205
|
// instead of a silent permanent blank.
|
|
198
|
-
this.
|
|
199
|
-
|
|
206
|
+
const env: any = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
207
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
208
|
+
storeBus.publish({
|
|
209
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
210
|
+
kind: "bodyFetchError",
|
|
211
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
200
212
|
error: "body fetch returned no content (message too large for the API or stub)",
|
|
201
213
|
transient: false,
|
|
202
214
|
});
|
|
@@ -204,10 +216,14 @@ export class Reconciler {
|
|
|
204
216
|
} catch (err: any) {
|
|
205
217
|
if (err?.isNotFound) {
|
|
206
218
|
try {
|
|
219
|
+
const env: any = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
220
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
207
221
|
this.db.deleteMessage(next.accountId, next.uid);
|
|
208
222
|
this.db.recalcFolderCounts(next.folderId);
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
storeBus.publish({
|
|
224
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
225
|
+
kind: "messageRemoved",
|
|
226
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
211
227
|
});
|
|
212
228
|
} catch { /* */ }
|
|
213
229
|
} else {
|
|
@@ -224,8 +240,12 @@ export class Reconciler {
|
|
|
224
240
|
const backoffMs = 500 * Math.pow(2, next.attempts);
|
|
225
241
|
setTimeout(() => this.pumpBodyFetches(), backoffMs);
|
|
226
242
|
} else {
|
|
227
|
-
this.
|
|
228
|
-
|
|
243
|
+
const env: any = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
|
|
244
|
+
const msgUuid: string | undefined = env?.uuid;
|
|
245
|
+
storeBus.publish({
|
|
246
|
+
topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
|
|
247
|
+
kind: "bodyFetchError",
|
|
248
|
+
accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
|
|
229
249
|
error: msg, transient,
|
|
230
250
|
});
|
|
231
251
|
}
|
|
@@ -37,7 +37,15 @@ export declare class SyncQueue {
|
|
|
37
37
|
private db;
|
|
38
38
|
private imapManager;
|
|
39
39
|
private bodyFetches;
|
|
40
|
+
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
41
|
+
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
42
|
+
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
43
|
+
* moved here because the queue surface is now the source of truth for
|
|
44
|
+
* "what needs draining" and it's where the rate limit belongs. */
|
|
45
|
+
private drainTimers;
|
|
46
|
+
private static readonly DRAIN_DEBOUNCE_MS;
|
|
40
47
|
constructor(db: MailxDB, imapManager: ImapManager);
|
|
48
|
+
private scheduleDrain;
|
|
41
49
|
/** Queue a server-side move. The local rows have already been moved in
|
|
42
50
|
* the DB; the mirror reaches the server on the next reconciler pass. */
|
|
43
51
|
enqueueMove(accountId: string, uid: number, fromFolderId: number, toFolderId: number): void;
|
|
@@ -45,6 +53,14 @@ export declare class SyncQueue {
|
|
|
45
53
|
enqueueFlag(accountId: string, uid: number, folderId: number, flags: string[]): void;
|
|
46
54
|
/** Queue a server-side delete (or trash, depending on action). */
|
|
47
55
|
enqueueDelete(accountId: string, uid: number, folderId: number, kind?: "delete" | "trash"): void;
|
|
56
|
+
/** Cancel a pending MOVE action if it hasn't drained yet. Returns true
|
|
57
|
+
* when an action was found and marked complete (effectively retracted),
|
|
58
|
+
* false when no matching action exists — caller then queues a
|
|
59
|
+
* counter-move (the server already saw the original move).
|
|
60
|
+
*
|
|
61
|
+
* Used by undelete: if the to-trash MOVE is still queued, retract it
|
|
62
|
+
* rather than queue a redundant trash→original. */
|
|
63
|
+
cancelPendingMove(accountId: string, uid: number, fromFolderId: number, targetFolderId: number): boolean;
|
|
48
64
|
/** Queue an IMAP APPEND of a draft body. Drafts already have crash
|
|
49
65
|
* recovery via the editing/.eml on disk, so this lane is in-memory
|
|
50
66
|
* fire-and-forget for now — the architectural value is centralizing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-queue.d.ts","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpE;;;sDAGsD;AACtD,UAAU,YAAY;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAEzG,qBAAa,SAAS;
|
|
1
|
+
{"version":3,"file":"sync-queue.d.ts","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpE;;;sDAGsD;AACtD,UAAU,YAAY;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAEzG,qBAAa,SAAS;IAWd,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IAXvB,OAAO,CAAC,WAAW,CAAmC;IACtD;;;;uEAImE;IACnE,OAAO,CAAC,WAAW,CAAoD;IACvE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;gBAGrC,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW;IAGpC,OAAO,CAAC,aAAa;IAWrB;6EACyE;IACzE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAK3F,0EAA0E;IAC1E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAKpF,kEAAkE;IAClE,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,QAAQ,GAAG,OAAkB,GAAG,IAAI;IAK1G;;;;;;wDAMoD;IACpD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO;IAOxG;;;;;6DAKyD;IACzD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAO1G;;2DAEuD;IACvD,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAMxD;;;4DAGwD;IACxD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,IAAoB,GAAG,IAAI;IAYpG;;;qEAGiE;IACjE,gBAAgB,CAAC,GAAG,EAAE,YAAY,EAAE,WAAW,SAAI,GAAG,OAAO;IAQ7D;2EACuE;IACvE,aAAa,CAAC,OAAO,GAAE,IAAiB,GAAG,YAAY,GAAG,IAAI;IAY9D,4DAA4D;IAC5D,YAAY,IAAI;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;CAMlE"}
|
|
@@ -23,26 +23,56 @@ export class SyncQueue {
|
|
|
23
23
|
db;
|
|
24
24
|
imapManager;
|
|
25
25
|
bodyFetches = new Map();
|
|
26
|
+
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
27
|
+
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
28
|
+
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
29
|
+
* moved here because the queue surface is now the source of truth for
|
|
30
|
+
* "what needs draining" and it's where the rate limit belongs. */
|
|
31
|
+
drainTimers = new Map();
|
|
32
|
+
static DRAIN_DEBOUNCE_MS = 1000;
|
|
26
33
|
constructor(db, imapManager) {
|
|
27
34
|
this.db = db;
|
|
28
35
|
this.imapManager = imapManager;
|
|
29
36
|
}
|
|
37
|
+
scheduleDrain(accountId) {
|
|
38
|
+
const existing = this.drainTimers.get(accountId);
|
|
39
|
+
if (existing)
|
|
40
|
+
clearTimeout(existing);
|
|
41
|
+
this.drainTimers.set(accountId, setTimeout(() => {
|
|
42
|
+
this.drainTimers.delete(accountId);
|
|
43
|
+
this.imapManager.processSyncActions(accountId).catch(() => { });
|
|
44
|
+
}, SyncQueue.DRAIN_DEBOUNCE_MS));
|
|
45
|
+
}
|
|
30
46
|
// ── Message-mirror enqueues (commit local DB row first; then call here) ──
|
|
31
47
|
/** Queue a server-side move. The local rows have already been moved in
|
|
32
48
|
* the DB; the mirror reaches the server on the next reconciler pass. */
|
|
33
49
|
enqueueMove(accountId, uid, fromFolderId, toFolderId) {
|
|
34
50
|
this.db.queueSyncAction(accountId, "move", uid, fromFolderId, { targetFolderId: toFolderId });
|
|
35
|
-
this.
|
|
51
|
+
this.scheduleDrain(accountId);
|
|
36
52
|
}
|
|
37
53
|
/** Queue a server-side flag update (\Seen, \Flagged, \Answered, etc.). */
|
|
38
54
|
enqueueFlag(accountId, uid, folderId, flags) {
|
|
39
55
|
this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
|
|
40
|
-
this.
|
|
56
|
+
this.scheduleDrain(accountId);
|
|
41
57
|
}
|
|
42
58
|
/** Queue a server-side delete (or trash, depending on action). */
|
|
43
59
|
enqueueDelete(accountId, uid, folderId, kind = "delete") {
|
|
44
60
|
this.db.queueSyncAction(accountId, kind, uid, folderId, {});
|
|
45
|
-
this.
|
|
61
|
+
this.scheduleDrain(accountId);
|
|
62
|
+
}
|
|
63
|
+
/** Cancel a pending MOVE action if it hasn't drained yet. Returns true
|
|
64
|
+
* when an action was found and marked complete (effectively retracted),
|
|
65
|
+
* false when no matching action exists — caller then queues a
|
|
66
|
+
* counter-move (the server already saw the original move).
|
|
67
|
+
*
|
|
68
|
+
* Used by undelete: if the to-trash MOVE is still queued, retract it
|
|
69
|
+
* rather than queue a redundant trash→original. */
|
|
70
|
+
cancelPendingMove(accountId, uid, fromFolderId, targetFolderId) {
|
|
71
|
+
const pending = this.db.findPendingSyncAction(accountId, "move", uid, fromFolderId, targetFolderId);
|
|
72
|
+
if (!pending)
|
|
73
|
+
return false;
|
|
74
|
+
this.db.completeSyncAction(pending.id);
|
|
75
|
+
return true;
|
|
46
76
|
}
|
|
47
77
|
/** Queue an IMAP APPEND of a draft body. Drafts already have crash
|
|
48
78
|
* recovery via the editing/.eml on disk, so this lane is in-memory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,MAAM,OAAO,SAAS;
|
|
1
|
+
{"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,MAAM,OAAO,SAAS;IAWN;IACA;IAXJ,WAAW,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtD;;;;uEAImE;IAC3D,WAAW,GAAG,IAAI,GAAG,EAAyC,CAAC;IAC/D,MAAM,CAAU,iBAAiB,GAAG,IAAI,CAAC;IAEjD,YACY,EAAW,EACX,WAAwB;QADxB,OAAE,GAAF,EAAE,CAAS;QACX,gBAAW,GAAX,WAAW,CAAa;IACjC,CAAC;IAEI,aAAa,CAAC,SAAiB;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,EAAE;YAC5C,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAA+B,CAAC,CAAC,CAAC;QAChG,CAAC,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,4EAA4E;IAE5E;6EACyE;IACzE,WAAW,CAAC,SAAiB,EAAE,GAAW,EAAE,YAAoB,EAAE,UAAkB;QAChF,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9F,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,0EAA0E;IAC1E,WAAW,CAAC,SAAiB,EAAE,GAAW,EAAE,QAAgB,EAAE,KAAe;QACzE,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,kEAAkE;IAClE,aAAa,CAAC,SAAiB,EAAE,GAAW,EAAE,QAAgB,EAAE,OAA2B,QAAQ;QAC/F,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED;;;;;;wDAMoD;IACpD,iBAAiB,CAAC,SAAiB,EAAE,GAAW,EAAE,YAAoB,EAAE,cAAsB;QAC1F,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;QACpG,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;6DAKyD;IACzD,gBAAgB,CAAC,SAAiB,EAAE,UAAkB,EAAE,gBAAyB,EAAE,OAAgB;QAC/F,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAM,EAAE,EAAE;YAC1F,OAAO,CAAC,KAAK,CAAC,uCAAuC,OAAO,MAAM,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;YACrF,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QACvG,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;2DAEuD;IACvD,WAAW,CAAC,UAAkB,EAAE,SAAiB;QAC7C,mEAAmE;IACvE,CAAC;IAED,gDAAgD;IAEhD;;;4DAGwD;IACxD,gBAAgB,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW,EAAE,OAAa,aAAa;QACzF,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,QAAQ,IAAI,GAAG,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,QAAQ,EAAE,CAAC;YACX,iDAAiD;YACjD,MAAM,IAAI,GAAyB,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YACzF,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;YAC3D,OAAO;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED;;;qEAGiE;IACjE,gBAAgB,CAAC,GAAiB,EAAE,WAAW,GAAG,CAAC;QAC/C,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC,IAAI,WAAW;YAAE,OAAO,KAAK,CAAC;QAClD,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;QACxD,uEAAuE;QACvE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;2EACuE;IACvE,aAAa,CAAC,UAAgB,UAAU;QACpC,MAAM,IAAI,GAAyB,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACzF,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,IAAI,IAAI,GAAwB,IAAI,CAAC;QACrC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG;gBAAE,SAAS;YACjC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,IAAI,GAAG,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,IAAI;YAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,4DAA4D;IAC5D,YAAY;QACR,OAAO;YACH,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,wBAAwB,EAAE;YAClD,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI;SACrC,CAAC;IACN,CAAC"}
|
|
@@ -33,31 +33,61 @@ interface BodyFetchKey { accountId: string; folderId: number; uid: number; lane:
|
|
|
33
33
|
|
|
34
34
|
export class SyncQueue {
|
|
35
35
|
private bodyFetches = new Map<string, BodyFetchKey>();
|
|
36
|
+
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
37
|
+
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
38
|
+
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
39
|
+
* moved here because the queue surface is now the source of truth for
|
|
40
|
+
* "what needs draining" and it's where the rate limit belongs. */
|
|
41
|
+
private drainTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
42
|
+
private static readonly DRAIN_DEBOUNCE_MS = 1000;
|
|
36
43
|
|
|
37
44
|
constructor(
|
|
38
45
|
private db: MailxDB,
|
|
39
46
|
private imapManager: ImapManager,
|
|
40
47
|
) {}
|
|
41
48
|
|
|
49
|
+
private scheduleDrain(accountId: string): void {
|
|
50
|
+
const existing = this.drainTimers.get(accountId);
|
|
51
|
+
if (existing) clearTimeout(existing);
|
|
52
|
+
this.drainTimers.set(accountId, setTimeout(() => {
|
|
53
|
+
this.drainTimers.delete(accountId);
|
|
54
|
+
this.imapManager.processSyncActions(accountId).catch(() => { /* retried by reconciler */ });
|
|
55
|
+
}, SyncQueue.DRAIN_DEBOUNCE_MS));
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
// ── Message-mirror enqueues (commit local DB row first; then call here) ──
|
|
43
59
|
|
|
44
60
|
/** Queue a server-side move. The local rows have already been moved in
|
|
45
61
|
* the DB; the mirror reaches the server on the next reconciler pass. */
|
|
46
62
|
enqueueMove(accountId: string, uid: number, fromFolderId: number, toFolderId: number): void {
|
|
47
63
|
this.db.queueSyncAction(accountId, "move", uid, fromFolderId, { targetFolderId: toFolderId });
|
|
48
|
-
this.
|
|
64
|
+
this.scheduleDrain(accountId);
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/** Queue a server-side flag update (\Seen, \Flagged, \Answered, etc.). */
|
|
52
68
|
enqueueFlag(accountId: string, uid: number, folderId: number, flags: string[]): void {
|
|
53
69
|
this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
|
|
54
|
-
this.
|
|
70
|
+
this.scheduleDrain(accountId);
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/** Queue a server-side delete (or trash, depending on action). */
|
|
58
74
|
enqueueDelete(accountId: string, uid: number, folderId: number, kind: "delete" | "trash" = "delete"): void {
|
|
59
75
|
this.db.queueSyncAction(accountId, kind, uid, folderId, {});
|
|
60
|
-
this.
|
|
76
|
+
this.scheduleDrain(accountId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Cancel a pending MOVE action if it hasn't drained yet. Returns true
|
|
80
|
+
* when an action was found and marked complete (effectively retracted),
|
|
81
|
+
* false when no matching action exists — caller then queues a
|
|
82
|
+
* counter-move (the server already saw the original move).
|
|
83
|
+
*
|
|
84
|
+
* Used by undelete: if the to-trash MOVE is still queued, retract it
|
|
85
|
+
* rather than queue a redundant trash→original. */
|
|
86
|
+
cancelPendingMove(accountId: string, uid: number, fromFolderId: number, targetFolderId: number): boolean {
|
|
87
|
+
const pending = this.db.findPendingSyncAction(accountId, "move", uid, fromFolderId, targetFolderId);
|
|
88
|
+
if (!pending) return false;
|
|
89
|
+
this.db.completeSyncAction(pending.id);
|
|
90
|
+
return true;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
/** Queue an IMAP APPEND of a draft body. Drafts already have crash
|