@delali/sirannon-db 0.1.6 → 0.1.7

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 +134 -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-4IGMIJQK.mjs +318 -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-DJLX6CAE.mjs +20 -0
  10. package/dist/chunk-DVWQD3GF.mjs +49 -0
  11. package/dist/chunk-GEZUUIKV.mjs +268 -0
  12. package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
  13. package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
  14. package/dist/chunk-TKGHYWQ6.mjs +35 -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 +107 -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
@@ -0,0 +1,268 @@
1
+ import { ForbiddenSqlError } from './chunk-YPYVQJ4C.mjs';
2
+
3
+ // src/core/internal-tables.ts
4
+ var INTERNAL_TABLE_PREFIX = "_sirannon_";
5
+ var CHANGES_TABLE = "_sirannon_changes";
6
+ var META_TABLE = "_sirannon_meta";
7
+ var MIGRATIONS_TABLE = "_sirannon_migrations";
8
+ var PEER_STATE_TABLE = "_sirannon_peer_state";
9
+ var APPLIED_CHANGES_TABLE = "_sirannon_applied_changes";
10
+ var COLUMN_VERSIONS_TABLE = "_sirannon_column_versions";
11
+ var SYNC_STATE_TABLE = "_sirannon_sync_state";
12
+ var CDC_TRIGGER_PREFIX = "_sirannon_trg_";
13
+ var SIRANNON_PREFIX = "_sirannon";
14
+ var SQLITE_PREFIX = "sqlite_";
15
+ var SIRANNON_MESSAGE = "Access to internal tables is not permitted: identifiers beginning with '_sirannon' are reserved";
16
+ var SQLITE_WRITE_MESSAGE = "The SQLite schema catalogue is read-only: statements that modify a 'sqlite_' identifier are not permitted";
17
+ var ATTACH_MESSAGE = "ATTACH and DETACH statements are not permitted";
18
+ var WRITABLE_SCHEMA_MESSAGE = "PRAGMA writable_schema is not permitted";
19
+ var WRITE_VERBS = /* @__PURE__ */ new Set(["insert", "update", "delete", "replace", "create", "alter", "drop", "vacuum", "reindex"]);
20
+ function reservedKind(name) {
21
+ const lower = name.toLowerCase();
22
+ if (lower.startsWith(SIRANNON_PREFIX)) return "sirannon";
23
+ if (lower.startsWith(SQLITE_PREFIX)) return "sqlite";
24
+ return null;
25
+ }
26
+ function isReservedIdentifier(name) {
27
+ return reservedKind(name) !== null;
28
+ }
29
+ function reservedSqlError(sql) {
30
+ const lower = sql.toLowerCase();
31
+ if (!lower.includes("_sirannon") && !lower.includes("sqlite_") && !lower.includes("attach") && !lower.includes("detach") && !lower.includes("writable_schema")) {
32
+ return null;
33
+ }
34
+ return scan(sql);
35
+ }
36
+ function assertSqlAllowed(sql) {
37
+ const reason = reservedSqlError(sql);
38
+ if (reason) {
39
+ throw new ForbiddenSqlError(reason);
40
+ }
41
+ }
42
+ function isWordStart(c) {
43
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
44
+ }
45
+ function isWordPart(c) {
46
+ return isWordStart(c) || c >= "0" && c <= "9" || c === "$";
47
+ }
48
+ function referenceError(kind, verb) {
49
+ if (kind === "sirannon") return SIRANNON_MESSAGE;
50
+ if (kind === "sqlite" && WRITE_VERBS.has(verb)) return SQLITE_WRITE_MESSAGE;
51
+ return null;
52
+ }
53
+ function scan(sql) {
54
+ const n = sql.length;
55
+ let i = 0;
56
+ let statementStart = true;
57
+ let verb = "";
58
+ while (i < n) {
59
+ const c = sql[i];
60
+ if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v") {
61
+ i++;
62
+ continue;
63
+ }
64
+ if (c === "-" && sql[i + 1] === "-") {
65
+ i += 2;
66
+ while (i < n && sql[i] !== "\n") i++;
67
+ continue;
68
+ }
69
+ if (c === "/" && sql[i + 1] === "*") {
70
+ i += 2;
71
+ while (i < n && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
72
+ i += 2;
73
+ continue;
74
+ }
75
+ if (c === "'") {
76
+ i = skipQuoted(sql, i, "'");
77
+ statementStart = false;
78
+ continue;
79
+ }
80
+ if (c === '"' || c === "`") {
81
+ const delimited = readDelimited(sql, i, c);
82
+ const error = referenceError(reservedKind(delimited.value), verb);
83
+ if (error) return error;
84
+ i = delimited.next;
85
+ statementStart = false;
86
+ continue;
87
+ }
88
+ if (c === "[") {
89
+ const end = sql.indexOf("]", i + 1);
90
+ const stop = end === -1 ? n : end;
91
+ const error = referenceError(reservedKind(sql.slice(i + 1, stop)), verb);
92
+ if (error) return error;
93
+ i = stop + 1;
94
+ statementStart = false;
95
+ continue;
96
+ }
97
+ if (c === ";") {
98
+ i++;
99
+ statementStart = true;
100
+ verb = "";
101
+ continue;
102
+ }
103
+ if (isWordStart(c)) {
104
+ let j = i + 1;
105
+ while (j < n && isWordPart(sql[j])) j++;
106
+ const word = sql.slice(i, j);
107
+ const lowerWord = word.toLowerCase();
108
+ if (statementStart) {
109
+ verb = lowerWord;
110
+ if (lowerWord === "attach" || lowerWord === "detach") return ATTACH_MESSAGE;
111
+ } else if (verb === "pragma" && lowerWord === "writable_schema") {
112
+ return WRITABLE_SCHEMA_MESSAGE;
113
+ }
114
+ const error = referenceError(reservedKind(word), verb);
115
+ if (error) return error;
116
+ i = j;
117
+ statementStart = false;
118
+ continue;
119
+ }
120
+ i++;
121
+ statementStart = false;
122
+ }
123
+ return null;
124
+ }
125
+ function skipQuoted(sql, start, quote) {
126
+ const n = sql.length;
127
+ let i = start + 1;
128
+ while (i < n) {
129
+ if (sql[i] === quote) {
130
+ if (sql[i + 1] === quote) {
131
+ i += 2;
132
+ continue;
133
+ }
134
+ return i + 1;
135
+ }
136
+ i++;
137
+ }
138
+ return n;
139
+ }
140
+ function readDelimited(sql, start, quote) {
141
+ const n = sql.length;
142
+ let i = start + 1;
143
+ let value = "";
144
+ while (i < n) {
145
+ if (sql[i] === quote) {
146
+ if (sql[i + 1] === quote) {
147
+ value += quote;
148
+ i += 2;
149
+ continue;
150
+ }
151
+ return { value, next: i + 1 };
152
+ }
153
+ value += sql[i];
154
+ i++;
155
+ }
156
+ return { value, next: n };
157
+ }
158
+
159
+ // src/core/cdc/encoding.ts
160
+ var BLOB_TAG = "__sirannon_blob";
161
+ var INT_TAG = "__sirannon_int";
162
+ var SAFE_INT_BOUND_TEXT = "9007199254740991";
163
+ var SAFE_INT_BOUND = 9007199254740991n;
164
+ var HEX_BYTES_RE = /^[0-9a-fA-F]*$/;
165
+ var INT_PAYLOAD_RE = /^-?\d{1,19}$/;
166
+ function encodeTaggedValues(value) {
167
+ if (typeof value === "bigint" || isBinaryValue(value)) {
168
+ return encodeTaggedLeaf(value);
169
+ }
170
+ if (value === null || typeof value !== "object") return value;
171
+ if (Array.isArray(value)) {
172
+ return value.map(encodeTaggedValues);
173
+ }
174
+ if (typeof value.toJSON === "function") {
175
+ return value;
176
+ }
177
+ const obj = value;
178
+ const out = {};
179
+ for (const k of Object.keys(obj)) {
180
+ out[k] = encodeTaggedValues(obj[k]);
181
+ }
182
+ return out;
183
+ }
184
+ function encodeTaggedLeaf(value) {
185
+ if (typeof value === "bigint") {
186
+ if (value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
187
+ return Number(value);
188
+ }
189
+ return { [INT_TAG]: value.toString() };
190
+ }
191
+ if (isBinaryValue(value)) {
192
+ return { [BLOB_TAG]: encodeHexBytes(value) };
193
+ }
194
+ return value;
195
+ }
196
+ function encodeWireRowsInPlace(rows) {
197
+ for (let i = 0; i < rows.length; i++) {
198
+ const row = rows[i];
199
+ if (row === null || typeof row !== "object") continue;
200
+ const obj = row;
201
+ for (const k of Object.keys(obj)) {
202
+ const cell = obj[k];
203
+ if (typeof cell === "bigint" || isBinaryValue(cell)) {
204
+ obj[k] = encodeTaggedLeaf(cell);
205
+ }
206
+ }
207
+ }
208
+ return rows;
209
+ }
210
+ function decodeTaggedValues(value) {
211
+ if (value === null || typeof value !== "object") return value;
212
+ if (isBinaryValue(value)) return value;
213
+ if (Array.isArray(value)) {
214
+ return value.map(decodeTaggedValues);
215
+ }
216
+ const obj = value;
217
+ const keys = Object.keys(obj);
218
+ if (keys.length === 1) {
219
+ const tag = keys[0];
220
+ const tagValue = obj[tag];
221
+ if (tag === BLOB_TAG && typeof tagValue === "string") {
222
+ return decodeHexBytes(tagValue);
223
+ }
224
+ if (tag === INT_TAG && typeof tagValue === "string") {
225
+ if (!INT_PAYLOAD_RE.test(tagValue)) {
226
+ throw new Error("Invalid tagged integer payload");
227
+ }
228
+ return BigInt(tagValue);
229
+ }
230
+ }
231
+ const out = {};
232
+ for (const k of keys) {
233
+ out[k] = decodeTaggedValues(obj[k]);
234
+ }
235
+ return out;
236
+ }
237
+ function isBinaryValue(value) {
238
+ return value instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(value);
239
+ }
240
+ function encodeHexBytes(bytes) {
241
+ if (typeof Buffer !== "undefined") {
242
+ return Buffer.from(bytes).toString("hex").toUpperCase();
243
+ }
244
+ let hex = "";
245
+ for (const byte of bytes) {
246
+ hex += byte.toString(16).padStart(2, "0");
247
+ }
248
+ return hex.toUpperCase();
249
+ }
250
+ function decodeHexBytes(hex) {
251
+ if (hex.length % 2 !== 0 || !HEX_BYTES_RE.test(hex)) {
252
+ throw new Error("Invalid CDC BLOB hex payload");
253
+ }
254
+ if (typeof Buffer !== "undefined") {
255
+ return Buffer.from(hex, "hex");
256
+ }
257
+ const bytes = new Uint8Array(hex.length / 2);
258
+ for (let i = 0; i < hex.length; i += 2) {
259
+ const byte = Number.parseInt(hex.slice(i, i + 2), 16);
260
+ if (Number.isNaN(byte)) {
261
+ throw new Error("Invalid CDC BLOB hex payload");
262
+ }
263
+ bytes[i / 2] = byte;
264
+ }
265
+ return bytes;
266
+ }
267
+
268
+ export { APPLIED_CHANGES_TABLE, CDC_TRIGGER_PREFIX, CHANGES_TABLE, COLUMN_VERSIONS_TABLE, INTERNAL_TABLE_PREFIX, META_TABLE, MIGRATIONS_TABLE, PEER_STATE_TABLE, SAFE_INT_BOUND_TEXT, SYNC_STATE_TABLE, assertSqlAllowed, decodeTaggedValues, encodeTaggedValues, encodeWireRowsInPlace, isReservedIdentifier };
@@ -1,4 +1,4 @@
1
- import { SirannonError } from './chunk-O7BHI3CF.mjs';
1
+ import { SirannonError } from './chunk-YPYVQJ4C.mjs';
2
2
 
3
3
  // src/replication/errors.ts
4
4
  var ReplicationError = class extends SirannonError {
@@ -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 };