@bobfrankston/mailx-store 0.1.9 → 0.1.11
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.js +47 -6
- package/file-store.d.ts +6 -0
- package/file-store.js +8 -0
- package/package.json +3 -3
package/db.js
CHANGED
|
@@ -334,6 +334,25 @@ export class MailxDB {
|
|
|
334
334
|
// this column landed. One UPDATE + an id roundtrip per row — cheap
|
|
335
335
|
// at our row counts, runs once per DB upgrade.
|
|
336
336
|
this.backfillUuids();
|
|
337
|
+
// One-shot cleanup: the retired insertOptimisticSentRow path wrote
|
|
338
|
+
// synthetic-negative-UID rows into Sent. Those rows are stale (the
|
|
339
|
+
// real server-synced row eventually appears with a positive UID),
|
|
340
|
+
// they pollute MAX(uid) (which broke Sent sync entirely), and the
|
|
341
|
+
// mechanism is gone. Drop them. Bodies on disk are orphaned; the
|
|
342
|
+
// body-store has no GC today but a one-time leak is fine.
|
|
343
|
+
try {
|
|
344
|
+
const purgedFlag = this.getKv("schema", "drop_synthetic_uids_v1");
|
|
345
|
+
if (!purgedFlag) {
|
|
346
|
+
const r = this.db.prepare("DELETE FROM messages WHERE uid < 0").run();
|
|
347
|
+
if (r.changes) {
|
|
348
|
+
console.log(` [migration] dropped ${r.changes} synthetic-UID rows from messages`);
|
|
349
|
+
}
|
|
350
|
+
this.setKv("schema", "drop_synthetic_uids_v1", "1");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
console.error(` [migration] synthetic-UID cleanup failed: ${e?.message || e}`);
|
|
355
|
+
}
|
|
337
356
|
// One-shot contacts table reset: the contacts schema's UNIQUE constraint
|
|
338
357
|
// was widened from `(email)` to `(source, email, name)` so the same
|
|
339
358
|
// address can carry multiple distinct (name, source) entries — Bob's
|
|
@@ -1029,15 +1048,14 @@ export class MailxDB {
|
|
|
1029
1048
|
// LEFT JOIN sync_actions so each row carries a `pending` flag —
|
|
1030
1049
|
// true when the user has a queued local action (move/flag/delete)
|
|
1031
1050
|
// not yet acknowledged by the server. UI renders these in pink so
|
|
1032
|
-
// local-only state is visible (Slice C of S1).
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
// before the real APPENDUID comes back from the server).
|
|
1051
|
+
// local-only state is visible (Slice C of S1). The optimistic-Sent
|
|
1052
|
+
// negative-UID convention was retired — Sent now reflects only what
|
|
1053
|
+
// the server has; pending sends live in the Outbox view.
|
|
1036
1054
|
const rows = this.db.prepare(`SELECT m.*, (
|
|
1037
1055
|
EXISTS(
|
|
1038
1056
|
SELECT 1 FROM sync_actions sa
|
|
1039
1057
|
WHERE sa.account_id = m.account_id AND sa.uid = m.uid
|
|
1040
|
-
)
|
|
1058
|
+
)
|
|
1041
1059
|
) AS pending
|
|
1042
1060
|
FROM messages m WHERE ${where.replace(/\b(account_id|folder_id|uid|date|subject|from_name|from_address|flags_json)\b/g, "m.$1")}
|
|
1043
1061
|
ORDER BY m.${sortCol} ${sortDir} LIMIT ? OFFSET ?`).all(...params, pageSize, offset);
|
|
@@ -1188,7 +1206,13 @@ export class MailxDB {
|
|
|
1188
1206
|
}
|
|
1189
1207
|
/** Get messages without cached bodies (for background prefetch) */
|
|
1190
1208
|
getMessagesWithoutBody(accountId, limit = 50) {
|
|
1191
|
-
|
|
1209
|
+
// Prefetch order: smallest first, NULLs last, recent within tiebreak.
|
|
1210
|
+
// Reasoning: on slow / metered Android networks, the user feels the
|
|
1211
|
+
// cache "fill up" much faster when the queue is dominated by short
|
|
1212
|
+
// notification mails (a handful of KB each) instead of a single
|
|
1213
|
+
// multi-megabyte attachment that monopolizes bandwidth for minutes.
|
|
1214
|
+
// Size 0 / NULL fall to the end so they don't masquerade as small.
|
|
1215
|
+
return this.db.prepare("SELECT uid, folder_id as folderId FROM messages WHERE account_id = ? AND (body_path IS NULL OR body_path = '') ORDER BY (size IS NULL OR size = 0), size ASC, date DESC LIMIT ?").all(accountId, limit);
|
|
1192
1216
|
}
|
|
1193
1217
|
getHighestUid(accountId, folderId) {
|
|
1194
1218
|
const r = this.db.prepare("SELECT MAX(uid) as maxUid FROM messages WHERE account_id = ? AND folder_id = ?").get(accountId, folderId);
|
|
@@ -1613,6 +1637,23 @@ export class MailxDB {
|
|
|
1613
1637
|
const score = (r) => (r.match_rank || 0) * 10_000
|
|
1614
1638
|
+ (r.use_count || 0) * Math.pow(0.5, Math.max(0, now - (r.last_used || 0)) / HALF_LIFE_MS);
|
|
1615
1639
|
rows.sort((a, b) => score(b) - score(a));
|
|
1640
|
+
// Dedup by lowercased email — same address often appears as both
|
|
1641
|
+
// source='google' (synced from Google Contacts) and source='discovered'
|
|
1642
|
+
// (auto-collected from sent mail). The user only wants to see the
|
|
1643
|
+
// best entry; we keep the higher-ranked source (which the sort above
|
|
1644
|
+
// has already put first), and silently drop the duplicate. Without
|
|
1645
|
+
// this, the autocomplete dropdown showed two identical Kevin Healy
|
|
1646
|
+
// rows just labeled GOOGLE and DISCOVERED.
|
|
1647
|
+
const seenEmails = new Set();
|
|
1648
|
+
rows = rows.filter(r => {
|
|
1649
|
+
const k = (r.email || "").toLowerCase();
|
|
1650
|
+
if (!k)
|
|
1651
|
+
return true;
|
|
1652
|
+
if (seenEmails.has(k))
|
|
1653
|
+
return false;
|
|
1654
|
+
seenEmails.add(k);
|
|
1655
|
+
return true;
|
|
1656
|
+
});
|
|
1616
1657
|
rows = rows.slice(0, limit);
|
|
1617
1658
|
return rows.map(r => ({ name: r.name, email: r.email, source: r.source, useCount: r.use_count }));
|
|
1618
1659
|
}
|
package/file-store.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export declare class FileMessageStore implements MessageStore {
|
|
|
28
28
|
* it in `body_path`. The (folderId, uid) args are kept for interface
|
|
29
29
|
* compatibility; they do NOT affect the filename. */
|
|
30
30
|
putMessage(accountId: string, _folderId: number, _uid: number, raw: Buffer): Promise<string>;
|
|
31
|
+
/** Resolve a stored body_path (relative or absolute) to an absolute
|
|
32
|
+
* filesystem path. Returns "" if the input doesn't resolve to a file
|
|
33
|
+
* inside the store. UI / "Source" actions need the absolute path so
|
|
34
|
+
* they can open / pass it to OS file pickers without re-introducing
|
|
35
|
+
* the basePath context every time. */
|
|
36
|
+
absolutePath(stored: string): string;
|
|
31
37
|
/** Read by stored path (relative or absolute). */
|
|
32
38
|
readByPath(stored: string): Promise<Buffer>;
|
|
33
39
|
hasByPath(stored: string): Promise<boolean>;
|
package/file-store.js
CHANGED
|
@@ -51,6 +51,14 @@ export class FileMessageStore {
|
|
|
51
51
|
fs.writeFileSync(abs, raw);
|
|
52
52
|
return rel;
|
|
53
53
|
}
|
|
54
|
+
/** Resolve a stored body_path (relative or absolute) to an absolute
|
|
55
|
+
* filesystem path. Returns "" if the input doesn't resolve to a file
|
|
56
|
+
* inside the store. UI / "Source" actions need the absolute path so
|
|
57
|
+
* they can open / pass it to OS file pickers without re-introducing
|
|
58
|
+
* the basePath context every time. */
|
|
59
|
+
absolutePath(stored) {
|
|
60
|
+
return this.resolveStored(stored);
|
|
61
|
+
}
|
|
54
62
|
/** Read by stored path (relative or absolute). */
|
|
55
63
|
async readByPath(stored) {
|
|
56
64
|
const abs = this.resolveStored(stored);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.13"
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
".transformedSnapshot": {
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
29
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
29
|
+
"@bobfrankston/mailx-settings": "^0.1.13"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|