@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.
Files changed (92) hide show
  1. package/bin/mailx.js +9 -1
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +10 -1
  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 +58 -1
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/compose/compose.bundle.js +41 -1
  9. package/client/compose/compose.bundle.js.map +2 -2
  10. package/client/lib/api-client.js +41 -0
  11. package/client/lib/api-client.js.map +1 -1
  12. package/client/lib/api-client.ts +29 -0
  13. package/client/lib/message-state.js +23 -0
  14. package/client/lib/message-state.js.map +1 -1
  15. package/client/lib/message-state.ts +21 -0
  16. package/package.json +4 -3
  17. package/packages/mailx-bus/index.d.ts +122 -0
  18. package/packages/mailx-bus/index.d.ts.map +1 -0
  19. package/packages/mailx-bus/index.js +247 -0
  20. package/packages/mailx-bus/index.js.map +1 -0
  21. package/packages/mailx-bus/index.ts +275 -0
  22. package/packages/mailx-bus/package.json +19 -0
  23. package/packages/mailx-bus/store-events.d.ts +79 -0
  24. package/packages/mailx-bus/store-events.d.ts.map +1 -0
  25. package/packages/mailx-bus/store-events.js +155 -0
  26. package/packages/mailx-bus/store-events.js.map +1 -0
  27. package/packages/mailx-bus/store-events.ts +165 -0
  28. package/packages/mailx-bus/tsconfig.json +9 -0
  29. package/packages/mailx-core/index.d.ts.map +1 -1
  30. package/packages/mailx-core/index.js +25 -3
  31. package/packages/mailx-core/index.js.map +1 -1
  32. package/packages/mailx-core/index.ts +23 -3
  33. package/packages/mailx-host/package.json +1 -1
  34. package/packages/mailx-imap/index.d.ts +0 -37
  35. package/packages/mailx-imap/index.d.ts.map +1 -1
  36. package/packages/mailx-imap/index.js +17 -172
  37. package/packages/mailx-imap/index.js.map +1 -1
  38. package/packages/mailx-imap/index.ts +17 -179
  39. package/packages/mailx-imap/package-lock.json +2 -2
  40. package/packages/mailx-imap/package.json +1 -1
  41. package/packages/mailx-service/db-worker-client.d.ts +32 -0
  42. package/packages/mailx-service/db-worker-client.d.ts.map +1 -0
  43. package/packages/mailx-service/db-worker-client.js +66 -0
  44. package/packages/mailx-service/db-worker-client.js.map +1 -0
  45. package/packages/mailx-service/db-worker-client.ts +75 -0
  46. package/packages/mailx-service/db-worker.d.ts +39 -0
  47. package/packages/mailx-service/db-worker.d.ts.map +1 -0
  48. package/packages/mailx-service/db-worker.js +104 -0
  49. package/packages/mailx-service/db-worker.js.map +1 -0
  50. package/packages/mailx-service/db-worker.ts +153 -0
  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/package.json +1 -0
  62. package/packages/mailx-service/reconciler.d.ts.map +1 -1
  63. package/packages/mailx-service/reconciler.js +28 -8
  64. package/packages/mailx-service/reconciler.js.map +1 -1
  65. package/packages/mailx-service/reconciler.ts +28 -8
  66. package/packages/mailx-service/sync-queue.d.ts +16 -0
  67. package/packages/mailx-service/sync-queue.d.ts.map +1 -1
  68. package/packages/mailx-service/sync-queue.js +33 -3
  69. package/packages/mailx-service/sync-queue.js.map +1 -1
  70. package/packages/mailx-service/sync-queue.ts +33 -3
  71. package/packages/mailx-settings/package.json +1 -1
  72. package/packages/mailx-store/bus.d.ts +79 -0
  73. package/packages/mailx-store/bus.d.ts.map +1 -0
  74. package/packages/mailx-store/bus.js +155 -0
  75. package/packages/mailx-store/bus.js.map +1 -0
  76. package/packages/mailx-store/index.d.ts +2 -0
  77. package/packages/mailx-store/index.d.ts.map +1 -1
  78. package/packages/mailx-store/index.js +5 -0
  79. package/packages/mailx-store/index.js.map +1 -1
  80. package/packages/mailx-store/index.ts +7 -0
  81. package/packages/mailx-store/package.json +2 -1
  82. package/packages/mailx-store-web/db.d.ts.map +1 -1
  83. package/packages/mailx-store-web/db.js +9 -1
  84. package/packages/mailx-store-web/db.js.map +1 -1
  85. package/packages/mailx-store-web/db.ts +9 -1
  86. package/packages/mailx-store-web/package.json +2 -1
  87. package/packages/mailx-store-web/sync-manager.d.ts +4 -0
  88. package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
  89. package/packages/mailx-store-web/sync-manager.js +32 -4
  90. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  91. package/packages/mailx-store-web/sync-manager.ts +28 -4
  92. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-24548 → node_modules.npmglobalize-stash-444}/.package-lock.json +0 -0
