@bobfrankston/rmfmail 1.1.2 → 1.1.4

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.
Files changed (91) hide show
  1. package/bin/mailx.js +10 -13
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +11 -13
  4. package/client/android-bootstrap.bundle.js +1 -1
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/client/app.bundle.js +88 -17
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/app.js +20 -20
  9. package/client/app.js.map +1 -1
  10. package/client/app.ts +20 -20
  11. package/client/components/context-menu.js +9 -0
  12. package/client/components/context-menu.js.map +1 -1
  13. package/client/components/context-menu.ts +8 -0
  14. package/client/components/message-viewer.js +21 -5
  15. package/client/components/message-viewer.js.map +1 -1
  16. package/client/components/message-viewer.ts +21 -5
  17. package/client/compose/compose.bundle.js +45 -1
  18. package/client/compose/compose.bundle.js.map +2 -2
  19. package/client/index.html +23 -4
  20. package/client/lib/api-client.js +41 -0
  21. package/client/lib/api-client.js.map +1 -1
  22. package/client/lib/api-client.ts +29 -0
  23. package/client/lib/mailxapi.js +11 -7
  24. package/client/lib/message-state.js +23 -0
  25. package/client/lib/message-state.js.map +1 -1
  26. package/client/lib/message-state.ts +21 -0
  27. package/package.json +9 -9
  28. package/packages/mailx-bus/index.d.ts +2 -0
  29. package/packages/mailx-bus/index.d.ts.map +1 -1
  30. package/packages/mailx-bus/index.js +9 -0
  31. package/packages/mailx-bus/index.js.map +1 -1
  32. package/packages/mailx-bus/index.ts +11 -0
  33. package/packages/mailx-bus/package.json +1 -1
  34. package/packages/mailx-bus/store-events.d.ts +79 -0
  35. package/packages/mailx-bus/store-events.d.ts.map +1 -0
  36. package/packages/mailx-bus/store-events.js +155 -0
  37. package/packages/mailx-bus/store-events.js.map +1 -0
  38. package/packages/mailx-bus/store-events.ts +165 -0
  39. package/packages/mailx-core/index.d.ts.map +1 -1
  40. package/packages/mailx-core/index.js +25 -3
  41. package/packages/mailx-core/index.js.map +1 -1
  42. package/packages/mailx-core/index.ts +23 -3
  43. package/packages/mailx-host/package.json +1 -1
  44. package/packages/mailx-imap/index.d.ts +0 -37
  45. package/packages/mailx-imap/index.d.ts.map +1 -1
  46. package/packages/mailx-imap/index.js +17 -172
  47. package/packages/mailx-imap/index.js.map +1 -1
  48. package/packages/mailx-imap/index.ts +17 -179
  49. package/packages/mailx-imap/package-lock.json +2 -2
  50. package/packages/mailx-imap/package.json +1 -1
  51. package/packages/mailx-service/index.d.ts +16 -0
  52. package/packages/mailx-service/index.d.ts.map +1 -1
  53. package/packages/mailx-service/index.js +89 -77
  54. package/packages/mailx-service/index.js.map +1 -1
  55. package/packages/mailx-service/index.ts +83 -75
  56. package/packages/mailx-service/local-store.d.ts +54 -2
  57. package/packages/mailx-service/local-store.d.ts.map +1 -1
  58. package/packages/mailx-service/local-store.js +147 -2
  59. package/packages/mailx-service/local-store.js.map +1 -1
  60. package/packages/mailx-service/local-store.ts +147 -3
  61. package/packages/mailx-service/reconciler.d.ts.map +1 -1
  62. package/packages/mailx-service/reconciler.js +28 -8
  63. package/packages/mailx-service/reconciler.js.map +1 -1
  64. package/packages/mailx-service/reconciler.ts +28 -8
  65. package/packages/mailx-service/sync-queue.d.ts +16 -0
  66. package/packages/mailx-service/sync-queue.d.ts.map +1 -1
  67. package/packages/mailx-service/sync-queue.js +33 -3
  68. package/packages/mailx-service/sync-queue.js.map +1 -1
  69. package/packages/mailx-service/sync-queue.ts +33 -3
  70. package/packages/mailx-settings/package.json +1 -1
  71. package/packages/mailx-store/bus.d.ts +79 -0
  72. package/packages/mailx-store/bus.d.ts.map +1 -0
  73. package/packages/mailx-store/bus.js +155 -0
  74. package/packages/mailx-store/bus.js.map +1 -0
  75. package/packages/mailx-store/index.d.ts +2 -0
  76. package/packages/mailx-store/index.d.ts.map +1 -1
  77. package/packages/mailx-store/index.js +5 -0
  78. package/packages/mailx-store/index.js.map +1 -1
  79. package/packages/mailx-store/index.ts +7 -0
  80. package/packages/mailx-store/package.json +2 -1
  81. package/packages/mailx-store-web/db.d.ts.map +1 -1
  82. package/packages/mailx-store-web/db.js +9 -1
  83. package/packages/mailx-store-web/db.js.map +1 -1
  84. package/packages/mailx-store-web/db.ts +9 -1
  85. package/packages/mailx-store-web/package.json +2 -1
  86. package/packages/mailx-store-web/sync-manager.d.ts +4 -0
  87. package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
  88. package/packages/mailx-store-web/sync-manager.js +32 -4
  89. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  90. package/packages/mailx-store-web/sync-manager.ts +28 -4
  91. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-36832 → node_modules.npmglobalize-stash-39436}/.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 and sync-queue enqueue methods come in step 3.
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
  }
