@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.
Files changed (54) hide show
  1. package/ai-usage.d.ts +32 -0
  2. package/ai-usage.d.ts.map +1 -0
  3. package/ai-usage.js +98 -0
  4. package/ai-usage.js.map +1 -0
  5. package/charset.d.ts +15 -0
  6. package/charset.d.ts.map +1 -0
  7. package/charset.js +61 -0
  8. package/charset.js.map +1 -0
  9. package/db-worker-client.d.ts +32 -0
  10. package/db-worker-client.d.ts.map +1 -0
  11. package/db-worker-client.js +66 -0
  12. package/db-worker-client.js.map +1 -0
  13. package/db-worker.d.ts +39 -0
  14. package/db-worker.d.ts.map +1 -0
  15. package/db-worker.js +115 -0
  16. package/db-worker.js.map +1 -0
  17. package/google-sync.d.ts +151 -0
  18. package/google-sync.d.ts.map +1 -0
  19. package/google-sync.js +259 -0
  20. package/google-sync.js.map +1 -0
  21. package/html-to-docx.d.ts +1 -0
  22. package/index.d.ts +1102 -0
  23. package/index.d.ts.map +1 -0
  24. package/index.js +5476 -0
  25. package/index.js.map +1 -0
  26. package/jsonrpc.d.ts +29 -0
  27. package/jsonrpc.d.ts.map +1 -0
  28. package/jsonrpc.js +481 -0
  29. package/jsonrpc.js.map +1 -0
  30. package/local-store.d.ts +10 -0
  31. package/local-store.d.ts.map +1 -0
  32. package/local-store.js +9 -0
  33. package/local-store.js.map +1 -0
  34. package/package.json +53 -0
  35. package/reconciler.d.ts +90 -0
  36. package/reconciler.d.ts.map +1 -0
  37. package/reconciler.js +277 -0
  38. package/reconciler.js.map +1 -0
  39. package/sync-queue.d.ts +127 -0
  40. package/sync-queue.d.ts.map +1 -0
  41. package/sync-queue.js +271 -0
  42. package/sync-queue.js.map +1 -0
  43. package/sync-worker-client.d.ts +33 -0
  44. package/sync-worker-client.d.ts.map +1 -0
  45. package/sync-worker-client.js +89 -0
  46. package/sync-worker-client.js.map +1 -0
  47. package/sync-worker.d.ts +33 -0
  48. package/sync-worker.d.ts.map +1 -0
  49. package/sync-worker.js +143 -0
  50. package/sync-worker.js.map +1 -0
  51. package/word-html.d.ts +41 -0
  52. package/word-html.d.ts.map +1 -0
  53. package/word-html.js +122 -0
  54. 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
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-worker.js","sourceRoot":"","sources":["db-worker.ts"],"names":[],"mappings":"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"}