@@ -7,7 +7,7 @@
7
7
  import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-direct";
8
8
  import type { TransportFactory } from "@bobfrankston/tcp-transport";
9
9
  import { authenticateOAuth } from "@bobfrankston/oauthsupport";
10
- import { MailxDB, FileMessageStore, parseSerial } from "@bobfrankston/mailx-store";
10
+ import { MailxDB, FileMessageStore, parseSerial, storeBus } from "@bobfrankston/mailx-store";
11
11
  import { loadSettings, getStorePath, getConfigDir, getHistoryDays, getPrefetch } from "@bobfrankston/mailx-settings";
12
12
  import type { AccountConfig, MessageEnvelope, EmailAddress, Folder } from "@bobfrankston/mailx-types";
13
13
  import { EventEmitter } from "node:events";
@@ -283,6 +283,22 @@ export class ImapManager extends EventEmitter {
283
283
  console.log(` [reconcile-cancel] ${info.accountId} ${info.fromFolderId}/${info.fromUid}: deferred delete cancelled (move-detect rebound to ${info.toFolderId}/${info.toUid})`);
284
284
  }
285
285
  });
286
+ // Bridge legacy EventEmitter "folderCountsChanged" to the Store bus
287
+ // so subscribers can listen on a single mechanism. Done in-class
288
+ // rather than at every call site (~20 emit points in this file) to
289
+ // keep the diff small and avoid a per-call regression risk during
290
+ // the refactor. The Store bus is the long-term home; once every
291
+ // consumer subscribes via the bus, the legacy listeners in
292
+ // bin/mailx.ts get retired and the `this.emit("folderCountsChanged"...)`
293
+ // calls can convert in bulk.
294
+ this.on("folderCountsChanged", (accountId: string, payload: any) => {
295
+ storeBus.publish({
296
+ topic: `account:${accountId}`,
297
+ kind: "folderCountsChanged",
298
+ accountId,
299
+ folderId: payload?.folderId,
300
+ });
301
+ });
286
302
  }
287
303
 
288
304
  /** Get OAuth access token for an account (for SMTP auth) */
@@ -2945,135 +2961,6 @@ export class ImapManager extends EventEmitter {
2945
2961
  return this.bodyStore;
2946
2962
  }
2947
2963
 
