@delali/sirannon-db 0.1.6 → 0.1.8

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/README.md +154 -19
  2. package/dist/backup-scheduler/index.d.ts +18 -3
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
  5. package/dist/chunk-4ISB7XMA.mjs +21 -0
  6. package/dist/chunk-BNUTBHHH.mjs +22 -0
  7. package/dist/chunk-CJLYFDP5.mjs +26 -0
  8. package/dist/chunk-CW6S3WL5.mjs +222 -0
  9. package/dist/chunk-DVWQD3GF.mjs +49 -0
  10. package/dist/chunk-GEZUUIKV.mjs +268 -0
  11. package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
  12. package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
  13. package/dist/chunk-TKGHYWQ6.mjs +35 -0
  14. package/dist/chunk-UXLAO6ZH.mjs +352 -0
  15. package/dist/chunk-VLTICJOD.mjs +470 -0
  16. package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
  17. package/dist/client/index.d.ts +107 -15
  18. package/dist/client/index.mjs +348 -37
  19. package/dist/core/index.d.ts +70 -13
  20. package/dist/core/index.mjs +635 -192
  21. package/dist/core/writer-worker.d.ts +2 -0
  22. package/dist/core/writer-worker.mjs +129 -0
  23. package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
  24. package/dist/driver/better-sqlite3.d.ts +1 -1
  25. package/dist/driver/better-sqlite3.mjs +44 -6
  26. package/dist/driver/bun.mjs +35 -6
  27. package/dist/driver/expo.mjs +2 -2
  28. package/dist/driver/node.d.ts +1 -1
  29. package/dist/driver/node.mjs +48 -5
  30. package/dist/driver/wa-sqlite.d.ts +109 -0
  31. package/dist/driver/wa-sqlite.mjs +30 -2
  32. package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
  33. package/dist/file-migrations/index.d.ts +2 -3
  34. package/dist/file-migrations/index.mjs +1 -1
  35. package/dist/replication/coordinator/etcd.mjs +2 -2
  36. package/dist/replication/index.d.ts +7 -8
  37. package/dist/replication/index.mjs +79 -78
  38. package/dist/server/index.d.ts +111 -28
  39. package/dist/server/index.mjs +1016 -384
  40. package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
  41. package/dist/transport/grpc.d.ts +3 -4
  42. package/dist/transport/grpc.mjs +2 -2
  43. package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
  44. package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
  45. package/dist/types-D_hQW1hr.d.ts +494 -0
  46. package/package.json +3 -23
  47. package/dist/chunk-3MCMONVP.mjs +0 -115
  48. package/dist/chunk-74UN4DIE.mjs +0 -14
  49. package/dist/chunk-FB2U2Q3Y.mjs +0 -21
  50. package/dist/chunk-GS7T5YMI.mjs +0 -51
  51. package/dist/chunk-PXKAKK2V.mjs +0 -124
  52. package/dist/index-CLdNrcPz.d.ts +0 -16
  53. package/dist/types-BFSsG77t.d.ts +0 -29
  54. package/dist/types-D-74JiXb.d.ts +0 -265
