@bobfrankston/rmfmail 1.1.2 → 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 +3 -3
- package/packages/mailx-bus/index.d.ts +2 -0
- package/packages/mailx-bus/index.d.ts.map +1 -1
- package/packages/mailx-bus/index.js +9 -0
- package/packages/mailx-bus/index.js.map +1 -1
- package/packages/mailx-bus/index.ts +11 -0
- package/packages/mailx-bus/package.json +1 -1
- 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-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/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/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-36832 → 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
|
}
|
|
@@ -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":"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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.
|
|
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
|
|
@@ -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
|