2948
- /** Bulk trash messages — local-first, single IMAP connection for all */
2949
- async trashMessages(accountId: string, messages: { uid: number; folderId: number }[]): Promise<void> {
2950
- if (messages.length === 0) return;
2951
- const trash = this.findFolder(accountId, "trash");
2952
-
2953
- // Tombstone each Message-ID so sync won't re-import the source-folder
2954
- // row before the server-side MOVE completes. Cleared on permanent
2955
- // failure (clearTombstoneForUid in processSyncActions).
2956
- for (const msg of messages) {
2957
- const env = this.db.getMessageByUid(accountId, msg.uid, msg.folderId);
2958
- if (env?.messageId) this.db.addTombstone(accountId, env.messageId, env.subject || "");
2959
- }
2960
-
2961
- // Local first — move to trash folder locally so the row stays
2962
- // visible in Trash and Ctrl+Z can restore it. Body file stays in
2963
- // its original folder dir; the next sync rebinds path on
2964
- // membership uid change. Old behavior was `db.deleteMessage` +
2965
- // `unlinkBodyFile` which made undelete impossible (no row to
2966
- // restore, no body to read). For folders that ARE the trash
2967
- // already, fall through to hard delete (the action will EXPUNGE
2968
- // and reconciliation cleans up).
2969
- for (const msg of messages) {
2970
- if (trash && trash.id !== msg.folderId) {
2971
- this.db.moveMessageLocal(accountId, msg.uid, msg.folderId, trash.id);
2972
- } else {
2973
- this.unlinkBodyFile(accountId, msg.uid, msg.folderId).catch(() => {});
2974
- this.db.deleteMessage(accountId, msg.uid, "user-initiated trash (already in trash → expunge)", "mailx-imap trashMessages");
2975
- }
2976
- }
2977
- console.log(` Trashed ${messages.length} messages locally (moved to trash folder, body files retained)`);
2978
-
2979
- // Queue IMAP actions
2980
- for (const msg of messages) {
2981
- if (trash && trash.id !== msg.folderId) {
2982
- this.db.queueSyncAction(accountId, "move", msg.uid, msg.folderId, { targetFolderId: trash.id });
2983
- } else {
2984
- this.db.queueSyncAction(accountId, "delete", msg.uid, msg.folderId);
2985
- }
2986
- }
2987
-
2988
- // Recalc folder counts so the tree badge updates immediately instead
2989
- // of showing stale numbers until the next full sync.
2990
- const sourceFolderIds = new Set(messages.map(m => m.folderId));
2991
- for (const fid of sourceFolderIds) this.db.recalcFolderCounts(fid);
2992
- if (trash) this.db.recalcFolderCounts(trash.id);
2993
- this.emit("folderCountsChanged", accountId, {});
2994
-
2995
- // Process all queued actions in one IMAP session
2996
- this.debounceSyncActions(accountId);
2997
- }
2998
-
2999
- /** Bulk move messages — queues the IMAP action only. The service layer
3000
- * (MailxService.moveMessages) owns the local DB mutation via
3001
- * updateMessageFolder; this method used to ALSO deleteMessage here,
3002
- * which wiped the row the service just updated — the message vanished
3003
- * on the next reconcile and "spam folder empty" was the symptom. */
3004
- async moveMessages(accountId: string, messages: { uid: number; folderId: number }[], targetFolderId: number): Promise<void> {
3005
- if (messages.length === 0) return;
3006
- for (const msg of messages) {
3007
- this.db.queueSyncAction(accountId, "move", msg.uid, msg.folderId, { targetFolderId });
3008
- }
3009
- console.log(` [move] ${accountId}: queued IMAP MOVE for ${messages.length} message(s) → folder ${targetFolderId}`);
3010
- this.debounceSyncActions(accountId);
3011
- }
3012
-
3013
- /** Debounced sync actions — batches rapid local changes into one IMAP operation */
3014
- private syncActionTimers = new Map<string, ReturnType<typeof setTimeout>>();
3015
-
3016
- private debounceSyncActions(accountId: string): void {
3017
- const existing = this.syncActionTimers.get(accountId);
3018
- if (existing) clearTimeout(existing);
3019
- this.syncActionTimers.set(accountId, setTimeout(() => {
3020
- this.syncActionTimers.delete(accountId);
3021
- this.processSyncActions(accountId).catch(() => {});
3022
- }, 1000));
3023
- }
3024
-
3025
- /** Move a message to Trash (delete) — local-first, queues IMAP sync */
3026
- async trashMessage(accountId: string, folderId: number, uid: number): Promise<void> {
3027
- const trash = this.findFolder(accountId, "trash");
3028
-
3029
- // Tombstone the Message-ID so sync won't re-import the row in the
3030
- // source folder before the server-side move completes. Cleared on
3031
- // permanent failure of the queued sync_action (see processSyncActions
3032
- // catch block, where clearTombstoneForUid runs after attempts >= 5)
3033
- // so the user sees the row reappear when their action didn't take.
3034
- const env = this.db.getMessageByUid(accountId, uid, folderId);
3035
- if (env?.messageId) this.db.addTombstone(accountId, env.messageId, env.subject || "");
3036
-
3037
- // Local first — move to trash folder so the row stays visible in
3038
- // Trash and Ctrl+Z can restore. Body file retained for undelete.
3039
- // If we're already in trash (or no trash configured), fall through
3040
- // to hard delete + EXPUNGE.
3041
- if (trash && trash.id !== folderId) {
3042
- this.db.moveMessageLocal(accountId, uid, folderId, trash.id);
3043
- } else {
3044
- this.unlinkBodyFile(accountId, uid, folderId).catch(() => {});
3045
- this.db.deleteMessage(accountId, uid, "user-initiated trash (already in trash → expunge)", "mailx-imap trashMessage");
3046
- }
3047
-
3048
- // Queue IMAP action + log the resolution so "I deleted a message and
3049
- // now it's in neither trash nor deleted" is diagnosable from the log.
3050
- if (trash && trash.id !== folderId) {
3051
- const trashFolder = this.db.getFolders(accountId).find(f => f.id === trash.id);
3052
- this.db.queueSyncAction(accountId, "move", uid, folderId, { targetFolderId: trash.id });
3053
- console.log(` [trash] ${accountId} UID ${uid}: queued MOVE to "${trashFolder?.path || trash.path}" (id=${trash.id}, specialUse=trash)`);
3054
- } else {
3055
- this.db.queueSyncAction(accountId, "delete", uid, folderId);
3056
- console.log(` [trash] ${accountId} UID ${uid}: queued EXPUNGE in folder ${folderId} (already in trash or no trash configured)`);
3057
- }
3058
-
3059
- // Folder counts moved — refresh both source and trash so the
3060
- // tree badges update immediately, not at the next sync.
3061
- this.db.recalcFolderCounts(folderId);
3062
- if (trash && trash.id !== folderId) this.db.recalcFolderCounts(trash.id);
3063
- this.emit("folderCountsChanged", accountId, {});
3064
-
3065
- // Debounced sync — batches multiple deletes into one IMAP session
3066
- this.debounceSyncActions(accountId);
3067
- }
3068
-
3069
- /** Move a message between folders — queues IMAP sync only. Service
3070
- * layer owns the local DB update (see MailxService.moveMessage). */
3071
- async moveMessage(accountId: string, uid: number, fromFolderId: number, toFolderId: number): Promise<void> {
3072
- this.db.queueSyncAction(accountId, "move", uid, fromFolderId, { targetFolderId: toFolderId });
3073
- console.log(` [move] ${accountId}: queued IMAP MOVE UID ${uid} folder ${fromFolderId} → ${toFolderId}`);
3074
- this.debounceSyncActions(accountId);
3075
- }
3076
-
3077
2964
  /** Move message across accounts using iflow's moveMessageToServer */
