@bobfrankston/mailx-service 0.1.47
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/ai-usage.d.ts +32 -0
- package/ai-usage.d.ts.map +1 -0
- package/ai-usage.js +98 -0
- package/ai-usage.js.map +1 -0
- package/charset.d.ts +15 -0
- package/charset.d.ts.map +1 -0
- package/charset.js +61 -0
- package/charset.js.map +1 -0
- package/db-worker-client.d.ts +32 -0
- package/db-worker-client.d.ts.map +1 -0
- package/db-worker-client.js +66 -0
- package/db-worker-client.js.map +1 -0
- package/db-worker.d.ts +39 -0
- package/db-worker.d.ts.map +1 -0
- package/db-worker.js +115 -0
- package/db-worker.js.map +1 -0
- package/google-sync.d.ts +151 -0
- package/google-sync.d.ts.map +1 -0
- package/google-sync.js +259 -0
- package/google-sync.js.map +1 -0
- package/html-to-docx.d.ts +1 -0
- package/index.d.ts +1102 -0
- package/index.d.ts.map +1 -0
- package/index.js +5476 -0
- package/index.js.map +1 -0
- package/jsonrpc.d.ts +29 -0
- package/jsonrpc.d.ts.map +1 -0
- package/jsonrpc.js +481 -0
- package/jsonrpc.js.map +1 -0
- package/local-store.d.ts +10 -0
- package/local-store.d.ts.map +1 -0
- package/local-store.js +9 -0
- package/local-store.js.map +1 -0
- package/package.json +53 -0
- package/reconciler.d.ts +90 -0
- package/reconciler.d.ts.map +1 -0
- package/reconciler.js +277 -0
- package/reconciler.js.map +1 -0
- package/sync-queue.d.ts +127 -0
- package/sync-queue.d.ts.map +1 -0
- package/sync-queue.js +271 -0
- package/sync-queue.js.map +1 -0
- package/sync-worker-client.d.ts +33 -0
- package/sync-worker-client.d.ts.map +1 -0
- package/sync-worker-client.js +89 -0
- package/sync-worker-client.js.map +1 -0
- package/sync-worker.d.ts +33 -0
- package/sync-worker.d.ts.map +1 -0
- package/sync-worker.js +143 -0
- package/sync-worker.js.map +1 -0
- package/word-html.d.ts +41 -0
- package/word-html.d.ts.map +1 -0
- package/word-html.js +122 -0
- package/word-html.js.map +1 -0
package/ai-usage.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface AiUsageEntry {
|
|
2
|
+
ts: number;
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
action: string;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
/** null = no published price in the table for this model — tokens only. */
|
|
9
|
+
costUsd: number | null;
|
|
10
|
+
}
|
|
11
|
+
/** Price a call. Local providers are free; unpriced models return null. */
|
|
12
|
+
export declare function priceCall(provider: string, model: string, inputTokens: number, outputTokens: number): number | null;
|
|
13
|
+
/** Append one call to the usage log. Never throws — accounting must not be
|
|
14
|
+
* able to break the feature it is measuring. */
|
|
15
|
+
export declare function recordAiUsage(entry: AiUsageEntry): void;
|
|
16
|
+
export interface AiUsageSummary {
|
|
17
|
+
calls: number;
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
costUsd: number;
|
|
21
|
+
/** Calls whose model has no published price — excluded from costUsd. */
|
|
22
|
+
unpricedCalls: number;
|
|
23
|
+
byModel: Record<string, {
|
|
24
|
+
calls: number;
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
costUsd: number;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** Summarize the usage log. `sinceMs` defaults to the start of this month. */
|
|
31
|
+
export declare function summarizeAiUsage(sinceMs?: number): AiUsageSummary;
|
|
32
|
+
//# sourceMappingURL=ai-usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-usage.d.ts","sourceRoot":"","sources":["ai-usage.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAMD,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKnH;AAED;iDACiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAQvD;AAED,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1G;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,cAAc,CA4BjE"}
|
package/ai-usage.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI usage + cost accounting.
|
|
3
|
+
*
|
|
4
|
+
* Bob turned on the AI features and asked "can you monitor costs?" (2026-08-11).
|
|
5
|
+
* Nothing tracked them before, so the honest answer was "no". Every aiTransform
|
|
6
|
+
* call now appends one line to ~/.rmfmail/logs/ai-usage.jsonl and prints a
|
|
7
|
+
* running month-to-date total to the daemon log.
|
|
8
|
+
*
|
|
9
|
+
* Two rules this file exists to keep:
|
|
10
|
+
* - A LOCAL model is free. Ollama calls are recorded with costUsd 0 so the
|
|
11
|
+
* token counts are still visible, and so "$0.00 this month" is a measured
|
|
12
|
+
* fact rather than an absence of data.
|
|
13
|
+
* - A model with no price in the table records `costUsd: null`, NOT a guessed
|
|
14
|
+
* number. An invented rate is worse than a blank — it reads as authoritative.
|
|
15
|
+
* Add the model below when you know its real price.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
/** USD per MILLION tokens. Anthropic list prices as published 2026-06-24.
|
|
21
|
+
* Verify before trusting these for anything billable — vendor prices move,
|
|
22
|
+
* and this table does not auto-update. */
|
|
23
|
+
const PRICES_PER_MTOK = {
|
|
24
|
+
"claude-fable-5": { input: 10, output: 50 },
|
|
25
|
+
"claude-mythos-5": { input: 10, output: 50 },
|
|
26
|
+
"claude-opus-5": { input: 5, output: 25 },
|
|
27
|
+
"claude-opus-4-8": { input: 5, output: 25 },
|
|
28
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
29
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
30
|
+
"claude-sonnet-5": { input: 3, output: 15 },
|
|
31
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
32
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
33
|
+
};
|
|
34
|
+
function usageLogPath() {
|
|
35
|
+
return path.join(os.homedir(), ".rmfmail", "logs", "ai-usage.jsonl");
|
|
36
|
+
}
|
|
37
|
+
/** Price a call. Local providers are free; unpriced models return null. */
|
|
38
|
+
export function priceCall(provider, model, inputTokens, outputTokens) {
|
|
39
|
+
if (provider === "ollama")
|
|
40
|
+
return 0; // runs on this machine
|
|
41
|
+
const price = PRICES_PER_MTOK[model];
|
|
42
|
+
if (!price)
|
|
43
|
+
return null; // unknown — don't guess
|
|
44
|
+
return (inputTokens * price.input + outputTokens * price.output) / 1_000_000;
|
|
45
|
+
}
|
|
46
|
+
/** Append one call to the usage log. Never throws — accounting must not be
|
|
47
|
+
* able to break the feature it is measuring. */
|
|
48
|
+
export function recordAiUsage(entry) {
|
|
49
|
+
try {
|
|
50
|
+
const p = usageLogPath();
|
|
51
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
52
|
+
fs.appendFileSync(p, JSON.stringify(entry) + "\n");
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.error(` [ai-usage] could not write ${usageLogPath()}: ${e?.message || e}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Summarize the usage log. `sinceMs` defaults to the start of this month. */
|
|
59
|
+
export function summarizeAiUsage(sinceMs) {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const from = sinceMs ?? new Date(now.getFullYear(), now.getMonth(), 1).getTime();
|
|
62
|
+
const empty = { calls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, unpricedCalls: 0, byModel: {} };
|
|
63
|
+
let text;
|
|
64
|
+
try {
|
|
65
|
+
text = fs.readFileSync(usageLogPath(), "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return empty; // no log yet = no usage
|
|
69
|
+
}
|
|
70
|
+
for (const line of text.split("\n")) {
|
|
71
|
+
if (!line.trim())
|
|
72
|
+
continue;
|
|
73
|
+
let e;
|
|
74
|
+
try {
|
|
75
|
+
e = JSON.parse(line);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (e.ts < from)
|
|
81
|
+
continue;
|
|
82
|
+
empty.calls++;
|
|
83
|
+
empty.inputTokens += e.inputTokens || 0;
|
|
84
|
+
empty.outputTokens += e.outputTokens || 0;
|
|
85
|
+
if (e.costUsd == null)
|
|
86
|
+
empty.unpricedCalls++;
|
|
87
|
+
else
|
|
88
|
+
empty.costUsd += e.costUsd;
|
|
89
|
+
const key = `${e.provider}/${e.model}`;
|
|
90
|
+
const m = empty.byModel[key] ||= { calls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0 };
|
|
91
|
+
m.calls++;
|
|
92
|
+
m.inputTokens += e.inputTokens || 0;
|
|
93
|
+
m.outputTokens += e.outputTokens || 0;
|
|
94
|
+
m.costUsd += e.costUsd || 0;
|
|
95
|
+
}
|
|
96
|
+
return empty;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=ai-usage.js.map
|
package/ai-usage.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-usage.js","sourceRoot":"","sources":["ai-usage.ts"],"names":[],"mappings":"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"}
|
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.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charset.d.ts","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
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/charset.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charset.js","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,KAAK,CAAC;IACzB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClF,MAAM,EAAE,GAAG,+DAA+D,CAAC;IAC3E,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,+DAA+D,EAAE,eAAe,CAAC,CAAC;IAC7G,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;qDAEqD;AACrD,SAAS,WAAW,CAAC,GAAW;IAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;YAAC,CAAC,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QAChC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;aAC7D,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC;aAClC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;;YAClE,OAAO,KAAK,CAAC;QAClB,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;QACnD,CAAC;QACD,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
|
@@ -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"}
|
package/db-worker.d.ts
ADDED
|
@@ -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"}
|
package/db-worker.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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, Store } from "@bobfrankston/mailx-store";
|
|
41
|
+
if (!parentPort) {
|
|
42
|
+
throw new Error("db-worker: must be spawned as a worker, parentPort is null");
|
|
43
|
+
}
|
|
44
|
+
// Wait for the init message before constructing the DB — the main thread
|
|
45
|
+
// owns the resolved paths and passes them in.
|
|
46
|
+
let store = null;
|
|
47
|
+
let initialized = false;
|
|
48
|
+
parentPort.once("message", async (msg) => {
|
|
49
|
+
if (msg?.kind !== "init") {
|
|
50
|
+
parentPort.postMessage({ kind: "init-error", error: `expected "init" message, got ${JSON.stringify(msg)}` });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Read-only handle: the main thread owns all writes/migrations and has
|
|
55
|
+
// already run them before spawning us. query_only forbids any write
|
|
56
|
+
// here, and WAL lets us read committed rows the instant the writer
|
|
57
|
+
// commits. See MailxDB readOnly branch.
|
|
58
|
+
const db = new MailxDB(msg.dbPath, { readOnly: true });
|
|
59
|
+
const bodyStore = new FileMessageStore(msg.storePath);
|
|
60
|
+
store = new Store(db, bodyStore);
|
|
61
|
+
initialized = true;
|
|
62
|
+
// Now that the store is up, register handlers and ack init.
|
|
63
|
+
registerHandlers(store);
|
|
64
|
+
parentPort.postMessage({ kind: "init-ok" });
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
parentPort.postMessage({ kind: "init-error", error: e?.message || String(e) });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
function registerHandlers(s) {
|
|
71
|
+
// The bus speaks over `parentPort`. `WorkerBus` reads the same channel
|
|
72
|
+
// we just consumed `init` from — that's fine, `once()` removes the
|
|
73
|
+
// single-shot listener and the bus's own `on("message")` takes over.
|
|
74
|
+
const bus = new WorkerBus(parentPort);
|
|
75
|
+
// FTS body backfill relay. This worker's connection is query_only, so
|
|
76
|
+
// Store.getMessage can't UPDATE messages_fts here — it hands the parsed
|
|
77
|
+
// body text to us and we ship it to the main thread's writable handle.
|
|
78
|
+
// Before this hook the UPDATE failed silently and body search was
|
|
79
|
+
// preview-only for any message first parsed on the read path.
|
|
80
|
+
s.onFtsBodyBackfill = (rowId, bodyText) => {
|
|
81
|
+
bus.publish("db:ftsBodyBackfill", { rowId, bodyText });
|
|
82
|
+
};
|
|
83
|
+
// ── Folders ─────────────────────────────────────────────────────
|
|
84
|
+
bus.register("db:getFolders", ({ accountId }) => s.getFolders(accountId));
|
|
85
|
+
// ── Message list ────────────────────────────────────────────────
|
|
86
|
+
bus.register("db:getMessages", (q) => s.getMessages(q));
|
|
87
|
+
bus.register("db:getUnifiedInbox", ({ page = 1, pageSize = 50, flaggedOnly = false }) => s.getUnifiedInbox(page, pageSize, flaggedOnly));
|
|
88
|
+
bus.register("db:searchMessages", ({ query, page = 1, pageSize = 50, accountId, folderId, includeTrashSpam = false }) => s.searchMessages(query, page, pageSize, accountId, folderId, includeTrashSpam));
|
|
89
|
+
// ── Single message (envelope + body) ────────────────────────────
|
|
90
|
+
bus.register("db:getMessage", ({ accountId, uid, allowRemote = false, folderId }) => s.getMessage(accountId, uid, allowRemote, folderId));
|
|
91
|
+
bus.register("db:getThreadMessages", ({ accountId, threadId }) => s.getThreadMessages?.(accountId, threadId) ?? []);
|
|
92
|
+
// ── Contacts read (lookups) ─────────────────────────────────────
|
|
93
|
+
bus.register("db:searchContacts", ({ query, limit }) => s.searchContacts?.(query, limit) ?? []);
|
|
94
|
+
// ── Accounts (read-only DB shape) ───────────────────────────────
|
|
95
|
+
bus.register("db:getAccounts", () => s.getAccounts());
|
|
96
|
+
// ── Config-cache invalidation ──────────────────────────────────
|
|
97
|
+
// Main thread receives `configChanged` events (allowlist.jsonc etc.)
|
|
98
|
+
// and publishes them; the worker invalidates its in-Store caches
|
|
99
|
+
// so the next read sees the fresh JSONC.
|
|
100
|
+
bus.subscribe("config:changed", () => {
|
|
101
|
+
s.invalidateConfigCaches();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Defensive: if the bus is ever asked something before init completes, the
|
|
105
|
+
// caller gets a clear error rather than a hang. Mirrors the init-error path.
|
|
106
|
+
parentPort.on("message", (msg) => {
|
|
107
|
+
if (!initialized && msg && typeof msg === "object" && msg.kind === "request") {
|
|
108
|
+
parentPort.postMessage({
|
|
109
|
+
kind: "reply",
|
|
110
|
+
id: msg.id,
|
|
111
|
+
error: "db-worker: not initialized — main thread must send {kind:'init'} first",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=db-worker.js.map
|
package/db-worker.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-worker.js","sourceRoot":"","sources":["db-worker.ts"],"names":[],"mappings":"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"}
|