@bobfrankston/mailx-store 0.1.20 → 0.1.22
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/db.d.ts +1 -0
- package/db.js +23 -4
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +7 -4
- package/parse-serial.d.ts +33 -0
- package/parse-serial.js +38 -0
package/db.d.ts
CHANGED
|
@@ -83,6 +83,7 @@ export declare class MailxDB {
|
|
|
83
83
|
recurringEventId?: string;
|
|
84
84
|
htmlLink?: string;
|
|
85
85
|
isHoliday?: boolean;
|
|
86
|
+
reminderMinutes?: number[];
|
|
86
87
|
}): string;
|
|
87
88
|
getCalendarEvents(accountId: string, fromMs: number, toMs: number): any[];
|
|
88
89
|
/** Lookup by uuid only — used by patch/delete paths that don't have an
|
package/db.js
CHANGED
|
@@ -411,6 +411,15 @@ export class MailxDB {
|
|
|
411
411
|
// calendar_events table; the flag distinguishes them so the UI can
|
|
412
412
|
// style them differently and the alarm scheduler can skip them.
|
|
413
413
|
this.addColumnIfMissing("calendar_events", "is_holiday", "INTEGER NOT NULL DEFAULT 0");
|
|
414
|
+
// Per-event reminder offsets (Bob 2026-05-12: "use the reminder
|
|
415
|
+
// times in the event (there can be multiple)"). Google's API
|
|
416
|
+
// returns `reminders.overrides` as an array of {method, minutes}
|
|
417
|
+
// — we store the minute offsets as JSON so alarms.ts can fire ONE
|
|
418
|
+
// popup per offset (10-min, 30-min, 1-day, …) instead of the
|
|
419
|
+
// hard-coded 10-min default. `useDefault: true` events get the
|
|
420
|
+
// calendar's defaultReminders pre-resolved server-side and pulled
|
|
421
|
+
// through here too.
|
|
422
|
+
this.addColumnIfMissing("calendar_events", "reminder_minutes_json", "TEXT DEFAULT '[]'");
|
|
414
423
|
// Backfill UUIDs for any pre-existing rows that were inserted before
|
|
415
424
|
// this column landed. One UPDATE + an id roundtrip per row — cheap
|
|
416
425
|
// at our row counts, runs once per DB upgrade.
|
|
@@ -733,12 +742,13 @@ export class MailxDB {
|
|
|
733
742
|
// ── Calendar events (two-way cache) ──
|
|
734
743
|
upsertCalendarEvent(ev) {
|
|
735
744
|
const uuid = ev.uuid || randomUUID().replace(/-/g, "");
|
|
745
|
+
const remindersJson = JSON.stringify(Array.isArray(ev.reminderMinutes) ? ev.reminderMinutes : []);
|
|
736
746
|
this.db.prepare(`
|
|
737
747
|
INSERT INTO calendar_events
|
|
738
748
|
(uuid, account_id, provider_id, calendar_id, title, start_ms, end_ms,
|
|
739
749
|
all_day, location, notes, etag, last_synced, dirty, deleted, updated_at,
|
|
740
|
-
recurring_event_id, html_link, is_holiday)
|
|
741
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?)
|
|
750
|
+
recurring_event_id, html_link, is_holiday, reminder_minutes_json)
|
|
751
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)
|
|
742
752
|
ON CONFLICT(uuid) DO UPDATE SET
|
|
743
753
|
account_id=excluded.account_id, provider_id=excluded.provider_id,
|
|
744
754
|
calendar_id=excluded.calendar_id, title=excluded.title,
|
|
@@ -749,8 +759,9 @@ export class MailxDB {
|
|
|
749
759
|
updated_at=excluded.updated_at,
|
|
750
760
|
recurring_event_id=excluded.recurring_event_id,
|
|
751
761
|
html_link=excluded.html_link,
|
|
752
|
-
is_holiday=excluded.is_holiday
|
|
753
|
-
|
|
762
|
+
is_holiday=excluded.is_holiday,
|
|
763
|
+
reminder_minutes_json=excluded.reminder_minutes_json
|
|
764
|
+
`).run(uuid, ev.accountId, ev.providerId || null, ev.calendarId || "primary", ev.title, ev.startMs, ev.endMs, ev.allDay ? 1 : 0, ev.location || "", ev.notes || "", ev.etag || null, ev.dirty ? 0 : Date.now(), ev.dirty ? 1 : 0, Date.now(), ev.recurringEventId || null, ev.htmlLink || null, ev.isHoliday ? 1 : 0, remindersJson);
|
|
754
765
|
return uuid;
|
|
755
766
|
}
|
|
756
767
|
getCalendarEvents(accountId, fromMs, toMs) {
|
|
@@ -778,7 +789,15 @@ export class MailxDB {
|
|
|
778
789
|
return rows.map(this.calendarRowToObject);
|
|
779
790
|
}
|
|
780
791
|
calendarRowToObject(r) {
|
|
792
|
+
let reminderMinutes = [];
|
|
793
|
+
try {
|
|
794
|
+
const parsed = JSON.parse(r.reminder_minutes_json || "[]");
|
|
795
|
+
if (Array.isArray(parsed))
|
|
796
|
+
reminderMinutes = parsed.filter((n) => Number.isFinite(n)).map((n) => Number(n));
|
|
797
|
+
}
|
|
798
|
+
catch { /* invalid JSON → empty */ }
|
|
781
799
|
return {
|
|
800
|
+
reminderMinutes,
|
|
782
801
|
uuid: r.uuid, accountId: r.account_id, providerId: r.provider_id,
|
|
783
802
|
calendarId: r.calendar_id, title: r.title, startMs: r.start_ms,
|
|
784
803
|
endMs: r.end_ms, allDay: !!r.all_day, location: r.location, notes: r.notes,
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.11",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.16"
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.16",
|
|
14
|
+
"mailparser": "^3.7.2"
|
|
14
15
|
},
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
@@ -21,12 +22,14 @@
|
|
|
21
22
|
},
|
|
22
23
|
".dependencies": {
|
|
23
24
|
"@bobfrankston/mailx-types": "file:../mailx-types",
|
|
24
|
-
"@bobfrankston/mailx-settings": "file:../mailx-settings"
|
|
25
|
+
"@bobfrankston/mailx-settings": "file:../mailx-settings",
|
|
26
|
+
"mailparser": "^3.7.2"
|
|
25
27
|
},
|
|
26
28
|
".transformedSnapshot": {
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"@bobfrankston/mailx-types": "^0.1.11",
|
|
29
|
-
"@bobfrankston/mailx-settings": "^0.1.16"
|
|
31
|
+
"@bobfrankston/mailx-settings": "^0.1.16",
|
|
32
|
+
"mailparser": "^3.7.2"
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide serialized simpleParser.
|
|
3
|
+
*
|
|
4
|
+
* mailparser's `simpleParser` is declared `async` but its work is CPU-bound
|
|
5
|
+
* (Node Streams + libmime decoding). When N parses run concurrently on the
|
|
6
|
+
* single-threaded event loop they share CPU and each finishes at roughly
|
|
7
|
+
* N× wall-clock. Real evidence (2026-05-13): four near-concurrent parses
|
|
8
|
+
* each reported `14691ms for 3 KB` — pure contention, not size.
|
|
9
|
+
*
|
|
10
|
+
* The downstream symptom is the IPC pipe: while the event loop is
|
|
11
|
+
* saturated by interleaving parses, unrelated IPC operations (mark-as-spam,
|
|
12
|
+
* move, delete) wait their turn and the WebView-side `mailxapi` shim
|
|
13
|
+
* times out at 120s with a misleading "stayed in the list" alert.
|
|
14
|
+
*
|
|
15
|
+
* Serializing through a module-level promise chain bounds the damage with
|
|
16
|
+
* minimal plumbing: a single parse runs at full CPU and finishes in its
|
|
17
|
+
* natural ~50-500ms budget; the next parse starts when it's done. Each
|
|
18
|
+
* UI click produces a clean preview latency instead of a 14-second stall.
|
|
19
|
+
*
|
|
20
|
+
* Lives in mailx-store rather than mailx-service so both the UI-hot path
|
|
21
|
+
* (mailx-service/local-store.ts) and the sync path (mailx-imap) can share
|
|
22
|
+
* one queue — otherwise sync-time parses would still contend with UI
|
|
23
|
+
* parses through the event loop, just not through this module's chain.
|
|
24
|
+
*
|
|
25
|
+
* Future work: replace the in-process chain with a `node:worker_threads`
|
|
26
|
+
* pool. The chain is the precursor that bounds the worst case while the
|
|
27
|
+
* pool is built.
|
|
28
|
+
*/
|
|
29
|
+
import { type ParsedMail, type Source } from "mailparser";
|
|
30
|
+
/** Serialized wrapper around `mailparser.simpleParser`. Drop-in replacement —
|
|
31
|
+
* same signature, same return value. */
|
|
32
|
+
export declare function parseSerial(source: Source): Promise<ParsedMail>;
|
|
33
|
+
//# sourceMappingURL=parse-serial.d.ts.map
|
package/parse-serial.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide serialized simpleParser.
|
|
3
|
+
*
|
|
4
|
+
* mailparser's `simpleParser` is declared `async` but its work is CPU-bound
|
|
5
|
+
* (Node Streams + libmime decoding). When N parses run concurrently on the
|
|
6
|
+
* single-threaded event loop they share CPU and each finishes at roughly
|
|
7
|
+
* N× wall-clock. Real evidence (2026-05-13): four near-concurrent parses
|
|
8
|
+
* each reported `14691ms for 3 KB` — pure contention, not size.
|
|
9
|
+
*
|
|
10
|
+
* The downstream symptom is the IPC pipe: while the event loop is
|
|
11
|
+
* saturated by interleaving parses, unrelated IPC operations (mark-as-spam,
|
|
12
|
+
* move, delete) wait their turn and the WebView-side `mailxapi` shim
|
|
13
|
+
* times out at 120s with a misleading "stayed in the list" alert.
|
|
14
|
+
*
|
|
15
|
+
* Serializing through a module-level promise chain bounds the damage with
|
|
16
|
+
* minimal plumbing: a single parse runs at full CPU and finishes in its
|
|
17
|
+
* natural ~50-500ms budget; the next parse starts when it's done. Each
|
|
18
|
+
* UI click produces a clean preview latency instead of a 14-second stall.
|
|
19
|
+
*
|
|
20
|
+
* Lives in mailx-store rather than mailx-service so both the UI-hot path
|
|
21
|
+
* (mailx-service/local-store.ts) and the sync path (mailx-imap) can share
|
|
22
|
+
* one queue — otherwise sync-time parses would still contend with UI
|
|
23
|
+
* parses through the event loop, just not through this module's chain.
|
|
24
|
+
*
|
|
25
|
+
* Future work: replace the in-process chain with a `node:worker_threads`
|
|
26
|
+
* pool. The chain is the precursor that bounds the worst case while the
|
|
27
|
+
* pool is built.
|
|
28
|
+
*/
|
|
29
|
+
import { simpleParser } from "mailparser";
|
|
30
|
+
let _chain = Promise.resolve();
|
|
31
|
+
/** Serialized wrapper around `mailparser.simpleParser`. Drop-in replacement —
|
|
32
|
+
* same signature, same return value. */
|
|
33
|
+
export async function parseSerial(source) {
|
|
34
|
+
const queued = _chain.then(() => simpleParser(source));
|
|
35
|
+
_chain = queued.catch(() => undefined);
|
|
36
|
+
return queued;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=parse-serial.js.map
|