3078
2965
  async moveMessageCrossAccount(
3079
2966
  fromAccountId: string, uid: number, fromFolderId: number,
@@ -3102,55 +2989,6 @@ export class ImapManager extends EventEmitter {
3102
2989
  });
3103
2990
  }
3104
2991
 
3105
- /** Undelete — move from Trash back to original folder. Local-first:
3106
- * the row was moved (not deleted) on trash, so we just move it back
3107
- * in the local DB and reconcile the IMAP queue. Two cases:
3108
- * (a) the to-trash MOVE is still pending — cancel it; the server
3109
- * never saw the delete, so no counter-action is needed.
3110
- * (b) the to-trash MOVE drained — the message is now in Trash on
3111
- * the server with a new uid. Queue a counter-move from
3112
- * trash → original. The IMAP processor's fetchByUid in trash
3113
- * will use the local membership uid (which the reconciler
3114
- * rebound to the server's new trash uid via Message-ID match).
3115
- * If reconcile hasn't run yet (unlikely race), action retries
3116
- * until it does. */
3117
- async undeleteMessage(accountId: string, uid: number, originalFolderId: number): Promise<void> {
3118
- const trash = this.findFolder(accountId, "trash");
3119
- if (!trash) throw new Error("No Trash folder found");
3120
-
3121
- // Move locally back to the original folder.
3122
- const moved = this.db.moveMessageLocal(accountId, uid, trash.id, originalFolderId);
3123
- if (!moved) {
3124
- console.log(` [undelete] ${accountId} UID ${uid}: no row in trash — nothing to restore locally (sync may have already pruned)`);
3125
- }
3126
-
3127
- // (a) cancel still-pending to-trash action.
3128
- const pending = this.db.findPendingSyncAction(accountId, "move", uid, originalFolderId, trash.id);
3129
- if (pending) {
3130
- this.db.completeSyncAction(pending.id);
3131
- console.log(` [undelete] ${accountId} UID ${uid}: cancelled pending MOVE to trash (server never saw delete)`);
3132
- this.emit("folderCountsChanged", accountId, {});
3133
- return;
3134
- }
3135
-
3136
- // (b) queue counter-move from trash → original.
3137
- this.db.queueSyncAction(accountId, "move", uid, trash.id, { targetFolderId: originalFolderId });
3138
- console.log(` [undelete] ${accountId} UID ${uid}: queued counter-MOVE trash → folder ${originalFolderId}`);
3139
- this.debounceSyncActions(accountId);
3140
- this.emit("folderCountsChanged", accountId, {});
3141
- }
3142
-
3143
- /** Update flags — local-first, queues IMAP sync */
3144
- async updateFlagsLocal(accountId: string, uid: number, folderId: number, flags: string[]): Promise<void> {
3145
- this.db.updateMessageFlags(accountId, uid, flags);
3146
- this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
3147
- // User-visible pink-dot pending state stays until the action drains.
3148
- // The 30-second periodic tick was too slow — opening one message to
3149
- // auto-mark-as-read left it pink for half a minute. Same 1-second
3150
- // debounce as moves/deletes batches rapid flag churn without the
3151
- // visual lag.
3152
- this.debounceSyncActions(accountId);
3153
- }
3154
2992
 
3155
2993
  /** Process pending sync actions for an account */