@@ -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;AACzD,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;IAkEvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
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
- this.imapManager.emit("bodyAvailable", {
160
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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.imapManager.emit("bodyFetchError", {
174
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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
- this.imapManager.emit("messageRemoved", {
186
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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.imapManager.emit("bodyFetchError", {
207
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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":"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"}
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
- this.imapManager.emit("bodyAvailable", {
186
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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.imapManager.emit("bodyFetchError", {
199
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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
- this.imapManager.emit("messageRemoved", {
210
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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.imapManager.emit("bodyFetchError", {
228
- accountId: next.accountId, folderId: next.folderId, uid: next.uid,
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;IAId,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IAJvB,OAAO,CAAC,WAAW,CAAmC;gBAG1C,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW;IAKpC;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;;;;;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"}
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.imapManager.processSyncActions(accountId).catch(() => { });
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.imapManager.processSyncActions(accountId).catch(() => { });
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.imapManager.processSyncActions(accountId).catch(() => { });
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;IAIN;IACA;IAJJ,WAAW,GAAG,IAAI,GAAG,EAAwB,CAAC;IAEtD,YACY,EAAW,EACX,WAAwB;QADxB,OAAE,GAAF,EAAE,CAAS;QACX,gBAAW,GAAX,WAAW,CAAa;IACjC,CAAC;IAEJ,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,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAA+B,CAAC,CAAC,CAAC;IAChG,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,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAiB,CAAC,CAAC,CAAC;IAClF,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,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAiB,CAAC,CAAC,CAAC;IAClF,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;CACJ"}
1
+ {"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"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"}
@@ -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.imapManager.processSyncActions(accountId).catch(() => { /* retried by reconciler */ });
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.imapManager.processSyncActions(accountId).catch(() => { /* retried */ });
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.imapManager.processSyncActions(accountId).catch(() => { /* retried */ });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-settings",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,79 @@
1
+ /**
2
+ * StoreBus — typed pub/sub for Store events.
3
+ *
4
+ * Postmessage-style: subscribers register a topic string + handler; publishers
5
+ * emit a {topic, kind, ...} object. The exact same shape works inside Node,
6
+ * across worker_threads (postMessage), and across the WebView⇄host boundary
7
+ * (also postMessage). That's the load-bearing property: one mental model from
8
+ * DB write to UI re-render, on any platform.
9
+ *
10
+ * Topics
11
+ * ──────
12
+ * `message:<uuid>` — a specific message changed (flags, body, removal)
13
+ * `folder:<id>` — anything inside folder <id> changed; the bus fans
14
+ * out per-message events to the parent folder topic
15
+ * automatically (see fanOutToFolder())
16
+ * `account:<id>` — account-scoped event (sync status, auth, quota)
17
+ * `*` — wildcard; receives every event. Use for the IPC
18
+ * forwarder that pushes to the WebView, and for
19
+ * diagnostics. Avoid in normal UI subscribers.
20
+ *
21
+ * Batching
22
+ * ────────
23
+ * Bulk writes (sync round inserts 200 envelopes) would emit 200 events. The
24
+ * `withBatch(fn)` wrapper buffers events fired during fn() and coalesces
25
+ * per-topic into one {kind:"batch", changedUuids, summary} event at the end.
26
+ * Subscribers either iterate `changedUuids` or treat batch as "rerender this
27
+ * topic." Nested withBatch() is supported; inner scopes just append to the
28
+ * outer's buffer.
29
+ */
30
+ export type StoreEventKind = "messageInserted" | "messageUpdated" | "messageRemoved" | "messageMoved" | "flagsChanged" | "bodyAvailable" | "bodyFetchError" | "folderCountsChanged" | "draftSaved" | "draftSaveDeferred" | "batch";
31
+ export interface StoreEvent {
32
+ topic: string;
33
+ kind: StoreEventKind;
34
+ accountId?: string;
35
+ folderId?: number;
36
+ targetFolderId?: number;
37
+ uid?: number;
38
+ msgUuid?: string;
39
+ flags?: string[];
40
+ error?: string;
41
+ /** Set on `batch` events. UUIDs of every message touched in the scope. */
42
+ changedUuids?: string[];
43
+ /** Set on `batch` events. */
44
+ summary?: {
45
+ inserted: number;
46
+ updated: number;
47
+ deleted: number;
48
+ };
49
+ /** Arbitrary additional payload for kind-specific data the typed fields
50
+ * above don't cover (e.g. flag-set details, body path). Keep small. */
51
+ [extra: string]: unknown;
52
+ }
53
+ export type StoreEventHandler = (event: StoreEvent) => void;
54
+ export declare class StoreBus {
55
+ private subscribers;
56
+ private batchDepth;
57
+ private buffer;
58
+ subscribe(topic: string, handler: StoreEventHandler): () => void;
59
+ /** Publish an event. If a folderId is set and the topic is a message
60
+ * topic, the bus also publishes a copy to the parent folder topic so
61
+ * list views (subscribed to folder:<id>) wake without subscribing to
62
+ * every message individually. */
63
+ publish(event: StoreEvent): void;
64
+ /** Run `fn` with publishes buffered. On exit, coalesces per-topic and
65
+ * delivers either the single event (when only one fired for that topic)
66
+ * or a synthetic batch event summarizing the changes. */
67
+ withBatch<T>(fn: () => T): T;
68
+ private flush;
69
+ private deliver;
70
+ /** When a message-topic event fires and carries a folderId, generate
71
+ * the parallel folder-topic event so folder subscribers don't have to
72
+ * also subscribe to every message individually. Returns null when no
73
+ * fan-out applies. */
74
+ private fanOutToFolder;
75
+ }
76
+ /** Singleton bus shared by every Store consumer in the process. Workers
77
+ * get their own; the worker boundary serializes events via postMessage. */
78
+ export declare const storeBus: StoreBus;
79
+ //# sourceMappingURL=bus.d.ts.map