@bobfrankston/mailx-store 0.1.24 → 0.1.26
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/bus.d.ts +79 -0
- package/bus.js +155 -0
- package/charset.d.ts +15 -0
- package/charset.js +61 -0
- package/index.d.ts +5 -1
- package/index.js +8 -1
- package/package.json +8 -5
- package/parse-serial.d.ts +40 -24
- package/parse-serial.js +161 -29
- package/parse-worker.d.ts +2 -0
- package/parse-worker.js +69 -0
- package/store.d.ts +169 -0
- package/store.js +528 -0
package/bus.d.ts
ADDED
|
@@ -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
|
package/bus.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
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 class StoreBus {
|
|
31
|
+
subscribers = new Map();
|
|
32
|
+
batchDepth = 0;
|
|
33
|
+
buffer = [];
|
|
34
|
+
subscribe(topic, handler) {
|
|
35
|
+
let set = this.subscribers.get(topic);
|
|
36
|
+
if (!set) {
|
|
37
|
+
set = new Set();
|
|
38
|
+
this.subscribers.set(topic, set);
|
|
39
|
+
}
|
|
40
|
+
set.add(handler);
|
|
41
|
+
return () => {
|
|
42
|
+
const s = this.subscribers.get(topic);
|
|
43
|
+
if (s) {
|
|
44
|
+
s.delete(handler);
|
|
45
|
+
if (s.size === 0)
|
|
46
|
+
this.subscribers.delete(topic);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Publish an event. If a folderId is set and the topic is a message
|
|
51
|
+
* topic, the bus also publishes a copy to the parent folder topic so
|
|
52
|
+
* list views (subscribed to folder:<id>) wake without subscribing to
|
|
53
|
+
* every message individually. */
|
|
54
|
+
publish(event) {
|
|
55
|
+
if (this.batchDepth > 0) {
|
|
56
|
+
this.buffer.push(event);
|
|
57
|
+
// Folder fan-out also buffers; coalesces at flush time.
|
|
58
|
+
const fanned = this.fanOutToFolder(event);
|
|
59
|
+
if (fanned)
|
|
60
|
+
this.buffer.push(fanned);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.deliver(event);
|
|
64
|
+
const fanned = this.fanOutToFolder(event);
|
|
65
|
+
if (fanned)
|
|
66
|
+
this.deliver(fanned);
|
|
67
|
+
}
|
|
68
|
+
/** Run `fn` with publishes buffered. On exit, coalesces per-topic and
|
|
69
|
+
* delivers either the single event (when only one fired for that topic)
|
|
70
|
+
* or a synthetic batch event summarizing the changes. */
|
|
71
|
+
withBatch(fn) {
|
|
72
|
+
this.batchDepth++;
|
|
73
|
+
try {
|
|
74
|
+
return fn();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
this.batchDepth--;
|
|
78
|
+
if (this.batchDepth === 0)
|
|
79
|
+
this.flush();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
flush() {
|
|
83
|
+
const buf = this.buffer;
|
|
84
|
+
this.buffer = [];
|
|
85
|
+
if (buf.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const byTopic = new Map();
|
|
88
|
+
for (const e of buf) {
|
|
89
|
+
let arr = byTopic.get(e.topic);
|
|
90
|
+
if (!arr) {
|
|
91
|
+
arr = [];
|
|
92
|
+
byTopic.set(e.topic, arr);
|
|
93
|
+
}
|
|
94
|
+
arr.push(e);
|
|
95
|
+
}
|
|
96
|
+
for (const [topic, events] of byTopic) {
|
|
97
|
+
if (events.length === 1) {
|
|
98
|
+
this.deliver(events[0]);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const summary = { inserted: 0, updated: 0, deleted: 0 };
|
|
102
|
+
const uuids = new Set();
|
|
103
|
+
for (const e of events) {
|
|
104
|
+
if (e.kind === "messageInserted")
|
|
105
|
+
summary.inserted++;
|
|
106
|
+
else if (e.kind === "messageRemoved")
|
|
107
|
+
summary.deleted++;
|
|
108
|
+
else
|
|
109
|
+
summary.updated++;
|
|
110
|
+
if (e.msgUuid)
|
|
111
|
+
uuids.add(e.msgUuid);
|
|
112
|
+
}
|
|
113
|
+
this.deliver({ topic, kind: "batch", changedUuids: [...uuids], summary });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
deliver(event) {
|
|
117
|
+
const exact = this.subscribers.get(event.topic);
|
|
118
|
+
if (exact)
|
|
119
|
+
for (const h of exact) {
|
|
120
|
+
try {
|
|
121
|
+
h(event);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
console.error("[store-bus]", e);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const wild = this.subscribers.get("*");
|
|
128
|
+
if (wild)
|
|
129
|
+
for (const h of wild) {
|
|
130
|
+
try {
|
|
131
|
+
h(event);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
console.error("[store-bus]", e);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** When a message-topic event fires and carries a folderId, generate
|
|
139
|
+
* the parallel folder-topic event so folder subscribers don't have to
|
|
140
|
+
* also subscribe to every message individually. Returns null when no
|
|
141
|
+
* fan-out applies. */
|
|
142
|
+
fanOutToFolder(event) {
|
|
143
|
+
if (event.kind === "batch")
|
|
144
|
+
return null;
|
|
145
|
+
if (!event.topic.startsWith("message:"))
|
|
146
|
+
return null;
|
|
147
|
+
if (event.folderId == null)
|
|
148
|
+
return null;
|
|
149
|
+
return { ...event, topic: `folder:${event.folderId}` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Singleton bus shared by every Store consumer in the process. Workers
|
|
153
|
+
* get their own; the worker boundary serializes events via postMessage. */
|
|
154
|
+
export const storeBus = new StoreBus();
|
|
155
|
+
//# sourceMappingURL=bus.js.map
|
package/charset.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charset normalization for incoming email bodies.
|
|
3
|
+
*
|
|
4
|
+
* Many senders (esp. PHPMailer-driven marketing) declare
|
|
5
|
+
* `charset=iso-8859-1` but emit UTF-8 bytes. simpleParser honors the
|
|
6
|
+
* declared charset and produces "â??" garbage for every non-ASCII
|
|
7
|
+
* codepoint (em-dash, smart quotes, …). When the raw body bytes are
|
|
8
|
+
* valid UTF-8, rewrite the charset header before parsing. We only
|
|
9
|
+
* override the obviously-wrong legacy declarations; explicit utf-8 /
|
|
10
|
+
* koi8 / etc. pass through.
|
|
11
|
+
*/
|
|
12
|
+
/** Returns either the original buffer (no change needed) or a copy with
|
|
13
|
+
* the leading charset declaration rewritten to utf-8. */
|
|
14
|
+
export declare function sniffAndFixCharset(raw: Buffer): Buffer;
|
|
15
|
+
//# sourceMappingURL=charset.d.ts.map
|
package/charset.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charset normalization for incoming email bodies.
|
|
3
|
+
*
|
|
4
|
+
* Many senders (esp. PHPMailer-driven marketing) declare
|
|
5
|
+
* `charset=iso-8859-1` but emit UTF-8 bytes. simpleParser honors the
|
|
6
|
+
* declared charset and produces "â??" garbage for every non-ASCII
|
|
7
|
+
* codepoint (em-dash, smart quotes, …). When the raw body bytes are
|
|
8
|
+
* valid UTF-8, rewrite the charset header before parsing. We only
|
|
9
|
+
* override the obviously-wrong legacy declarations; explicit utf-8 /
|
|
10
|
+
* koi8 / etc. pass through.
|
|
11
|
+
*/
|
|
12
|
+
/** Returns either the original buffer (no change needed) or a copy with
|
|
13
|
+
* the leading charset declaration rewritten to utf-8. */
|
|
14
|
+
export function sniffAndFixCharset(raw) {
|
|
15
|
+
const HEAD_LIMIT = 16384;
|
|
16
|
+
const head = raw.subarray(0, Math.min(HEAD_LIMIT, raw.length)).toString("latin1");
|
|
17
|
+
const re = /charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi;
|
|
18
|
+
if (!re.test(head))
|
|
19
|
+
return raw;
|
|
20
|
+
if (!isValidUtf8(raw))
|
|
21
|
+
return raw;
|
|
22
|
+
const fixed = head.replace(/charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi, "charset=utf-8");
|
|
23
|
+
return Buffer.concat([Buffer.from(fixed, "latin1"), raw.subarray(head.length)]);
|
|
24
|
+
}
|
|
25
|
+
/** Strict UTF-8 validity check: rejects overlong forms, invalid start
|
|
26
|
+
* bytes, and dangling continuations. Used to confirm the body is really
|
|
27
|
+
* UTF-8 before overriding a Latin-1 declaration. */
|
|
28
|
+
function isValidUtf8(buf) {
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < buf.length) {
|
|
31
|
+
const b = buf[i];
|
|
32
|
+
if (b < 0x80) {
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
let need;
|
|
37
|
+
if ((b & 0xE0) === 0xC0) {
|
|
38
|
+
if (b < 0xC2)
|
|
39
|
+
return false;
|
|
40
|
+
need = 1;
|
|
41
|
+
}
|
|
42
|
+
else if ((b & 0xF0) === 0xE0)
|
|
43
|
+
need = 2;
|
|
44
|
+
else if ((b & 0xF8) === 0xF0) {
|
|
45
|
+
if (b > 0xF4)
|
|
46
|
+
return false;
|
|
47
|
+
need = 3;
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
return false;
|
|
51
|
+
if (i + need >= buf.length)
|
|
52
|
+
return false;
|
|
53
|
+
for (let k = 1; k <= need; k++) {
|
|
54
|
+
if ((buf[i + k] & 0xC0) !== 0x80)
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
i += need + 1;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=charset.js.map
|
package/index.d.ts
CHANGED
|
@@ -4,5 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { MailxDB } from "./db.js";
|
|
6
6
|
export { FileMessageStore } from "./file-store.js";
|
|
7
|
-
export { parseSerial } from "./parse-serial.js";
|
|
7
|
+
export { parseSerial, prewarmParseWorker } from "./parse-serial.js";
|
|
8
|
+
export { Store } from "./store.js";
|
|
9
|
+
export type { StoreMessage } from "./store.js";
|
|
10
|
+
export { StoreBus, storeBus } from "@bobfrankston/mailx-bus";
|
|
11
|
+
export type { StoreEvent, StoreEventKind, StoreEventHandler } from "@bobfrankston/mailx-bus";
|
|
8
12
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -4,5 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { MailxDB } from "./db.js";
|
|
6
6
|
export { FileMessageStore } from "./file-store.js";
|
|
7
|
-
export { parseSerial } from "./parse-serial.js";
|
|
7
|
+
export { parseSerial, prewarmParseWorker } from "./parse-serial.js";
|
|
8
|
+
// Store — the nexus. Owns DB + .eml files + operations + bus.
|
|
9
|
+
export { Store } from "./store.js";
|
|
10
|
+
// Store-event bus lives in `@bobfrankston/mailx-bus` so the browser-side
|
|
11
|
+
// store (mailx-store-web) and the desktop-side store (this package) share
|
|
12
|
+
// the same bus. Re-exported here so existing callers don't have to learn
|
|
13
|
+
// the new import path.
|
|
14
|
+
export { StoreBus, storeBus } from "@bobfrankston/mailx-bus";
|
|
8
15
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
12
|
+
"@bobfrankston/mailx-types": "^0.1.13",
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.17",
|
|
14
|
+
"@bobfrankston/mailx-bus": "^0.1.2",
|
|
14
15
|
"mailparser": "^3.7.2"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
|
@@ -23,12 +24,14 @@
|
|
|
23
24
|
".dependencies": {
|
|
24
25
|
"@bobfrankston/mailx-types": "file:../mailx-types",
|
|
25
26
|
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
27
|
+
"@bobfrankston/mailx-bus": "file:../mailx-bus",
|
|
26
28
|
"mailparser": "^3.7.2"
|
|
27
29
|
},
|
|
28
30
|
".transformedSnapshot": {
|
|
29
31
|
"dependencies": {
|
|
30
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
31
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
32
|
+
"@bobfrankston/mailx-types": "^0.1.13",
|
|
33
|
+
"@bobfrankston/mailx-settings": "^0.1.17",
|
|
34
|
+
"@bobfrankston/mailx-bus": "^0.1.2",
|
|
32
35
|
"mailparser": "^3.7.2"
|
|
33
36
|
}
|
|
34
37
|
}
|
package/parse-serial.d.ts
CHANGED
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Worker-thread-backed simpleParser dispatcher.
|
|
3
3
|
*
|
|
4
4
|
* mailparser's `simpleParser` is declared `async` but its work is CPU-bound
|
|
5
|
-
* (Node Streams + libmime decoding). When
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* (Node Streams + libmime decoding). When it runs on the main event loop:
|
|
6
|
+
* - The first parse after a fresh process takes 14-25 seconds (cold V8
|
|
7
|
+
* JIT + libmime + iconv-lite + charset-table loading). See log
|
|
8
|
+
* 2026-05-14 01:11:44 — simpleParser 14524ms for 5 KB.
|
|
9
|
+
* - Every parse, cold or warm, blocks the IPC stdin pump for its
|
|
10
|
+
* duration. A 200 ms parse delays every queued IPC message by 200 ms.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Moving the parse to a `worker_threads` Worker fixes both problems:
|
|
13
|
+
* - The main event loop stays responsive — IPC, sync, timers all run
|
|
14
|
+
* during the parse. Click-to-render latency is bounded by post-message
|
|
15
|
+
* RTT (sub-ms) + parse time on the worker, but the rest of the app
|
|
16
|
+
* stays alive.
|
|
17
|
+
* - The worker absorbs cold-start once. Subsequent parses ride the
|
|
18
|
+
* worker's warm JIT and run in their natural 50-500 ms budget.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
20
|
+
* Priority queue: foreground (UI clicks) jumps over background (sync /
|
|
21
|
+
* prefetch) when more than one parse is pending. Each parse still runs
|
|
22
|
+
* sequentially on the worker — the single-worker design intentionally
|
|
23
|
+
* mirrors the prior in-process serialization to keep CPU bounded.
|
|
19
24
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* Lazy spawn: the worker is created on first call. boot/setup code that
|
|
26
|
+
* runs no parses pays nothing.
|
|
27
|
+
*
|
|
28
|
+
* Worker lifecycle: the worker is process-singleton, never explicitly
|
|
29
|
+
* terminated. Node exits cleanly because the worker is `unref()`d so it
|
|
30
|
+
* doesn't keep the event loop alive.
|
|
24
31
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
32
|
+
* Lives in mailx-store rather than mailx-service so both the UI-hot path
|
|
33
|
+
* (mailx-service/local-store.ts) and the sync path (mailx-imap) share one
|
|
34
|
+
* worker — otherwise each module would spawn its own (parallel workers
|
|
35
|
+
* would defeat the bound on CPU, and each pays its own cold start).
|
|
28
36
|
*/
|
|
29
|
-
import {
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
37
|
+
import type { ParsedMail, Source } from "mailparser";
|
|
38
|
+
/** Spawn the parse worker early so its cold-start (mailparser module
|
|
39
|
+
* loading, V8 JIT, libmime / iconv-lite tables — empirically 14-25 s)
|
|
40
|
+
* runs in parallel with the rest of boot. Safe to call multiple times;
|
|
41
|
+
* the worker is a process-singleton. Returns immediately; the actual
|
|
42
|
+
* cold-start completes in the worker's internal self-warm. */
|
|
43
|
+
export declare function prewarmParseWorker(): void;
|
|
44
|
+
/** Serialized wrapper around `mailparser.simpleParser` running in a worker
|
|
45
|
+
* thread. `priority` defaults to "foreground" — only sync/background
|
|
46
|
+
* callers should pass "background" so user clicks aren't stuck behind a
|
|
47
|
+
* back-fill. If the worker can't be spawned (rare), falls back to an
|
|
48
|
+
* in-main-thread serial pump so the system still functions. */
|
|
33
49
|
export declare function parseSerial(source: Source, priority?: "foreground" | "background"): Promise<ParsedMail>;
|
|
34
50
|
//# sourceMappingURL=parse-serial.d.ts.map
|