3156
2994
  async processSyncActions(accountId: string): Promise<void> {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bobfrankston/mailx-imap",
9
- "version": "0.1.44",
9
+ "version": "0.1.45",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/iflow-direct": "^0.1.27",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Main-thread client for the DB factor worker.
3
+ *
4
+ * Spawns `db-worker.js` in a Node `worker_thread`, performs the init
5
+ * handshake (passes dbPath + storePath), and returns a `WorkerBus` the
6
+ * caller can use to send read requests and subscribe to DB events.
7
+ *
8
+ * The init handshake is a single `{kind:"init"}` postMessage followed by an
9
+ * `{kind:"init-ok"}` or `{kind:"init-error"}` reply. This is done OUTSIDE
10
+ * the WorkerBus message protocol so a bus method can't accidentally race
11
+ * the construction of LocalStore inside the worker.
12
+ *
13
+ * Failure mode: if the worker can't load (the file is missing in a packaged
14
+ * build, the DB can't be opened, etc.), `spawnDbWorker()` rejects. The
15
+ * caller can fall back to in-process LocalStore — same code, same answers,
16
+ * just shares the event loop with whatever else is running on main. That's
17
+ * the legacy path; the worker is the new default but we don't paint
18
+ * ourselves into a corner.
19
+ */
20
+ import { Worker } from "node:worker_threads";
21
+ import { WorkerBus } from "@bobfrankston/mailx-bus";
22
+ export interface SpawnedDbWorker {
23
+ bus: WorkerBus;
24
+ worker: Worker;
25
+ /** Stop the worker. After this, requests over the bus reject. */
26
+ close(): Promise<void>;
27
+ }
28
+ export declare function spawnDbWorker(opts: {
29
+ dbPath: string;
30
+ storePath: string;
31
+ }): Promise<SpawnedDbWorker>;
32
+ //# sourceMappingURL=db-worker-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-worker-client.d.ts","sourceRoot":"","sources":["db-worker-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,SAAS,EAAwB,MAAM,yBAAyB,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,SAAS,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CA0CzG"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Main-thread client for the DB factor worker.
3
+ *
4
+ * Spawns `db-worker.js` in a Node `worker_thread`, performs the init
5
+ * handshake (passes dbPath + storePath), and returns a `WorkerBus` the
6
+ * caller can use to send read requests and subscribe to DB events.
7
+ *
8
+ * The init handshake is a single `{kind:"init"}` postMessage followed by an
9
+ * `{kind:"init-ok"}` or `{kind:"init-error"}` reply. This is done OUTSIDE
10
+ * the WorkerBus message protocol so a bus method can't accidentally race
11
+ * the construction of LocalStore inside the worker.
12
+ *
13
+ * Failure mode: if the worker can't load (the file is missing in a packaged
14
+ * build, the DB can't be opened, etc.), `spawnDbWorker()` rejects. The
15
+ * caller can fall back to in-process LocalStore — same code, same answers,
16
+ * just shares the event loop with whatever else is running on main. That's
17
+ * the legacy path; the worker is the new default but we don't paint
18
+ * ourselves into a corner.
19
+ */
20
+ import { Worker } from "node:worker_threads";
21
+ import { fileURLToPath } from "node:url";
22
+ import { dirname, join } from "node:path";
23
+ import { WorkerBus } from "@bobfrankston/mailx-bus";
24
+ export async function spawnDbWorker(opts) {
25
+ const here = dirname(fileURLToPath(import.meta.url));
26
+ const workerPath = join(here, "db-worker.js");
27
+ const worker = new Worker(workerPath);
28
+ // Don't let the worker prevent process exit. Phase 1 design choice — the
29
+ // service shuts down cleanly via its own gracefulShutdown which closes
30
+ // the worker explicitly; without unref, a stuck worker could block exit.
31
+ worker.unref();
32
+ // Init handshake — promise resolves on init-ok, rejects on init-error.
33
+ await new Promise((resolve, reject) => {
34
+ const onMessage = (msg) => {
35
+ if (!msg || typeof msg !== "object")
36
+ return;
37
+ if (msg.kind === "init-ok") {
38
+ worker.off("message", onMessage);
39
+ resolve();
40
+ }
41
+ else if (msg.kind === "init-error") {
42
+ worker.off("message", onMessage);
43
+ reject(new Error(`db-worker init: ${msg.error}`));
44
+ }
45
+ // Ignore other messages (bus traffic) — the bus is built AFTER
46
+ // we resolve, so any bus message arriving here is stale.
47
+ };
48
+ worker.on("message", onMessage);
49
+ worker.on("error", (e) => {
50
+ worker.off("message", onMessage);
51
+ reject(e);
52
+ });
53
+ worker.postMessage({ kind: "init", dbPath: opts.dbPath, storePath: opts.storePath });
54
+ });
55
+ // Wrap the post-init port in a WorkerBus on this side. The worker has
56
+ // already wrapped its parentPort in a WorkerBus inside its init handler.
57
+ const bus = new WorkerBus(worker);
58
+ return {
59
+ bus,
60
+ worker,
61
+ async close() {
62
+ await worker.terminate();
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=db-worker-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-worker-client.js","sourceRoot":"","sources":["db-worker-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAwB,MAAM,yBAAyB,CAAC;AAS1E,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAA2C;IAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;IACtC,yEAAyE;IACzE,uEAAuE;IACvE,yEAAyE;IACzE,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,uEAAuE;IACvE,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,SAAS,GAAG,CAAC,GAAQ,EAAQ,EAAE;YACjC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO;YAC5C,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACjC,OAAO,EAAE,CAAC;YACd,CAAC;iBAAM,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACjC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACtD,CAAC;YACD,+DAA+D;YAC/D,yDAAyD;QAC7D,CAAC,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAQ,EAAE,EAAE;YAC5B,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACjC,MAAM,CAAC,CAAC,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,sEAAsE;IACtE,yEAAyE;IACzE,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,MAAoC,CAAC,CAAC;IAEhE,OAAO;QACH,GAAG;QACH,MAAM;QACN,KAAK,CAAC,KAAK;YACP,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;QAC7B,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Main-thread client for the DB factor worker.
3
+ *
4
+ * Spawns `db-worker.js` in a Node `worker_thread`, performs the init
5
+ * handshake (passes dbPath + storePath), and returns a `WorkerBus` the
6
+ * caller can use to send read requests and subscribe to DB events.
7
+ *
8
+ * The init handshake is a single `{kind:"init"}` postMessage followed by an
9
+ * `{kind:"init-ok"}` or `{kind:"init-error"}` reply. This is done OUTSIDE
10
+ * the WorkerBus message protocol so a bus method can't accidentally race
11
+ * the construction of LocalStore inside the worker.
12
+ *
13
+ * Failure mode: if the worker can't load (the file is missing in a packaged
14
+ * build, the DB can't be opened, etc.), `spawnDbWorker()` rejects. The
15
+ * caller can fall back to in-process LocalStore — same code, same answers,
16
+ * just shares the event loop with whatever else is running on main. That's
17
+ * the legacy path; the worker is the new default but we don't paint
18
+ * ourselves into a corner.
19
+ */
20
+
21
+ import { Worker } from "node:worker_threads";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, join } from "node:path";
24
+ import { WorkerBus, type MessagePortLike } from "@bobfrankston/mailx-bus";
25
+
26
+ export interface SpawnedDbWorker {
27
+ bus: WorkerBus;
28
+ worker: Worker;
29
+ /** Stop the worker. After this, requests over the bus reject. */
30
+ close(): Promise<void>;
31
+ }
32
+
33
+ export async function spawnDbWorker(opts: { dbPath: string; storePath: string }): Promise<SpawnedDbWorker> {
34
+ const here = dirname(fileURLToPath(import.meta.url));
35
+ const workerPath = join(here, "db-worker.js");
36
+ const worker = new Worker(workerPath);
37
+ // Don't let the worker prevent process exit. Phase 1 design choice — the
38
+ // service shuts down cleanly via its own gracefulShutdown which closes
39
+ // the worker explicitly; without unref, a stuck worker could block exit.
40
+ worker.unref();
41
+
42
+ // Init handshake — promise resolves on init-ok, rejects on init-error.
43
+ await new Promise<void>((resolve, reject) => {
44
+ const onMessage = (msg: any): void => {
45
+ if (!msg || typeof msg !== "object") return;
46
+ if (msg.kind === "init-ok") {
47
+ worker.off("message", onMessage);
48
+ resolve();
49
+ } else if (msg.kind === "init-error") {
50
+ worker.off("message", onMessage);
51
+ reject(new Error(`db-worker init: ${msg.error}`));
52
+ }
53
+ // Ignore other messages (bus traffic) — the bus is built AFTER
54
+ // we resolve, so any bus message arriving here is stale.
55
+ };
56
+ worker.on("message", onMessage);
57
+ worker.on("error", (e: Error) => {
58
+ worker.off("message", onMessage);
59
+ reject(e);
60
+ });
61
+ worker.postMessage({ kind: "init", dbPath: opts.dbPath, storePath: opts.storePath });
62
+ });
63
+
64
+ // Wrap the post-init port in a WorkerBus on this side. The worker has
65
+ // already wrapped its parentPort in a WorkerBus inside its init handler.
66
+ const bus = new WorkerBus(worker as unknown as MessagePortLike);
67
+
68
+ return {
69
+ bus,
70
+ worker,
71
+ async close(): Promise<void> {
72
+ await worker.terminate();
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * DB factor — Node worker thread.
3
+ *
4
+ * Owns the SQLite handle and FileMessageStore. Serves the UI-read surface
5
+ * (getFolders, getMessages, getUnifiedInbox, getMessage, searchMessages,
6
+ * searchContacts, getThreadMessages) over `mailx-bus`. Subscribes to write
7
+ * events from the sync factor (Phase 2) and emits `db:messageAdded` /
8
+ * `db:messageRemoved` / `db:flagChanged` so the UI can update incrementally
9
+ * without re-asking.
10
+ *
11
+ * **Why a worker.** Until now every UI click had to traverse the same Node
12
+ * event loop the sync code runs on. A 14-minute synchronous freeze in the
13
+ * sync path (Dovecot SELECT hang, Prefirst.OIA backfill loop) starved every
14
+ * UI read. The IPC pipe handler couldn't even be scheduled. From the user's
15
+ * perspective: Sent shows empty, message bodies never load, autocomplete
16
+ * dies, drafts can't save. All of those have one cause: the read path
17
+ * shares an event loop with the write/sync path.
18
+ *
19
+ * In this worker, the read path is on its own thread. Sync can freeze for
20
+ * an hour and the user's click still gets a 5 ms answer from local SQLite.
21
+ *
22
+ * **Topology**. Main thread spawns this worker once, holds a `WorkerBus` over
23
+ * the `parentPort`. The worker also holds a `WorkerBus` over `parentPort` and
24
+ * registers its handlers. Two ends of the same channel, mirror APIs.
25
+ *
26
+ * **DB connection**. `better-sqlite3` is in-process synchronous, so the
27
+ * worker opens its OWN connection to the same `.db` file. WAL mode (set in
28
+ * `MailxDB`) makes concurrent readers safe; writes still happen on the main
29
+ * thread for Phase 1 and the worker sees committed rows immediately.
30
+ * Phase 2 moves writes to the sync worker, which routes through this worker
31
+ * for any UI-visible mutation so the LRU/cache here can update in lock-step.
32
+ *
33
+ * **No mailparser here.** Body parsing is the parse-worker's job. This
34
+ * worker reads the .eml bytes off disk and delegates parsing via
35
+ * `parseSerial` (which already postMessages to its own worker). Two workers
36
+ * either side of this thread; the read coordinator is purely an indexer.
37
+ */
38
+ export {};
39
+ //# sourceMappingURL=db-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-worker.d.ts","sourceRoot":"","sources":["db-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * DB factor — Node worker thread.
3
+ *
4
+ * Owns the SQLite handle and FileMessageStore. Serves the UI-read surface
5
+ * (getFolders, getMessages, getUnifiedInbox, getMessage, searchMessages,
6
+ * searchContacts, getThreadMessages) over `mailx-bus`. Subscribes to write
7
+ * events from the sync factor (Phase 2) and emits `db:messageAdded` /
8
+ * `db:messageRemoved` / `db:flagChanged` so the UI can update incrementally
9
+ * without re-asking.
10
+ *
11
+ * **Why a worker.** Until now every UI click had to traverse the same Node
12
+ * event loop the sync code runs on. A 14-minute synchronous freeze in the
13
+ * sync path (Dovecot SELECT hang, Prefirst.OIA backfill loop) starved every
14
+ * UI read. The IPC pipe handler couldn't even be scheduled. From the user's
15
+ * perspective: Sent shows empty, message bodies never load, autocomplete
16
+ * dies, drafts can't save. All of those have one cause: the read path
17
+ * shares an event loop with the write/sync path.
18
+ *
19
+ * In this worker, the read path is on its own thread. Sync can freeze for
20
+ * an hour and the user's click still gets a 5 ms answer from local SQLite.
21
+ *
22
+ * **Topology**. Main thread spawns this worker once, holds a `WorkerBus` over
23
+ * the `parentPort`. The worker also holds a `WorkerBus` over `parentPort` and
24
+ * registers its handlers. Two ends of the same channel, mirror APIs.
25
+ *
26
+ * **DB connection**. `better-sqlite3` is in-process synchronous, so the
27
+ * worker opens its OWN connection to the same `.db` file. WAL mode (set in
28
+ * `MailxDB`) makes concurrent readers safe; writes still happen on the main
29
+ * thread for Phase 1 and the worker sees committed rows immediately.
30
+ * Phase 2 moves writes to the sync worker, which routes through this worker
31
+ * for any UI-visible mutation so the LRU/cache here can update in lock-step.
32
+ *
33
+ * **No mailparser here.** Body parsing is the parse-worker's job. This
34
+ * worker reads the .eml bytes off disk and delegates parsing via
35
+ * `parseSerial` (which already postMessages to its own worker). Two workers
36
+ * either side of this thread; the read coordinator is purely an indexer.
37
+ */
38
+ import { parentPort } from "node:worker_threads";
39
+ import { WorkerBus } from "@bobfrankston/mailx-bus";
40
+ import { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
41
+ import { LocalStore } from "./local-store.js";
42
+ if (!parentPort) {
43
+ throw new Error("db-worker: must be spawned as a worker, parentPort is null");
44
+ }
45
+ // Wait for the init message before constructing the DB — the main thread
46
+ // owns the resolved paths and passes them in.
47
+ let store = null;
48
+ let initialized = false;
49
+ parentPort.once("message", async (msg) => {
50
+ if (msg?.kind !== "init") {
51
+ parentPort.postMessage({ kind: "init-error", error: `expected "init" message, got ${JSON.stringify(msg)}` });
52
+ return;
53
+ }
54
+ try {
55
+ const db = new MailxDB(msg.dbPath);
56
+ const bodyStore = new FileMessageStore(msg.storePath);
57
+ store = new LocalStore(db, bodyStore);
58
+ initialized = true;
59
+ // Now that the store is up, register handlers and ack init.
60
+ registerHandlers(store);
61
+ parentPort.postMessage({ kind: "init-ok" });
62
+ }
63
+ catch (e) {
64
+ parentPort.postMessage({ kind: "init-error", error: e?.message || String(e) });
65
+ }
66
+ });
67
+ function registerHandlers(s) {
68
+ // The bus speaks over `parentPort`. `WorkerBus` reads the same channel
69
+ // we just consumed `init` from — that's fine, `once()` removes the
70
+ // single-shot listener and the bus's own `on("message")` takes over.
71
+ const bus = new WorkerBus(parentPort);
72
+ // ── Folders ─────────────────────────────────────────────────────
73
+ bus.register("db:getFolders", ({ accountId }) => s.getFolders(accountId));
74
+ // ── Message list ────────────────────────────────────────────────
75
+ bus.register("db:getMessages", (q) => s.getMessages(q));
76
+ bus.register("db:getUnifiedInbox", ({ page = 1, pageSize = 50 }) => s.getUnifiedInbox(page, pageSize));
77
+ bus.register("db:searchMessages", ({ query, page = 1, pageSize = 50, accountId, folderId, includeTrashSpam = false }) => s.searchMessages(query, page, pageSize, accountId, folderId, includeTrashSpam));
78
+ // ── Single message (envelope + body) ────────────────────────────
79
+ bus.register("db:getMessage", ({ accountId, uid, allowRemote = false, folderId }) => s.getMessage(accountId, uid, allowRemote, folderId));
80
+ bus.register("db:getThreadMessages", ({ accountId, threadId }) => s.getThreadMessages?.(accountId, threadId) ?? []);
81
+ // ── Contacts read (lookups) ─────────────────────────────────────
82
+ bus.register("db:searchContacts", ({ query, limit }) => s.searchContacts?.(query, limit) ?? []);
83
+ // ── Accounts (read-only DB shape) ───────────────────────────────
84
+ bus.register("db:getAccounts", () => s.getAccounts());
85
+ // ── Config-cache invalidation ──────────────────────────────────
86
+ // Main thread receives `configChanged` events (allowlist.jsonc etc.)
87
+ // and publishes them; the worker invalidates its in-LocalStore caches
88
+ // so the next read sees the fresh JSONC.
89
+ bus.subscribe("config:changed", () => {
90
+ s.invalidateConfigCaches();
91
+ });
92
+ }
93
+ // Defensive: if the bus is ever asked something before init completes, the
94
+ // caller gets a clear error rather than a hang. Mirrors the init-error path.
95
+ parentPort.on("message", (msg) => {
96
+ if (!initialized && msg && typeof msg === "object" && msg.kind === "request") {
97
+ parentPort.postMessage({
98
+ kind: "reply",
99
+ id: msg.id,
100
+ error: "db-worker: not initialized — main thread must send {kind:'init'} first",
101
+ });
102
+ }
103
+ });
104
+ //# sourceMappingURL=db-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-worker.js","sourceRoot":"","sources":["db-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAwB,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,IAAI,CAAC,UAAU,EAAE,CAAC;IACd,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;AAClF,CAAC;AAQD,yEAAyE;AACzE,8CAA8C;AAC9C,IAAI,KAAK,GAAsB,IAAI,CAAC;AACpC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,GAAgB,EAAE,EAAE;IAClD,IAAI,GAAG,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;QACvB,UAAW,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,gCAAgC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9G,OAAO;IACX,CAAC;IACD,IAAI,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACtD,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QACtC,WAAW,GAAG,IAAI,CAAC;QACnB,4DAA4D;QAC5D,gBAAgB,CAAC,KAAK,CAAC,CAAC;QACxB,UAAW,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QACd,UAAW,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpF,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,SAAS,gBAAgB,CAAC,CAAa;IACnC,uEAAuE;IACvE,mEAAmE;IACnE,qEAAqE;IACrE,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,UAAwC,CAAC,CAAC;IAEpE,mEAAmE;IACnE,GAAG,CAAC,QAAQ,CACR,eAAe,EACf,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAC7C,CAAC;IAEF,mEAAmE;IACnE,GAAG,CAAC,QAAQ,CACR,gBAAgB,EAChB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAC1B,CAAC;IAEF,GAAG,CAAC,QAAQ,CACR,oBAAoB,EACpB,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,QAAQ,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CACrE,CAAC;IAEF,GAAG,CAAC,QAAQ,CAIR,mBAAmB,EACnB,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,EAAE,QAAQ,GAAG,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,GAAG,KAAK,EAAE,EAAE,EAAE,CAClF,CAAC,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CACrF,CAAC;IAEF,mEAAmE;IACnE,GAAG,CAAC,QAAQ,CACR,eAAe,EACf,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAClD,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,CAAC,CAC1D,CAAC;IAEF,GAAG,CAAC,QAAQ,CACR,sBAAsB,EACtB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAE,CAAS,CAAC,iBAAiB,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CACzF,CAAC;IAEF,mEAAmE;IACnE,GAAG,CAAC,QAAQ,CACR,mBAAmB,EACnB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAE,CAAS,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CACxE,CAAC;IAEF,mEAAmE;IACnE,GAAG,CAAC,QAAQ,CACR,gBAAgB,EAChB,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CACxB,CAAC;IAEF,kEAAkE;IAClE,qEAAqE;IACrE,sEAAsE;IACtE,yCAAyC;IACzC,GAAG,CAAC,SAAS,CAAwB,gBAAgB,EAAE,GAAG,EAAE;QACxD,CAAC,CAAC,sBAAsB,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC;AACP,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAQ,EAAE,EAAE;IAClC,IAAI,CAAC,WAAW,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3E,UAAW,CAAC,WAAW,CAAC;YACpB,IAAI,EAAE,OAAO;YACb,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,KAAK,EAAE,wEAAwE;SAClF,CAAC,CAAC;IACP,CAAC;AACL,CAAC,CAAC,CAAC"}