@@ -1,11 +1,67 @@
1
- import { decodeTaggedValues, SAFE_INT_BOUND_TEXT } from './chunk-GS7T5YMI.mjs';
2
- import { CDCError } from './chunk-O7BHI3CF.mjs';
1
+ import { synchronousPragmaValue } from './chunk-CJLYFDP5.mjs';
2
+ import { CHANGES_TABLE, isReservedIdentifier, decodeTaggedValues, CDC_TRIGGER_PREFIX, SAFE_INT_BOUND_TEXT } from './chunk-GEZUUIKV.mjs';
3
+ import { SirannonError, ForbiddenSqlError, CDCError } from './chunk-YPYVQJ4C.mjs';
4
+
5
+ // src/core/bulk-load.ts
6
+ var DEFAULT_LOAD_DURABILITY = "off";
7
+ var CHECKPOINT_ATTEMPTS = 3;
8
+ var CHECKPOINT_RETRY_DELAY_MS = 50;
9
+ function isBulkLoadDurability(value) {
10
+ return value === "off" || value === "normal";
11
+ }
12
+ async function runBulkLoad(run) {
13
+ const durability = run.durability ?? DEFAULT_LOAD_DURABILITY;
14
+ if (!isBulkLoadDurability(durability)) {
15
+ throw new SirannonError("Bulk load durability must be 'off' or 'normal'", "INVALID_DURABILITY");
16
+ }
17
+ const restorePragma = `PRAGMA synchronous = ${synchronousPragmaValue(run.configuredSynchronous)}`;
18
+ await run.writer.exec(`PRAGMA synchronous = ${synchronousPragmaValue(durability)}`);
19
+ let result;
20
+ let loadError;
21
+ try {
22
+ result = await run.loadRows();
23
+ } catch (err) {
24
+ loadError = err;
25
+ }
26
+ try {
27
+ await run.writer.exec(restorePragma);
28
+ } catch {
29
+ if (loadError !== void 0) throw loadError;
30
+ throw new SirannonError(
31
+ "Bulk load committed but durability could not be restored because the writer connection failed",
32
+ "DURABILITY_RESTORE_FAILED"
33
+ );
34
+ }
35
+ if (loadError !== void 0) {
36
+ throw loadError;
37
+ }
38
+ if (result === void 0) {
39
+ throw new SirannonError("Bulk load completed without a result", "INTERNAL_ERROR");
40
+ }
41
+ if ((run.checkpoint ?? true) && run.walMode && result.changes > 0) {
42
+ await checkpoint(run.writer);
43
+ }
44
+ return result;
45
+ }
46
+ async function checkpoint(writer) {
47
+ for (let attempt = 0; attempt < CHECKPOINT_ATTEMPTS; attempt++) {
48
+ if (attempt > 0) {
49
+ await delay(attempt * CHECKPOINT_RETRY_DELAY_MS);
50
+ }
51
+ const stmt = await writer.prepare("PRAGMA wal_checkpoint(TRUNCATE)");
52
+ const row = await stmt.get();
53
+ if (!row || row.busy === 0) return;
54
+ }
55
+ }
56
+ function delay(ms) {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
3
59
 
4
60
  // src/core/cdc/trigger-sql.ts
5
61
  async function dropCdcTriggers(conn, table) {
6
- await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_insert"`);
7
- await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_update"`);
8
- await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_delete"`);
62
+ await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_insert"`);
63
+ await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_update"`);
64
+ await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_delete"`);
9
65
  }
10
66
  async function installCdcTriggers(conn, changesTable, table, columns, pkColumns, replication) {
11
67
  const newJson = buildJsonObject(columns, "NEW");
@@ -15,7 +71,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
15
71
  const replCols = replication ? ", node_id, tx_id, hlc" : "";
16
72
  const replVals = replication ? ", '', '', ''" : "";
17
73
  await conn.exec(`
18
- CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_insert"
74
+ CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_insert"
19
75
  AFTER INSERT ON "${table}"
20
76
  BEGIN
21
77
  INSERT INTO "${changesTable}" (table_name, operation, row_id, new_data${replCols})
@@ -23,7 +79,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
23
79
  END
24
80
  `);
25
81
  await conn.exec(`
26
- CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_update"
82
+ CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_update"
27
83
  AFTER UPDATE ON "${table}"
28
84
  BEGIN
29
85
  INSERT INTO "${changesTable}" (table_name, operation, row_id, old_data, new_data${replCols})
@@ -31,7 +87,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
31
87
  END
32
88
  `);
33
89
  await conn.exec(`
34
- CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_delete"
90
+ CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_delete"
35
91
  AFTER DELETE ON "${table}"
36
92
  BEGIN
37
93
  INSERT INTO "${changesTable}" (table_name, operation, row_id, old_data${replCols})
@@ -64,7 +120,6 @@ function escStr(name) {
64
120
 
65
121
  // src/core/cdc/change-tracker.ts
66
122
  var DEFAULT_RETENTION_MS = 36e5;
67
- var DEFAULT_CHANGES_TABLE = "_sirannon_changes";
68
123
  var DEFAULT_POLL_BATCH_SIZE = 1e3;
69
124
  var IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
70
125
  var ChangeTracker = class {
@@ -80,13 +135,16 @@ var ChangeTracker = class {
80
135
  pruneBoundary = null;
81
136
  constructor(options) {
82
137
  this.retentionMs = options?.retention ?? DEFAULT_RETENTION_MS;
83
- this.changesTable = options?.changesTable ?? DEFAULT_CHANGES_TABLE;
138
+ this.changesTable = options?.changesTable ?? CHANGES_TABLE;
84
139
  this.pollBatchSize = options?.pollBatchSize ?? DEFAULT_POLL_BATCH_SIZE;
85
140
  this.replication = options?.replication ?? false;
86
141
  this.assertIdentifier(this.changesTable, "changes table name");
87
142
  }
88
143
  async watch(conn, table) {
89
144
  this.assertIdentifier(table, "table name");
145
+ if (isReservedIdentifier(table)) {
146
+ throw new ForbiddenSqlError(`Table '${table}' is reserved for Sirannon and cannot be watched`);
147
+ }
90
148
  await this.ensureChangesTable(conn);
91
149
  const columns = await this.getColumns(conn, table);
92
150
  if (columns.length === 0) {
@@ -226,20 +284,64 @@ var ChangeTracker = class {
226
284
  if (rows.length === 0) {
227
285
  return [];
228
286
  }
229
- const events = [];
230
- for (const row of rows) {
231
- events.push({
232
- type: row.operation.toLowerCase(),
233
- table: row.table_name,
234
- row: row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : {},
235
- oldRow: row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : void 0,
236
- seq: BigInt(row.seq),
237
- timestamp: row.changed_at
238
- });
239
- }
287
+ const events = rows.map((row) => this.rowToEvent(row));
240
288
  this.lastSeq = BigInt(rows[rows.length - 1].seq);
241
289
  return events;
242
290
  }
291
+ /** The highest seq already polled; live subscribers receive events beyond it. */
292
+ get cursor() {
293
+ return this.lastSeq;
294
+ }
295
+ /**
296
+ * Reads retained changes for one table with seq in `(afterSeq, upToSeq]`,
297
+ * ordered ascending and capped at `limit`. Used to replay history to a
298
+ * resuming subscriber without disturbing the shared poll cursor.
299
+ */
300
+ async readSince(conn, table, afterSeq, upToSeq, limit) {
301
+ if (!this.changesTableReady) {
302
+ await this.detectChangesTable(conn);
303
+ if (!this.changesTableReady) {
304
+ return [];
305
+ }
306
+ }
307
+ const stmt = await this.getStmt(
308
+ conn,
309
+ "read_since",
310
+ `SELECT seq, table_name, operation, row_id, changed_at, old_data, new_data
311
+ FROM "${this.changesTable}"
312
+ WHERE table_name = ? AND seq > ? AND seq <= ?
313
+ ORDER BY seq ASC
314
+ LIMIT ?`
315
+ );
316
+ const rows = await stmt.all(table, afterSeq.toString(), upToSeq.toString(), limit);
317
+ return rows.map((row) => this.rowToEvent(row));
318
+ }
319
+ /** The lowest retained seq, or `null` when the change log is empty. */
320
+ async getMinSeq(conn) {
321
+ if (!this.changesTableReady) {
322
+ await this.detectChangesTable(conn);
323
+ if (!this.changesTableReady) {
324
+ return null;
325
+ }
326
+ }
327
+ const stmt = await this.getStmt(conn, "min_seq", `SELECT MIN(seq) AS seq FROM "${this.changesTable}"`);
328
+ const row = await stmt.get();
329
+ const seq = row?.seq;
330
+ if (seq === void 0 || seq === null) {
331
+ return null;
332
+ }
333
+ return typeof seq === "bigint" ? seq : BigInt(String(seq));
334
+ }
335
+ rowToEvent(row) {
336
+ return {
337
+ type: row.operation.toLowerCase(),
338
+ table: row.table_name,
339
+ row: row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : {},
340
+ oldRow: row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : void 0,
341
+ seq: BigInt(row.seq),
342
+ timestamp: row.changed_at
343
+ };
344
+ }
243
345
  async advanceToLatest(conn) {
244
346
  if (!this.changesTableReady) {
245
347
  await this.detectChangesTable(conn);
@@ -430,7 +532,7 @@ var SubscriptionManager = class {
430
532
  for (const id of ids) {
431
533
  const sub = this.subscriptions.get(id);
432
534
  if (!sub) continue;
433
- if (sub.filter && !matchesFilter(event, sub.filter)) {
535
+ if (sub.filter && !changeMatchesFilter(event, sub.filter)) {
434
536
  continue;
435
537
  }
436
538
  try {
@@ -461,18 +563,19 @@ var SubscriptionBuilderImpl = class {
461
563
  return this.manager.subscribe(this.table, this.conditions, callback);
462
564
  }
463
565
  };
464
- function startPolling(conn, tracker, manager, intervalMs, onError) {
566
+ function startPolling(conn, tracker, manager, intervalMs, onError, runExclusive) {
465
567
  let consecutiveErrors = 0;
466
568
  let tickCount = 0;
467
569
  let polling = false;
468
570
  const MAX_CONSECUTIVE_ERRORS = 10;
469
571
  const CLEANUP_INTERVAL_TICKS = 100;
572
+ const exclusive = runExclusive ?? ((operation) => operation());
470
573
  const tick = async () => {
471
574
  if (manager.size === 0) return;
472
575
  if (polling) return;
473
576
  polling = true;
474
577
  try {
475
- const events = await tracker.poll(conn);
578
+ const events = await exclusive(() => tracker.poll(conn));
476
579
  if (events.length > 0) {
477
580
  manager.dispatch(events);
478
581
  }
@@ -480,7 +583,7 @@ function startPolling(conn, tracker, manager, intervalMs, onError) {
480
583
  tickCount++;
481
584
  if (tickCount >= CLEANUP_INTERVAL_TICKS) {
482
585
  tickCount = 0;
483
- await tracker.cleanup(conn);
586
+ await exclusive(() => tracker.cleanup(conn));
484
587
  }
485
588
  } catch (err) {
486
589
  consecutiveErrors++;
@@ -501,14 +604,31 @@ function startPolling(conn, tracker, manager, intervalMs, onError) {
501
604
  };
502
605
  return stop;
503
606
  }
504
- function matchesFilter(event, filter) {
607
+ function changeMatchesFilter(event, filter) {
505
608
  const target = event.type === "delete" ? event.oldRow ?? {} : event.row;
506
609
  for (const [key, value] of Object.entries(filter)) {
507
- if (target[key] !== value) {
610
+ if (!filterValueMatches(target[key], value)) {
508
611
  return false;
509
612
  }
510
613
  }
511
614
  return true;
512
615
  }
616
+ function filterValueMatches(rowValue, filterValue) {
617
+ if (rowValue === filterValue) return true;
618
+ if (typeof rowValue === "bigint" && typeof filterValue === "number") {
619
+ return Number.isInteger(filterValue) && BigInt(filterValue) === rowValue;
620
+ }
621
+ if (typeof rowValue === "number" && typeof filterValue === "bigint") {
622
+ return Number.isInteger(rowValue) && BigInt(rowValue) === filterValue;
623
+ }
624
+ if (rowValue instanceof Uint8Array && filterValue instanceof Uint8Array) {
625
+ if (rowValue.byteLength !== filterValue.byteLength) return false;
626
+ for (let i = 0; i < rowValue.byteLength; i++) {
627
+ if (rowValue[i] !== filterValue[i]) return false;
628
+ }
629
+ return true;
630
+ }
631
+ return false;
632
+ }
513
633
 
514
- export { ChangeTracker, SubscriptionBuilderImpl, SubscriptionManager, startPolling };
634
+ export { ChangeTracker, SubscriptionBuilderImpl, SubscriptionManager, changeMatchesFilter, isBulkLoadDurability, runBulkLoad, startPolling };
@@ -0,0 +1,35 @@
1
+ import { query, execute, executeBatch } from './chunk-CW6S3WL5.mjs';
2
+
3
+ // src/core/transaction.ts
4
+ var Transaction = class _Transaction {
5
+ constructor(conn) {
6
+ this.conn = conn;
7
+ }
8
+ _lastInsertRowId = 0;
9
+ async query(sql, params) {
10
+ return query(this.conn, sql, params);
11
+ }
12
+ async execute(sql, params) {
13
+ const result = await execute(this.conn, sql, params);
14
+ this._lastInsertRowId = result.lastInsertRowId;
15
+ return result;
16
+ }
17
+ async executeBatch(sql, paramsBatch) {
18
+ const results = await executeBatch(this.conn, sql, paramsBatch);
19
+ if (results.length > 0) {
20
+ this._lastInsertRowId = results[results.length - 1].lastInsertRowId;
21
+ }
22
+ return results;
23
+ }
24
+ get lastInsertRowId() {
25
+ return this._lastInsertRowId;
26
+ }
27
+ static async run(conn, fn) {
28
+ return conn.transaction(async (txConn) => {
29
+ const tx = new _Transaction(txConn);
30
+ return fn(tx);
31
+ });
32
+ }
33
+ };
34
+
35
+ export { Transaction };
@@ -0,0 +1,352 @@
1
+ import { WORKER_CANCELLED_CODE, deserializeError } from './chunk-4ISB7XMA.mjs';
2
+ import { BackupManager, BackupScheduler } from './chunk-VLTICJOD.mjs';
3
+ import { SirannonError } from './chunk-YPYVQJ4C.mjs';
4
+ import { Worker } from 'worker_threads';
5
+ import { existsSync } from 'fs';
6
+ import { resolve, dirname, join, sep } from 'path';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
8
+ import { AsyncLocalStorage } from 'async_hooks';
9
+
10
+ // src/core/driver/statement-cache.ts
11
+ var STATEMENT_CACHE_CAPACITY = 128;
12
+ function createStatementCache(prepare) {
13
+ const cache = /* @__PURE__ */ new Map();
14
+ return (sql) => {
15
+ const cached = cache.get(sql);
16
+ if (cached !== void 0) {
17
+ cache.delete(sql);
18
+ cache.set(sql, cached);
19
+ return cached;
20
+ }
21
+ const prepared = prepare(sql);
22
+ cache.set(sql, prepared);
23
+ if (cache.size > STATEMENT_CACHE_CAPACITY) {
24
+ const oldest = cache.keys().next().value;
25
+ if (oldest !== void 0) cache.delete(oldest);
26
+ }
27
+ return prepared;
28
+ };
29
+ }
30
+
31
+ // src/core/driver/values.ts
32
+ var SAFE_INT_BOUND = 9007199254740991n;
33
+ function narrowSafeBigInt(value) {
34
+ if (typeof value === "bigint" && value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
35
+ return Number(value);
36
+ }
37
+ return value;
38
+ }
39
+ function narrowRowIntegers(row) {
40
+ if (row === null || typeof row !== "object") return row;
41
+ const obj = row;
42
+ for (const key of Object.keys(obj)) {
43
+ const value = obj[key];
44
+ if (typeof value === "bigint" && value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
45
+ obj[key] = Number(value);
46
+ }
47
+ }
48
+ return row;
49
+ }
50
+ function narrowRowsIntegers(rows) {
51
+ for (const row of rows) {
52
+ narrowRowIntegers(row);
53
+ }
54
+ return rows;
55
+ }
56
+ function findPackageRoot(startDir) {
57
+ let dir = startDir;
58
+ for (; ; ) {
59
+ if (existsSync(join(dir, "package.json"))) return dir;
60
+ const parent = dirname(dir);
61
+ if (parent === dir) {
62
+ throw new SirannonError(
63
+ "Could not locate the package root to spawn the writer worker",
64
+ "WRITER_WORKER_UNRESOLVED"
65
+ );
66
+ }
67
+ dir = parent;
68
+ }
69
+ }
70
+ function resolveWorkerScript() {
71
+ const selfPath = fileURLToPath(import.meta.url);
72
+ const root = findPackageRoot(dirname(selfPath));
73
+ const bootstrap = join(root, "src", "core", "worker", "dev-bootstrap.mjs");
74
+ const built = join(root, "dist", "core", "writer-worker.mjs");
75
+ const runningFromSource = selfPath.startsWith(`${join(root, "src")}${sep}`);
76
+ const preferred = runningFromSource ? bootstrap : built;
77
+ const fallback = runningFromSource ? built : bootstrap;
78
+ if (existsSync(preferred)) return { url: pathToFileURL(preferred), execArgv: [] };
79
+ if (existsSync(fallback)) return { url: pathToFileURL(fallback), execArgv: [] };
80
+ throw new SirannonError(
81
+ `Writer-worker script not found under '${root}'. Build the package or run from source.`,
82
+ "WRITER_WORKER_UNRESOLVED"
83
+ );
84
+ }
85
+
86
+ // src/core/worker/host.ts
87
+ var DEFAULT_WRITE_TIMEOUT_MS = 3e4;
88
+ var DEFAULT_MAX_RESTARTS = 5;
89
+ var CLOSE_GRACE_MS = 1e3;
90
+ var WriterWorker = class _WriterWorker {
91
+ constructor(openRequest, timeoutMs, maxRestarts) {
92
+ this.openRequest = openRequest;
93
+ this.timeoutMs = timeoutMs;
94
+ this.maxRestarts = maxRestarts;
95
+ this.connection = this.makeConnection();
96
+ }
97
+ worker = null;
98
+ pending = /* @__PURE__ */ new Map();
99
+ nextId = 1;
100
+ ready = Promise.resolve();
101
+ starting = true;
102
+ closed = false;
103
+ fatal = null;
104
+ restarts = 0;
105
+ connection;
106
+ static async start(entry, path, options, workerOptions) {
107
+ const worker = new _WriterWorker(
108
+ { kind: "open", entry, path, options },
109
+ workerOptions?.writeTimeoutMs ?? DEFAULT_WRITE_TIMEOUT_MS,
110
+ workerOptions?.maxRestarts ?? DEFAULT_MAX_RESTARTS
111
+ );
112
+ worker.spawn();
113
+ try {
114
+ await worker.ready;
115
+ } catch (err) {
116
+ await worker.close();
117
+ throw err;
118
+ }
119
+ worker.starting = false;
120
+ return worker;
121
+ }
122
+ spawn() {
123
+ const script = resolveWorkerScript();
124
+ const worker = new Worker(script.url, { execArgv: script.execArgv });
125
+ this.worker = worker;
126
+ worker.on("message", (res) => {
127
+ if (this.worker === worker) this.onResponse(res);
128
+ });
129
+ worker.on("error", (err) => {
130
+ if (this.worker === worker) this.fault(err);
131
+ });
132
+ worker.on("exit", (code) => {
133
+ if (this.worker === worker && !this.closed) {
134
+ this.fault(new SirannonError(`Writer worker exited with code ${code}`, "WRITER_WORKER_EXIT"));
135
+ }
136
+ });
137
+ this.ready = this.send(this.openRequest).then(() => void 0);
138
+ this.ready.catch(() => {
139
+ });
140
+ }
141
+ onResponse(res) {
142
+ const entry = this.pending.get(res.id);
143
+ if (!entry) return;
144
+ this.pending.delete(res.id);
145
+ clearPendingTimers(entry);
146
+ if (res.ok) {
147
+ entry.resolve(res.value);
148
+ return;
149
+ }
150
+ if (res.error.code === WORKER_CANCELLED_CODE) {
151
+ entry.reject(
152
+ new SirannonError(
153
+ `The writer worker could not take this operation within ${this.timeoutMs}ms; it was not applied and is safe to retry`,
154
+ "WRITE_OVERLOADED"
155
+ )
156
+ );
157
+ return;
158
+ }
159
+ entry.reject(deserializeError(res.error));
160
+ }
161
+ rejectPending(id, err) {
162
+ const entry = this.pending.get(id);
163
+ if (!entry) return;
164
+ this.pending.delete(id);
165
+ clearPendingTimers(entry);
166
+ entry.reject(err);
167
+ }
168
+ unresponsiveError(waitedMs) {
169
+ return new SirannonError(
170
+ `Writer worker did not respond within ${waitedMs}ms; the operation's outcome is unknown`,
171
+ "WRITER_WORKER_TIMEOUT"
172
+ );
173
+ }
174
+ onDeadline(id) {
175
+ const entry = this.pending.get(id);
176
+ if (!entry) return;
177
+ const worker = this.worker;
178
+ if (!entry.cancellable || !worker) {
179
+ this.rejectPending(id, this.unresponsiveError(this.timeoutMs));
180
+ return;
181
+ }
182
+ try {
183
+ worker.postMessage({ kind: "cancel", id });
184
+ } catch {
185
+ this.rejectPending(id, this.unresponsiveError(this.timeoutMs));
186
+ return;
187
+ }
188
+ entry.graceTimer = setTimeout(() => {
189
+ this.rejectPending(id, this.unresponsiveError(this.timeoutMs * 2));
190
+ }, this.timeoutMs);
191
+ entry.graceTimer.unref?.();
192
+ }
193
+ fault(errLike) {
194
+ if (this.closed || this.fatal) return;
195
+ const err = errLike instanceof Error ? errLike : new SirannonError(String(errLike), "WRITER_WORKER_ERROR");
196
+ const dead = this.worker;
197
+ this.worker = null;
198
+ for (const entry of this.pending.values()) {
199
+ clearPendingTimers(entry);
200
+ entry.reject(err);
201
+ }
202
+ this.pending.clear();
203
+ dead?.terminate().catch(() => {
204
+ });
205
+ if (this.starting) {
206
+ this.fatal = err;
207
+ this.ready = Promise.reject(err);
208
+ this.ready.catch(() => {
209
+ });
210
+ return;
211
+ }
212
+ this.restarts++;
213
+ if (this.restarts > this.maxRestarts) {
214
+ this.fatal = new SirannonError(
215
+ `Writer worker failed ${this.restarts} times and will not restart: ${err.message}`,
216
+ "WRITER_WORKER_FATAL"
217
+ );
218
+ this.ready = Promise.reject(this.fatal);
219
+ this.ready.catch(() => {
220
+ });
221
+ return;
222
+ }
223
+ this.spawn();
224
+ }
225
+ send(request) {
226
+ const worker = this.worker;
227
+ if (!worker) {
228
+ return Promise.reject(
229
+ this.fatal ?? new SirannonError("Writer worker is unavailable", "WRITER_WORKER_UNAVAILABLE")
230
+ );
231
+ }
232
+ const id = this.nextId++;
233
+ const message = { ...request, id };
234
+ return new Promise((resolve2, reject) => {
235
+ let timer = null;
236
+ if (this.timeoutMs > 0) {
237
+ timer = setTimeout(() => this.onDeadline(id), this.timeoutMs);
238
+ timer.unref?.();
239
+ }
240
+ const cancellable = request.kind !== "open" && request.kind !== "close";
241
+ this.pending.set(id, { resolve: resolve2, reject, timer, graceTimer: null, cancellable });
242
+ try {
243
+ worker.postMessage(message);
244
+ } catch (err) {
245
+ this.pending.delete(id);
246
+ if (timer) clearTimeout(timer);
247
+ reject(
248
+ new SirannonError(
249
+ `Failed to hand work to the writer worker: ${err instanceof Error ? err.message : String(err)}`,
250
+ "WRITER_WORKER_POST_FAILED"
251
+ )
252
+ );
253
+ }
254
+ });
255
+ }
256
+ request(request) {
257
+ if (this.fatal) return Promise.reject(this.fatal);
258
+ if (this.closed) return Promise.reject(new SirannonError("Writer worker is closed", "WRITER_WORKER_CLOSED"));
259
+ return this.ready.then(() => this.send(request)).then((value) => {
260
+ this.restarts = 0;
261
+ return value;
262
+ });
263
+ }
264
+ makeConnection() {
265
+ const conn = {
266
+ exec: (sql) => this.request({ kind: "exec", sql }),
267
+ prepare: async (sql) => ({
268
+ all: (...params) => this.request({ kind: "all", sql, params }),
269
+ get: (...params) => this.request({ kind: "get", sql, params }),
270
+ run: (...params) => this.request({ kind: "run", sql, params }),
271
+ allRaw: (...params) => this.request({ kind: "allRaw", sql, params })
272
+ }),
273
+ runBatch: (sql, paramsBatch) => this.request({ kind: "runBatch", sql, paramsBatch }),
274
+ runBatchSummary: (sql, paramsBatch) => this.request({ kind: "runBatchSummary", sql, paramsBatch }),
275
+ runGroup: (units) => this.request({
276
+ kind: "runGroup",
277
+ units: units.map((unit) => ({
278
+ statements: unit.statements.map((statement) => ({
279
+ sql: statement.sql,
280
+ params: statement.params ? [...statement.params] : []
281
+ }))
282
+ }))
283
+ }),
284
+ transaction: async (fn) => {
285
+ await conn.exec("BEGIN");
286
+ try {
287
+ const result = await fn(conn);
288
+ await conn.exec("COMMIT");
289
+ return result;
290
+ } catch (err) {
291
+ try {
292
+ await conn.exec("ROLLBACK");
293
+ } catch {
294
+ }
295
+ throw err;
296
+ }
297
+ },
298
+ close: () => this.close()
299
+ };
300
+ return conn;
301
+ }
302
+ async close() {
303
+ if (this.closed) return;
304
+ this.closed = true;
305
+ const worker = this.worker;
306
+ if (worker) {
307
+ try {
308
+ await Promise.race([
309
+ this.send({ kind: "close" }),
310
+ new Promise((_, reject) => {
311
+ const timer = setTimeout(() => reject(new Error("close timed out")), CLOSE_GRACE_MS);
312
+ timer.unref?.();
313
+ })
314
+ ]);
315
+ } catch {
316
+ }
317
+ await worker.terminate().catch(() => {
318
+ });
319
+ }
320
+ this.worker = null;
321
+ for (const entry of this.pending.values()) {
322
+ clearPendingTimers(entry);
323
+ entry.reject(new SirannonError("Writer worker is closed", "WRITER_WORKER_CLOSED"));
324
+ }
325
+ this.pending.clear();
326
+ }
327
+ };
328
+ function clearPendingTimers(entry) {
329
+ if (entry.timer) clearTimeout(entry.timer);
330
+ if (entry.graceTimer) clearTimeout(entry.graceTimer);
331
+ }
332
+ function nodeWriterContext() {
333
+ const held = new AsyncLocalStorage();
334
+ return {
335
+ run: (operation) => held.run(true, operation),
336
+ isActive: () => held.getStore() === true,
337
+ exit: (operation) => held.exit(operation)
338
+ };
339
+ }
340
+ function nodeResolveExtensionPath(extensionPath) {
341
+ return resolve(extensionPath);
342
+ }
343
+ function nodeBackupEngine() {
344
+ const manager = new BackupManager();
345
+ const scheduler = new BackupScheduler(manager);
346
+ return {
347
+ backup: (conn, destPath) => manager.backup(conn, destPath),
348
+ schedule: (conn, options, runExclusive) => scheduler.schedule(conn, options, runExclusive)
349
+ };
350
+ }
351
+
352
+ export { WriterWorker, createStatementCache, narrowRowIntegers, narrowRowsIntegers, narrowSafeBigInt, nodeBackupEngine, nodeResolveExtensionPath, nodeWriterContext };