@bobfrankston/rmfmail 1.1.24 → 1.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.
@@ -232,8 +232,8 @@ export class MailxService implements MailxApi {
232
232
  if (!raw) {
233
233
  // No file (yet). Reset in-memory denylist and seed discovered
234
234
  // from the local message corpus so autocomplete works immediately.
235
- this.db.applyContactsConfig({ preferred: [], denylist: [], discovered: [] });
236
- try { this.db.seedContactsFromMessages(); } catch { /* corpus may be empty */ }
235
+ await this.db.applyContactsConfig({ preferred: [], denylist: [], discovered: [] });
236
+ try { await this.db.seedContactsFromMessages(); } catch { /* corpus may be empty */ }
237
237
  // Auto-bootstrap GDrive copy if cloud is reachable. The file gets
238
238
  // a header comment so a user opening it on Drive sees what it is.
239
239
  if (cloudAvailable) {
@@ -252,29 +252,21 @@ export class MailxService implements MailxApi {
252
252
  const cfg = parseJsonc(raw, errors, { allowTrailingComma: true });
253
253
  if (errors.length) {
254
254
  console.error(` [contacts] contacts.jsonc has parse errors — applying empty config: ${errors.map((e: any) => e.error).join(", ")}`);
255
- this.db.applyContactsConfig({ preferred: [], denylist: [], discovered: [] });
255
+ await this.db.applyContactsConfig({ preferred: [], denylist: [], discovered: [] });
256
256
  return null;
257
257
  }
258
- const result = this.db.applyContactsConfig(cfg || {});
258
+ const result = await this.db.applyContactsConfig(cfg || {});
259
259
  // Seed-from-messages is a CORPUS WALK (every message's From/To/Cc
260
- // fields). On a 7K-message account it blocks the event loop for
261
- // ~20-30 s every other IPC (preview clicks, calendar pulls,
262
- // outbox status) queues behind it. We used to run it on every
263
- // loadContactsConfig, which meant every fs.watch firing of
264
- // contacts.jsonc (including remote-edit echoes) wedged the
265
- // daemon for half a minute. Now: only run on the FIRST load
266
- // after startup; subsequent reloads just sync the file → DB.
267
- // Individual sends keep the discovered set current via
260
+ // fields). It is now async + keyset-chunked (db.seedContactsFromMessages)
261
+ // so it yields the event loop between pages no longer the
262
+ // half-minute-to-minutes daemon wedge it once was. Still only run on
263
+ // the FIRST load after startup; subsequent reloads just sync the
264
+ // file → DB. Individual sends keep the discovered set current via
268
265
  // recordSentAddress() in the hot path.
269
266
  if (!this._contactsCorpusSeeded) {
270
267
  this._contactsCorpusSeeded = true;
271
- // Defer the corpus walk via setImmediate so the IPC that
272
- // triggered this load (configChanged event or first-boot
273
- // call) returns ack to the caller quickly. The walk still
274
- // blocks once it starts — chunking it is a follow-up.
275
- setImmediate(() => {
276
- try { this.db.seedContactsFromMessages(); } catch { /* corpus may be empty */ }
277
- });
268
+ this.db.seedContactsFromMessages()
269
+ .catch(() => { /* corpus may be empty */ });
278
270
  }
279
271
  return result;
280
272
  }
@@ -2507,8 +2499,8 @@ export class MailxService implements MailxApi {
2507
2499
  await this.imapManager.syncAllContacts();
2508
2500
  }
2509
2501
 
2510
- seedContacts(): number {
2511
- const added = this.db.seedContactsFromMessages();
2502
+ async seedContacts(): Promise<number> {
2503
+ const added = await this.db.seedContactsFromMessages();
2512
2504
  console.log(` Seeded ${added} contacts from message history`);
2513
2505
  return added;
2514
2506
  }
@@ -347,6 +347,14 @@ export declare class MailxDB {
347
347
  beginTransaction(): void;
348
348
  commitTransaction(): void;
349
349
  rollbackTransaction(): void;
350
+ /** Run `fn` inside a transaction — nesting-safe. `node:sqlite` throws on
351
+ * a nested BEGIN, and the async chunked walkers (seedContactsFromMessages,
352
+ * applyContactsConfig) can interleave with the sync backfill's own
353
+ * transactions across their `setImmediate` yields. If a transaction is
354
+ * already open this just runs `fn` (its writes join the open txn);
355
+ * otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
356
+ * can't span an await and leave a transaction open across a yield. */
357
+ runInTxn<T>(fn: () => T): T;
350
358
  /** Record an address used in sent mail */
351
359
  recordSentAddress(name: string, email: string): void;
352
360
  /** True if `email` (lowercased) appears in the active denylist. Cached
@@ -399,7 +407,16 @@ export declare class MailxDB {
399
407
  * Discovered is a single tier; sub-distinctions like sent-vs-received
400
408
  * collapse here because the user-facing UI shows them as one "discovered"
401
409
  * source. Recency-weighted use_count differentiates within the tier. */
402
- seedContactsFromMessages(): number;
410
+ /** ASYNC + chunked. This walks every cached message's address fields —
411
+ * on a large account that is hundreds of thousands of rows. The old
412
+ * synchronous version did `.all()` on the whole messages table in one
413
+ * call, materialising every row and blocking the event loop for tens
414
+ * of seconds to minutes (profiled 2026-05-15: 99% of daemon ticks were
415
+ * in native SQLite while this ran — every getMessage IPC, every preview
416
+ * click, queued behind it; that IS the "loading body takes forever"
417
+ * bug). Now keyset-paginated by `m.id` with a `setImmediate` yield
418
+ * between pages, so user IPC lands in the gaps. */
419
+ seedContactsFromMessages(): Promise<number>;
403
420
  /** Apply the contents of contacts.jsonc — replaces all preferred-tier rows
404
421
  * with the entries in `preferred[]`, merges `discovered[]` into the local
405
422
  * cache, sets the in-memory denylist, and purges any discovered rows
@@ -428,12 +445,12 @@ export declare class MailxDB {
428
445
  useCount?: number;
429
446
  lastUsed?: number;
430
447
  }[];
431
- }): {
448
+ }): Promise<{
432
449
  preferred: number;
433
450
  discovered: number;
434
451
  purged: number;
435
452
  conflicts: string[];
436
- };
453
+ }>;
437
454
  /** Build the contacts.jsonc shape from current DB state — for round-trip
438
455
  * to GDrive. Preferred-tier rows come from anything not in the reserved
439
456
  * system sources; discovered comes from `source='discovered'` rows;
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["db.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["db.ts"],"names":[],"mappings":"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"}
@@ -1901,6 +1901,30 @@ export class MailxDB {
1901
1901
  beginTransaction() { this.db.exec("BEGIN"); }
1902
1902
  commitTransaction() { this.db.exec("COMMIT"); }
1903
1903
  rollbackTransaction() { this.db.exec("ROLLBACK"); }
1904
+ /** Run `fn` inside a transaction — nesting-safe. `node:sqlite` throws on
1905
+ * a nested BEGIN, and the async chunked walkers (seedContactsFromMessages,
1906
+ * applyContactsConfig) can interleave with the sync backfill's own
1907
+ * transactions across their `setImmediate` yields. If a transaction is
1908
+ * already open this just runs `fn` (its writes join the open txn);
1909
+ * otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
1910
+ * can't span an await and leave a transaction open across a yield. */
1911
+ runInTxn(fn) {
1912
+ if (this.db.isTransaction)
1913
+ return fn();
1914
+ this.db.exec("BEGIN");
1915
+ try {
1916
+ const r = fn();
1917
+ this.db.exec("COMMIT");
1918
+ return r;
1919
+ }
1920
+ catch (e) {
1921
+ try {
1922
+ this.db.exec("ROLLBACK");
1923
+ }
1924
+ catch { /* already rolled back */ }
1925
+ throw e;
1926
+ }
1927
+ }
1904
1928
  // ── Contacts ──
1905
1929
  /** Record an address used in sent mail */
1906
1930
  recordSentAddress(name, email) {
@@ -1992,7 +2016,16 @@ export class MailxDB {
1992
2016
  * Discovered is a single tier; sub-distinctions like sent-vs-received
1993
2017
  * collapse here because the user-facing UI shows them as one "discovered"
1994
2018
  * source. Recency-weighted use_count differentiates within the tier. */
1995
- seedContactsFromMessages() {
2019
+ /** ASYNC + chunked. This walks every cached message's address fields —
2020
+ * on a large account that is hundreds of thousands of rows. The old
2021
+ * synchronous version did `.all()` on the whole messages table in one
2022
+ * call, materialising every row and blocking the event loop for tens
2023
+ * of seconds to minutes (profiled 2026-05-15: 99% of daemon ticks were
2024
+ * in native SQLite while this ran — every getMessage IPC, every preview
2025
+ * click, queued behind it; that IS the "loading body takes forever"
2026
+ * bug). Now keyset-paginated by `m.id` with a `setImmediate` yield
2027
+ * between pages, so user IPC lands in the gaps. */
2028
+ async seedContactsFromMessages() {
1996
2029
  const VALID = /^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/;
1997
2030
  const now = Date.now();
1998
2031
  const agg = new Map();
@@ -2016,57 +2049,75 @@ export class MailxDB {
2016
2049
  agg.set(email, { name: name || "", cnt: 1, last: date || 0 });
2017
2050
  }
2018
2051
  };
2019
- // Sent folder: recipients only (skip the user's own From address).
2020
- const sentRows = this.db.prepare(`SELECT m.to_json, m.cc_json, m.bcc_json, m.date
2021
- FROM messages m
2022
- JOIN folders f ON m.folder_id = f.id
2023
- WHERE f.special_use = 'sent'`).all();
2024
- for (const r of sentRows) {
2025
- const date = r.date || 0;
2026
- for (const field of [r.to_json, r.cc_json, r.bcc_json]) {
2027
- if (!field)
2028
- continue;
2029
- let parsed;
2030
- try {
2031
- parsed = JSON.parse(field);
2032
- }
2033
- catch {
2034
- continue;
2035
- }
2036
- if (!Array.isArray(parsed))
2052
+ const eatRecipients = (field, date) => {
2053
+ if (!field)
2054
+ return;
2055
+ let parsed;
2056
+ try {
2057
+ parsed = JSON.parse(field);
2058
+ }
2059
+ catch {
2060
+ return;
2061
+ }
2062
+ if (!Array.isArray(parsed))
2063
+ return;
2064
+ for (const a of parsed) {
2065
+ if (!a)
2037
2066
  continue;
2038
- for (const a of parsed) {
2039
- if (!a)
2040
- continue;
2041
- bump(a.name || "", a.address || a.email || "", date);
2067
+ bump(a.name || "", a.address || a.email || "", date);
2068
+ }
2069
+ };
2070
+ const yieldLoop = () => new Promise(r => setImmediate(r));
2071
+ const PAGE = 2000;
2072
+ // Sent folder: recipients only (skip the user's own From address).
2073
+ // Keyset pagination by m.id — each page is a bounded `.all()`, and
2074
+ // the event loop is handed back between pages.
2075
+ {
2076
+ const stmt = this.db.prepare(`SELECT m.id AS id, m.to_json, m.cc_json, m.bcc_json, m.date
2077
+ FROM messages m
2078
+ JOIN folders f ON m.folder_id = f.id
2079
+ WHERE f.special_use = 'sent' AND m.id > ?
2080
+ ORDER BY m.id LIMIT ?`);
2081
+ let lastId = 0;
2082
+ for (;;) {
2083
+ const rows = stmt.all(lastId, PAGE);
2084
+ if (rows.length === 0)
2085
+ break;
2086
+ for (const r of rows) {
2087
+ const date = r.date || 0;
2088
+ eatRecipients(r.to_json, date);
2089
+ eatRecipients(r.cc_json, date);
2090
+ eatRecipients(r.bcc_json, date);
2042
2091
  }
2092
+ lastId = rows[rows.length - 1].id;
2093
+ if (rows.length < PAGE)
2094
+ break;
2095
+ await yieldLoop();
2043
2096
  }
2044
2097
  }
2045
2098
  // Other folders: From + recipients.
2046
- const recvRows = this.db.prepare(`SELECT m.from_name, m.from_address, m.to_json, m.cc_json, m.bcc_json, m.date
2047
- FROM messages m
2048
- LEFT JOIN folders f ON m.folder_id = f.id
2049
- WHERE f.special_use IS NULL OR f.special_use != 'sent'`).all();
2050
- for (const r of recvRows) {
2051
- const date = r.date || 0;
2052
- bump(r.from_name, r.from_address, date);
2053
- for (const field of [r.to_json, r.cc_json, r.bcc_json]) {
2054
- if (!field)
2055
- continue;
2056
- let parsed;
2057
- try {
2058
- parsed = JSON.parse(field);
2059
- }
2060
- catch {
2061
- continue;
2062
- }
2063
- if (!Array.isArray(parsed))
2064
- continue;
2065
- for (const a of parsed) {
2066
- if (!a)
2067
- continue;
2068
- bump(a.name || "", a.address || a.email || "", date);
2099
+ {
2100
+ const stmt = this.db.prepare(`SELECT m.id AS id, m.from_name, m.from_address, m.to_json, m.cc_json, m.bcc_json, m.date
2101
+ FROM messages m
2102
+ LEFT JOIN folders f ON m.folder_id = f.id
2103
+ WHERE (f.special_use IS NULL OR f.special_use != 'sent') AND m.id > ?
2104
+ ORDER BY m.id LIMIT ?`);
2105
+ let lastId = 0;
2106
+ for (;;) {
2107
+ const rows = stmt.all(lastId, PAGE);
2108
+ if (rows.length === 0)
2109
+ break;
2110
+ for (const r of rows) {
2111
+ const date = r.date || 0;
2112
+ bump(r.from_name, r.from_address, date);
2113
+ eatRecipients(r.to_json, date);
2114
+ eatRecipients(r.cc_json, date);
2115
+ eatRecipients(r.bcc_json, date);
2069
2116
  }
2117
+ lastId = rows[rows.length - 1].id;
2118
+ if (rows.length < PAGE)
2119
+ break;
2120
+ await yieldLoop();
2070
2121
  }
2071
2122
  }
2072
2123
  let added = 0;
@@ -2077,16 +2128,30 @@ export class MailxDB {
2077
2128
  name = CASE WHEN name = '' AND ? != '' THEN ? ELSE name END,
2078
2129
  updated_at = ?
2079
2130
  WHERE id = ?`);
2080
- for (const [email, info] of agg) {
2081
- const existing = this.db.prepare("SELECT id FROM contacts WHERE source = 'discovered' AND lower(email) = ?").get(email);
2082
- if (!existing) {
2083
- insStmt.run(info.name, email, info.last, info.cnt, now);
2084
- added++;
2085
- }
2086
- else {
2087
- updStmt.run(info.cnt, info.last, info.name, info.name, now, existing.id);
2088
- bumped++;
2089
- }
2131
+ const findStmt = this.db.prepare("SELECT id FROM contacts WHERE source = 'discovered' AND lower(email) = ?");
2132
+ // Write phase — chunked. Each chunk runs via `db.transaction()`
2133
+ // (one synchronous turn, one commit) rather than raw BEGIN/COMMIT
2134
+ // straddling an `await` — the wrapper is nesting-safe (savepoints)
2135
+ // so it can't collide with the sync backfill's own transactions
2136
+ // when the two interleave across the yield below.
2137
+ const WRITE_CHUNK = 500;
2138
+ const entries = [...agg.entries()];
2139
+ for (let i = 0; i < entries.length; i += WRITE_CHUNK) {
2140
+ const slice = entries.slice(i, i + WRITE_CHUNK);
2141
+ this.runInTxn(() => {
2142
+ for (const [email, info] of slice) {
2143
+ const existing = findStmt.get(email);
2144
+ if (!existing) {
2145
+ insStmt.run(info.name, email, info.last, info.cnt, now);
2146
+ added++;
2147
+ }
2148
+ else {
2149
+ updStmt.run(info.cnt, info.last, info.name, info.name, now, existing.id);
2150
+ bumped++;
2151
+ }
2152
+ }
2153
+ });
2154
+ await yieldLoop();
2090
2155
  }
2091
2156
  if (added > 0 || bumped > 0) {
2092
2157
  console.log(` [contacts] seed: ${added} new + ${bumped} refreshed (discovered)`);
@@ -2104,7 +2169,7 @@ export class MailxDB {
2104
2169
  * Discovered rows from the file are MERGED with whatever the local
2105
2170
  * message-corpus seeder has produced. Each device contributes its
2106
2171
  * observed addresses; over time GDrive accumulates the union. */
2107
- applyContactsConfig(cfg) {
2172
+ async applyContactsConfig(cfg) {
2108
2173
  const preferred = Array.isArray(cfg.preferred) ? cfg.preferred : [];
2109
2174
  const denylist = Array.isArray(cfg.denylist) ? cfg.denylist : [];
2110
2175
  const denylistPatterns = Array.isArray(cfg.denylistPatterns) ? cfg.denylistPatterns : [];
@@ -2119,41 +2184,65 @@ export class MailxDB {
2119
2184
  .filter(e => e && e.priority === true && e.email)
2120
2185
  .map(e => e.email);
2121
2186
  this.setPriorityIndex(prioritySenders, priorityDomains);
2187
+ const VALID = /^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/;
2188
+ const now = Date.now();
2189
+ const denySet = new Set(denylist.map(e => (e || "").trim().toLowerCase()).filter(Boolean));
2190
+ const conflicts = [];
2191
+ const yieldLoop = () => new Promise(r => setImmediate(r));
2192
+ const CHUNK = 500;
2122
2193
  // Wipe and rewrite preferred-tier rows owned by contacts.jsonc.
2123
2194
  // The address-book UI's legacy `upsertContact` still writes
2124
2195
  // source='manual' rows; those are owned by the address-book code
2125
2196
  // path, not contacts.jsonc, so we leave them alone here.
2126
- this.db.exec("DELETE FROM contacts WHERE source NOT IN ('google', 'discovered', 'manual')");
2127
- const VALID = /^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/;
2128
- const now = Date.now();
2197
+ // ALL writes below run inside transactions and yield the event
2198
+ // loop between chunks. The prior version did one autocommitting
2199
+ // INSERT/UPDATE per row with NO transaction — on a contacts.jsonc
2200
+ // that has accumulated thousands of `discovered` entries across
2201
+ // devices, that was thousands of fsyncs back-to-back, blocking the
2202
+ // daemon for 25-30 s (profiled 2026-05-15: applyContactsConfig was
2203
+ // ~97% of the wedge). It also re-`prepare()`d a `lower(email)`
2204
+ // full-scan SELECT every iteration. Now: one statement prep, one
2205
+ // up-front map of existing discovered rows (kills the N×N scan),
2206
+ // chunked transactions, yields between chunks.
2129
2207
  const ins = this.db.prepare(`INSERT OR IGNORE INTO contacts (source, name, email, organization, last_used, use_count, updated_at)
2130
2208
  VALUES (?, ?, ?, ?, 0, 0, ?)`);
2131
- const denySet = new Set(denylist.map(e => (e || "").trim().toLowerCase()).filter(Boolean));
2132
- const conflicts = [];
2133
2209
  let inserted = 0;
2134
- for (const entry of preferred) {
2135
- if (!entry)
2136
- continue;
2137
- const email = (entry.email || "").trim();
2138
- if (!email || !VALID.test(email))
2139
- continue;
2140
- if (denySet.has(email.toLowerCase())) {
2141
- conflicts.push(email);
2142
- continue;
2143
- }
2144
- const source = (entry.source || "preferred").trim() || "preferred";
2145
- const name = (entry.name || "").trim();
2146
- const org = (entry.organization || entry.org || "").trim();
2147
- try {
2148
- const r = ins.run(source, name, email, org, now);
2149
- if (r.changes)
2150
- inserted++;
2210
+ // runInTxn: nesting-safe and atomic within one synchronous turn, so
2211
+ // these writes can't collide with the sync backfill's transactions
2212
+ // when the two interleave across the yields below.
2213
+ this.runInTxn(() => {
2214
+ this.db.exec("DELETE FROM contacts WHERE source NOT IN ('google', 'discovered', 'manual')");
2215
+ for (const entry of preferred) {
2216
+ if (!entry)
2217
+ continue;
2218
+ const email = (entry.email || "").trim();
2219
+ if (!email || !VALID.test(email))
2220
+ continue;
2221
+ if (denySet.has(email.toLowerCase())) {
2222
+ conflicts.push(email);
2223
+ continue;
2224
+ }
2225
+ const source = (entry.source || "preferred").trim() || "preferred";
2226
+ const name = (entry.name || "").trim();
2227
+ const org = (entry.organization || entry.org || "").trim();
2228
+ try {
2229
+ const r = ins.run(source, name, email, org, now);
2230
+ if (r.changes)
2231
+ inserted++;
2232
+ }
2233
+ catch { /* dup row, skip */ }
2151
2234
  }
2152
- catch { /* dup row, skip */ }
2153
- }
2235
+ });
2236
+ await yieldLoop();
2154
2237
  // Merge discovered[] from cloud into local cache. For each entry:
2155
2238
  // existing row wins on use_count (max), name fills if empty, lastUsed
2156
2239
  // is max. Missing rows are inserted. Denylisted entries skipped.
2240
+ // Existing discovered rows are mapped ONCE up front so the merge is
2241
+ // O(N) hash lookups, not O(N) `lower(email)` table scans.
2242
+ const existingDiscovered = new Map();
2243
+ for (const row of this.db.prepare("SELECT id, lower(email) AS le FROM contacts WHERE source = 'discovered'").all()) {
2244
+ existingDiscovered.set(row.le, row.id);
2245
+ }
2157
2246
  const insDiscovered = this.db.prepare("INSERT INTO contacts (source, name, email, last_used, use_count, updated_at) VALUES ('discovered', ?, ?, ?, ?, ?)");
2158
2247
  const updDiscovered = this.db.prepare(`UPDATE contacts SET use_count = max(use_count, ?),
2159
2248
  last_used = max(last_used, ?),
@@ -2161,36 +2250,44 @@ export class MailxDB {
2161
2250
  updated_at = ?
2162
2251
  WHERE id = ?`);
2163
2252
  let discoveredAdded = 0;
2164
- for (const entry of discovered) {
2165
- if (!entry)
2166
- continue;
2167
- const email = (entry.email || "").trim();
2168
- if (!email || !VALID.test(email))
2169
- continue;
2170
- const lower = email.toLowerCase();
2171
- if (denySet.has(lower))
2172
- continue;
2173
- if (isJunkContact(lower, entry.name || ""))
2174
- continue;
2175
- const name = (entry.name || "").trim();
2176
- const useCount = Math.max(0, entry.useCount || 0);
2177
- const lastUsed = Math.max(0, entry.lastUsed || 0);
2178
- const existing = this.db.prepare("SELECT id FROM contacts WHERE source = 'discovered' AND lower(email) = ?").get(lower);
2179
- if (!existing) {
2180
- insDiscovered.run(name, email, lastUsed, useCount, now);
2181
- discoveredAdded++;
2182
- }
2183
- else {
2184
- updDiscovered.run(useCount, lastUsed, name, name, now, existing.id);
2185
- }
2253
+ for (let i = 0; i < discovered.length; i += CHUNK) {
2254
+ const slice = discovered.slice(i, i + CHUNK);
2255
+ this.runInTxn(() => {
2256
+ for (const entry of slice) {
2257
+ if (!entry)
2258
+ continue;
2259
+ const email = (entry.email || "").trim();
2260
+ if (!email || !VALID.test(email))
2261
+ continue;
2262
+ const lower = email.toLowerCase();
2263
+ if (denySet.has(lower))
2264
+ continue;
2265
+ if (isJunkContact(lower, entry.name || ""))
2266
+ continue;
2267
+ const name = (entry.name || "").trim();
2268
+ const useCount = Math.max(0, entry.useCount || 0);
2269
+ const lastUsed = Math.max(0, entry.lastUsed || 0);
2270
+ const existingId = existingDiscovered.get(lower);
2271
+ if (existingId === undefined) {
2272
+ insDiscovered.run(name, email, lastUsed, useCount, now);
2273
+ discoveredAdded++;
2274
+ }
2275
+ else {
2276
+ updDiscovered.run(useCount, lastUsed, name, name, now, existingId);
2277
+ }
2278
+ }
2279
+ });
2280
+ await yieldLoop();
2186
2281
  }
2187
2282
  // Purge discovered rows for any denylisted email.
2188
2283
  const purge = this.db.prepare("DELETE FROM contacts WHERE source = 'discovered' AND lower(email) = ?");
2189
2284
  let purged = 0;
2190
- for (const e of denySet) {
2191
- const r = purge.run(e);
2192
- purged += Number(r.changes || 0);
2193
- }
2285
+ this.runInTxn(() => {
2286
+ for (const e of denySet) {
2287
+ const r = purge.run(e);
2288
+ purged += Number(r.changes || 0);
2289
+ }
2290
+ });
2194
2291
  if (conflicts.length > 0) {
2195
2292
  console.warn(` [contacts] config: ${conflicts.length} preferred entries also appear in denylist — denylist wins, entries skipped: ${conflicts.join(", ")}`);
2196
2293
  }