@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.
- package/README.md +154 -19
- package/dist/backup-scheduler/index.d.ts +18 -3
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
- package/dist/chunk-4ISB7XMA.mjs +21 -0
- package/dist/chunk-BNUTBHHH.mjs +22 -0
- package/dist/chunk-CJLYFDP5.mjs +26 -0
- package/dist/chunk-CW6S3WL5.mjs +222 -0
- package/dist/chunk-DVWQD3GF.mjs +49 -0
- package/dist/chunk-GEZUUIKV.mjs +268 -0
- package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
- package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
- package/dist/chunk-TKGHYWQ6.mjs +35 -0
- package/dist/chunk-UXLAO6ZH.mjs +352 -0
- package/dist/chunk-VLTICJOD.mjs +470 -0
- package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.mjs +348 -37
- package/dist/core/index.d.ts +70 -13
- package/dist/core/index.mjs +635 -192
- package/dist/core/writer-worker.d.ts +2 -0
- package/dist/core/writer-worker.mjs +129 -0
- package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +44 -6
- package/dist/driver/bun.mjs +35 -6
- package/dist/driver/expo.mjs +2 -2
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +48 -5
- package/dist/driver/wa-sqlite.d.ts +109 -0
- package/dist/driver/wa-sqlite.mjs +30 -2
- package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
- package/dist/file-migrations/index.d.ts +2 -3
- package/dist/file-migrations/index.mjs +1 -1
- package/dist/replication/coordinator/etcd.mjs +2 -2
- package/dist/replication/index.d.ts +7 -8
- package/dist/replication/index.mjs +79 -78
- package/dist/server/index.d.ts +111 -28
- package/dist/server/index.mjs +1016 -384
- package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/grpc.mjs +2 -2
- package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
- package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
- package/dist/types-D_hQW1hr.d.ts +494 -0
- package/package.json +3 -23
- package/dist/chunk-3MCMONVP.mjs +0 -115
- package/dist/chunk-74UN4DIE.mjs +0 -14
- package/dist/chunk-FB2U2Q3Y.mjs +0 -21
- package/dist/chunk-GS7T5YMI.mjs +0 -51
- package/dist/chunk-PXKAKK2V.mjs +0 -124
- package/dist/index-CLdNrcPz.d.ts +0 -16
- package/dist/types-BFSsG77t.d.ts +0 -29
- package/dist/types-D-74JiXb.d.ts +0 -265
|
@@ -1,11 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 "
|
|
7
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "
|
|
8
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 ??
|
|
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 && !
|
|
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
|
|
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]
|
|
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 };
|