@delali/sirannon-db 0.1.1 → 0.1.4

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.
@@ -1,91 +1,4 @@
1
- // src/core/errors.ts
2
- var SirannonError = class extends Error {
3
- constructor(message, code) {
4
- super(message);
5
- this.code = code;
6
- this.name = "SirannonError";
7
- }
8
- };
9
- var DatabaseNotFoundError = class extends SirannonError {
10
- constructor(id) {
11
- super(`Database '${id}' not found`, "DATABASE_NOT_FOUND");
12
- this.name = "DatabaseNotFoundError";
13
- }
14
- };
15
- var DatabaseAlreadyExistsError = class extends SirannonError {
16
- constructor(id) {
17
- super(`Database '${id}' already exists`, "DATABASE_ALREADY_EXISTS");
18
- this.name = "DatabaseAlreadyExistsError";
19
- }
20
- };
21
- var ReadOnlyError = class extends SirannonError {
22
- constructor(id) {
23
- super(`Database '${id}' is read-only`, "READ_ONLY");
24
- this.name = "ReadOnlyError";
25
- }
26
- };
27
- var QueryError = class extends SirannonError {
28
- constructor(message, sql) {
29
- super(message, "QUERY_ERROR");
30
- this.sql = sql;
31
- this.name = "QueryError";
32
- }
33
- };
34
- var TransactionError = class extends SirannonError {
35
- constructor(message) {
36
- super(message, "TRANSACTION_ERROR");
37
- this.name = "TransactionError";
38
- }
39
- };
40
- var MigrationError = class extends SirannonError {
41
- constructor(message, version, code = "MIGRATION_ERROR") {
42
- super(message, code);
43
- this.version = version;
44
- this.name = "MigrationError";
45
- }
46
- };
47
- var HookDeniedError = class extends SirannonError {
48
- constructor(hookName, reason) {
49
- super(
50
- reason ? `Hook '${hookName}' denied the operation: ${reason}` : `Hook '${hookName}' denied the operation`,
51
- "HOOK_DENIED"
52
- );
53
- this.name = "HookDeniedError";
54
- }
55
- };
56
- var CDCError = class extends SirannonError {
57
- constructor(message) {
58
- super(message, "CDC_ERROR");
59
- this.name = "CDCError";
60
- }
61
- };
62
- var BackupError = class extends SirannonError {
63
- constructor(message) {
64
- super(message, "BACKUP_ERROR");
65
- this.name = "BackupError";
66
- }
67
- };
68
- var ConnectionPoolError = class extends SirannonError {
69
- constructor(message) {
70
- super(message, "CONNECTION_POOL_ERROR");
71
- this.name = "ConnectionPoolError";
72
- }
73
- };
74
- var MaxDatabasesError = class extends SirannonError {
75
- constructor(max) {
76
- super(`Maximum number of open databases (${max}) reached`, "MAX_DATABASES");
77
- this.name = "MaxDatabasesError";
78
- }
79
- };
80
- var ExtensionError = class extends SirannonError {
81
- constructor(path, cause) {
82
- super(
83
- cause ? `Failed to load extension '${path}': ${cause}` : `Failed to load extension '${path}'`,
84
- "EXTENSION_ERROR"
85
- );
86
- this.name = "ExtensionError";
87
- }
88
- };
1
+ import { CDCError } from './chunk-O7BHI3CF.mjs';
89
2
 
90
3
  // src/core/cdc/change-tracker.ts
91
4
  var DEFAULT_RETENTION_MS = 36e5;
@@ -107,46 +20,50 @@ var ChangeTracker = class {
107
20
  this.pollBatchSize = options?.pollBatchSize ?? DEFAULT_POLL_BATCH_SIZE;
108
21
  this.assertIdentifier(this.changesTable, "changes table name");
109
22
  }
110
- watch(db, table) {
23
+ async watch(conn, table) {
111
24
  this.assertIdentifier(table, "table name");
112
- this.ensureChangesTable(db);
113
- const columns = this.getColumns(db, table);
25
+ await this.ensureChangesTable(conn);
26
+ const columns = await this.getColumns(conn, table);
114
27
  if (columns.length === 0) {
115
28
  throw new CDCError(`Table '${table}' does not exist or has no columns`);
116
29
  }
117
30
  for (const col of columns) {
118
31
  this.assertIdentifier(col, `column name in table '${table}'`);
119
32
  }
120
- const pkColumns = this.getPkColumns(db, table);
33
+ const pkColumns = await this.getPkColumns(conn, table);
121
34
  const existing = this.watched.get(table);
122
35
  if (existing) {
123
36
  const same = existing.columns.length === columns.length && existing.columns.every((col, i) => col === columns[i]);
124
37
  if (same) {
125
38
  return;
126
39
  }
127
- this.dropTriggers(db, table);
40
+ await conn.transaction(async (txConn) => {
41
+ await this.dropTriggers(txConn, table);
42
+ await this.installTriggers(txConn, table, columns, pkColumns);
43
+ });
44
+ } else {
45
+ await this.installTriggers(conn, table, columns, pkColumns);
128
46
  }
129
- this.installTriggers(db, table, columns, pkColumns);
130
47
  this.watched.set(table, { table, columns, pkColumns });
131
48
  this.watchedTablesCache = null;
132
49
  }
133
- unwatch(db, table) {
50
+ async unwatch(conn, table) {
134
51
  if (!this.watched.has(table)) {
135
52
  return;
136
53
  }
137
- this.dropTriggers(db, table);
54
+ await this.dropTriggers(conn, table);
138
55
  this.watched.delete(table);
139
56
  this.watchedTablesCache = null;
140
57
  }
141
- poll(db) {
58
+ async poll(conn) {
142
59
  if (!this.changesTableReady) {
143
- this.detectChangesTable(db);
60
+ await this.detectChangesTable(conn);
144
61
  if (!this.changesTableReady) {
145
62
  return [];
146
63
  }
147
64
  }
148
- const stmt = this.getStmt(
149
- db,
65
+ const stmt = await this.getStmt(
66
+ conn,
150
67
  "poll",
151
68
  `SELECT seq, table_name, operation, row_id, changed_at, old_data, new_data
152
69
  FROM "${this.changesTable}"
@@ -154,7 +71,7 @@ var ChangeTracker = class {
154
71
  ORDER BY seq ASC
155
72
  LIMIT ?`
156
73
  );
157
- const rows = stmt.all(this.lastSeq, this.pollBatchSize);
74
+ const rows = await stmt.all(this.lastSeq, this.pollBatchSize);
158
75
  if (rows.length === 0) {
159
76
  return [];
160
77
  }
@@ -172,24 +89,26 @@ var ChangeTracker = class {
172
89
  this.lastSeq = rows[rows.length - 1].seq;
173
90
  return events;
174
91
  }
175
- cleanup(db) {
92
+ async cleanup(conn) {
176
93
  if (!this.changesTableReady) {
177
- this.detectChangesTable(db);
94
+ await this.detectChangesTable(conn);
178
95
  if (!this.changesTableReady) {
179
96
  return 0;
180
97
  }
181
98
  }
182
99
  const cutoff = Date.now() / 1e3 - this.retentionMs / 1e3;
183
100
  if (this.lastSeq > 0) {
184
- const stmt2 = this.getStmt(
185
- db,
101
+ const stmt2 = await this.getStmt(
102
+ conn,
186
103
  "cleanup_coordinated",
187
104
  `DELETE FROM "${this.changesTable}" WHERE changed_at < ? AND seq <= ?`
188
105
  );
189
- return stmt2.run(cutoff, this.lastSeq).changes;
106
+ const result2 = await stmt2.run(cutoff, this.lastSeq);
107
+ return result2.changes;
190
108
  }
191
- const stmt = this.getStmt(db, "cleanup", `DELETE FROM "${this.changesTable}" WHERE changed_at < ?`);
192
- return stmt.run(cutoff).changes;
109
+ const stmt = await this.getStmt(conn, "cleanup", `DELETE FROM "${this.changesTable}" WHERE changed_at < ?`);
110
+ const result = await stmt.run(cutoff);
111
+ return result.changes;
193
112
  }
194
113
  get watchedTables() {
195
114
  if (!this.watchedTablesCache) {
@@ -204,17 +123,18 @@ var ChangeTracker = class {
204
123
  );
205
124
  }
206
125
  }
207
- detectChangesTable(db) {
208
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(this.changesTable);
126
+ async detectChangesTable(conn) {
127
+ const stmt = await conn.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?");
128
+ const row = await stmt.get(this.changesTable);
209
129
  if (row) {
210
130
  this.changesTableReady = true;
211
131
  }
212
132
  }
213
- ensureChangesTable(db) {
133
+ async ensureChangesTable(conn) {
214
134
  if (this.changesTableReady) {
215
135
  return;
216
136
  }
217
- db.exec(`
137
+ await conn.exec(`
218
138
  CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
219
139
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
220
140
  table_name TEXT NOT NULL,
@@ -224,34 +144,42 @@ CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
224
144
  old_data TEXT,
225
145
  new_data TEXT
226
146
  )`);
227
- db.exec(`CREATE INDEX IF NOT EXISTS "idx_${this.changesTable}_changed_at" ON "${this.changesTable}" (changed_at)`);
147
+ await conn.exec(
148
+ `CREATE INDEX IF NOT EXISTS "idx_${this.changesTable}_changed_at" ON "${this.changesTable}" (changed_at)`
149
+ );
228
150
  this.changesTableReady = true;
229
151
  }
230
- getColumns(db, table) {
231
- const info = db.prepare(`PRAGMA table_info("${table}")`).all();
152
+ async getColumns(conn, table) {
153
+ const stmt = await conn.prepare(`PRAGMA table_info("${table}")`);
154
+ const info = await stmt.all();
232
155
  return info.map((col) => col.name);
233
156
  }
234
- getPkColumns(db, table) {
235
- const info = db.prepare(`PRAGMA table_info("${table}")`).all();
157
+ async getPkColumns(conn, table) {
158
+ const stmt = await conn.prepare(`PRAGMA table_info("${table}")`);
159
+ const info = await stmt.all();
236
160
  return info.filter((col) => col.pk > 0).sort((a, b) => a.pk - b.pk).map((col) => col.name);
237
161
  }
238
- dropTriggers(db, table) {
239
- db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_insert"`);
240
- db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_update"`);
241
- db.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_delete"`);
162
+ async dropTriggers(conn, table) {
163
+ await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_insert"`);
164
+ await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_update"`);
165
+ await conn.exec(`DROP TRIGGER IF EXISTS "_sirannon_trg_${table}_delete"`);
242
166
  }
243
- getStmt(db, key, sql) {
244
- let stmts = this.stmtCache.get(db);
167
+ async getStmt(conn, key, sql) {
168
+ let stmts = this.stmtCache.get(conn);
245
169
  if (!stmts) {
246
170
  stmts = /* @__PURE__ */ new Map();
247
- this.stmtCache.set(db, stmts);
171
+ this.stmtCache.set(conn, stmts);
248
172
  }
249
- let stmt = stmts.get(key);
250
- if (!stmt) {
251
- stmt = db.prepare(sql);
252
- stmts.set(key, stmt);
173
+ const existing = stmts.get(key);
174
+ if (existing) return existing;
175
+ const pending = conn.prepare(sql);
176
+ stmts.set(key, pending);
177
+ try {
178
+ return await pending;
179
+ } catch (err) {
180
+ stmts.delete(key);
181
+ throw err;
253
182
  }
254
- return stmt;
255
183
  }
256
184
  buildPkRef(pkColumns, ref) {
257
185
  if (pkColumns.length === 0) {
@@ -262,12 +190,12 @@ CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
262
190
  }
263
191
  return pkColumns.map((col) => `${ref}."${this.escId(col)}"`).join(" || '-' || ");
264
192
  }
265
- installTriggers(db, table, columns, pkColumns) {
193
+ async installTriggers(conn, table, columns, pkColumns) {
266
194
  const newJson = this.buildJsonObject(columns, "NEW");
267
195
  const oldJson = this.buildJsonObject(columns, "OLD");
268
196
  const newPk = this.buildPkRef(pkColumns, "NEW");
269
197
  const oldPk = this.buildPkRef(pkColumns, "OLD");
270
- db.exec(`
198
+ await conn.exec(`
271
199
  CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_insert"
272
200
  AFTER INSERT ON "${table}"
273
201
  BEGIN
@@ -275,7 +203,7 @@ CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
275
203
  VALUES ('${table}', 'INSERT', ${newPk}, ${newJson});
276
204
  END
277
205
  `);
278
- db.exec(`
206
+ await conn.exec(`
279
207
  CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_update"
280
208
  AFTER UPDATE ON "${table}"
281
209
  BEGIN
@@ -283,7 +211,7 @@ CREATE TABLE IF NOT EXISTS "${this.changesTable}" (
283
211
  VALUES ('${table}', 'UPDATE', ${newPk}, ${oldJson}, ${newJson});
284
212
  END
285
213
  `);
286
- db.exec(`
214
+ await conn.exec(`
287
215
  CREATE TRIGGER IF NOT EXISTS "_sirannon_trg_${table}_delete"
288
216
  AFTER DELETE ON "${table}"
289
217
  BEGIN
@@ -369,15 +297,18 @@ var SubscriptionBuilderImpl = class {
369
297
  return this.manager.subscribe(this.table, this.conditions, callback);
370
298
  }
371
299
  };
372
- function startPolling(db, tracker, manager, intervalMs, onError) {
300
+ function startPolling(conn, tracker, manager, intervalMs, onError) {
373
301
  let consecutiveErrors = 0;
374
302
  let tickCount = 0;
303
+ let polling = false;
375
304
  const MAX_CONSECUTIVE_ERRORS = 10;
376
305
  const CLEANUP_INTERVAL_TICKS = 100;
377
- const tick = () => {
306
+ const tick = async () => {
378
307
  if (manager.size === 0) return;
308
+ if (polling) return;
309
+ polling = true;
379
310
  try {
380
- const events = tracker.poll(db);
311
+ const events = await tracker.poll(conn);
381
312
  if (events.length > 0) {
382
313
  manager.dispatch(events);
383
314
  }
@@ -385,7 +316,7 @@ function startPolling(db, tracker, manager, intervalMs, onError) {
385
316
  tickCount++;
386
317
  if (tickCount >= CLEANUP_INTERVAL_TICKS) {
387
318
  tickCount = 0;
388
- tracker.cleanup(db);
319
+ await tracker.cleanup(conn);
389
320
  }
390
321
  } catch (err) {
391
322
  consecutiveErrors++;
@@ -395,10 +326,14 @@ function startPolling(db, tracker, manager, intervalMs, onError) {
395
326
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
396
327
  stop();
397
328
  }
329
+ } finally {
330
+ polling = false;
398
331
  }
399
332
  };
400
333
  const interval = setInterval(tick, intervalMs);
401
- interval.unref();
334
+ if (typeof interval === "object" && "unref" in interval) {
335
+ interval.unref();
336
+ }
402
337
  const stop = () => {
403
338
  clearInterval(interval);
404
339
  };
@@ -414,4 +349,4 @@ function matchesFilter(event, filter) {
414
349
  return true;
415
350
  }
416
351
 
417
- export { BackupError, CDCError, ChangeTracker, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, SubscriptionBuilderImpl, SubscriptionManager, TransactionError, startPolling };
352
+ export { ChangeTracker, SubscriptionBuilderImpl, SubscriptionManager, startPolling };
@@ -0,0 +1,21 @@
1
+ // src/core/errors.ts
2
+ var SirannonError = class extends Error {
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "SirannonError";
7
+ }
8
+ };
9
+
10
+ // src/core/driver/define.ts
11
+ function defineDriver(config) {
12
+ if (!config.capabilities || typeof config.open !== "function") {
13
+ throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
14
+ }
15
+ return Object.freeze({
16
+ capabilities: Object.freeze({ ...config.capabilities }),
17
+ open: config.open
18
+ });
19
+ }
20
+
21
+ export { defineDriver };
@@ -0,0 +1,90 @@
1
+ // src/core/errors.ts
2
+ var SirannonError = class extends Error {
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "SirannonError";
7
+ }
8
+ };
9
+ var DatabaseNotFoundError = class extends SirannonError {
10
+ constructor(id) {
11
+ super(`Database '${id}' not found`, "DATABASE_NOT_FOUND");
12
+ this.name = "DatabaseNotFoundError";
13
+ }
14
+ };
15
+ var DatabaseAlreadyExistsError = class extends SirannonError {
16
+ constructor(id) {
17
+ super(`Database '${id}' already exists`, "DATABASE_ALREADY_EXISTS");
18
+ this.name = "DatabaseAlreadyExistsError";
19
+ }
20
+ };
21
+ var ReadOnlyError = class extends SirannonError {
22
+ constructor(id) {
23
+ super(`Database '${id}' is read-only`, "READ_ONLY");
24
+ this.name = "ReadOnlyError";
25
+ }
26
+ };
27
+ var QueryError = class extends SirannonError {
28
+ constructor(message, sql) {
29
+ super(message, "QUERY_ERROR");
30
+ this.sql = sql;
31
+ this.name = "QueryError";
32
+ }
33
+ };
34
+ var TransactionError = class extends SirannonError {
35
+ constructor(message) {
36
+ super(message, "TRANSACTION_ERROR");
37
+ this.name = "TransactionError";
38
+ }
39
+ };
40
+ var MigrationError = class extends SirannonError {
41
+ constructor(message, version, code = "MIGRATION_ERROR") {
42
+ super(message, code);
43
+ this.version = version;
44
+ this.name = "MigrationError";
45
+ }
46
+ };
47
+ var HookDeniedError = class extends SirannonError {
48
+ constructor(hookName, reason) {
49
+ super(
50
+ reason ? `Hook '${hookName}' denied the operation: ${reason}` : `Hook '${hookName}' denied the operation`,
51
+ "HOOK_DENIED"
52
+ );
53
+ this.name = "HookDeniedError";
54
+ }
55
+ };
56
+ var CDCError = class extends SirannonError {
57
+ constructor(message) {
58
+ super(message, "CDC_ERROR");
59
+ this.name = "CDCError";
60
+ }
61
+ };
62
+ var BackupError = class extends SirannonError {
63
+ constructor(message) {
64
+ super(message, "BACKUP_ERROR");
65
+ this.name = "BackupError";
66
+ }
67
+ };
68
+ var ConnectionPoolError = class extends SirannonError {
69
+ constructor(message) {
70
+ super(message, "CONNECTION_POOL_ERROR");
71
+ this.name = "ConnectionPoolError";
72
+ }
73
+ };
74
+ var MaxDatabasesError = class extends SirannonError {
75
+ constructor(max) {
76
+ super(`Maximum number of open databases (${max}) reached`, "MAX_DATABASES");
77
+ this.name = "MaxDatabasesError";
78
+ }
79
+ };
80
+ var ExtensionError = class extends SirannonError {
81
+ constructor(path, cause) {
82
+ super(
83
+ cause ? `Failed to load extension '${path}': ${cause}` : `Failed to load extension '${path}'`,
84
+ "EXTENSION_ERROR"
85
+ );
86
+ this.name = "ExtensionError";
87
+ }
88
+ };
89
+
90
+ export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError };
@@ -0,0 +1,124 @@
1
+ import { BackupError } from './chunk-O7BHI3CF.mjs';
2
+ import { existsSync, mkdirSync, rmSync, readdirSync, lstatSync } from 'fs';
3
+ import { resolve, dirname, join } from 'path';
4
+ import { Cron } from 'croner';
5
+
6
+ var BACKUP_FILE_PREFIX = "backup";
7
+ function hasControlCharacters(s) {
8
+ for (let i = 0; i < s.length; i++) {
9
+ const code = s.charCodeAt(i);
10
+ if (code <= 31) return true;
11
+ }
12
+ return false;
13
+ }
14
+ var BackupManager = class {
15
+ async backup(conn, destPath) {
16
+ if (hasControlCharacters(destPath)) {
17
+ throw new BackupError("Backup path contains invalid characters");
18
+ }
19
+ const segments = destPath.split(/[/\\]/);
20
+ if (segments.includes("..")) {
21
+ throw new BackupError("Backup path must not contain directory traversal segments");
22
+ }
23
+ const resolved = resolve(destPath);
24
+ const dir = dirname(resolved);
25
+ if (!existsSync(dir)) {
26
+ try {
27
+ mkdirSync(dir, { recursive: true });
28
+ } catch (err) {
29
+ throw new BackupError(
30
+ `Failed to create backup directory '${dir}': ${err instanceof Error ? err.message : String(err)}`
31
+ );
32
+ }
33
+ }
34
+ if (existsSync(resolved)) {
35
+ throw new BackupError(`Backup destination '${destPath}' already exists`);
36
+ }
37
+ const escaped = resolved.replace(/'/g, "''");
38
+ try {
39
+ await conn.exec(`VACUUM INTO '${escaped}'`);
40
+ } catch (err) {
41
+ try {
42
+ rmSync(resolved, { force: true });
43
+ } catch {
44
+ }
45
+ throw new BackupError(`Backup to '${destPath}' failed: ${err instanceof Error ? err.message : String(err)}`);
46
+ }
47
+ }
48
+ generateFilename() {
49
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
50
+ return `${BACKUP_FILE_PREFIX}-${ts}.db`;
51
+ }
52
+ rotate(dir, maxFiles) {
53
+ if (maxFiles <= 0) return;
54
+ const resolved = resolve(dir);
55
+ if (!existsSync(resolved)) return;
56
+ let entries;
57
+ try {
58
+ entries = readdirSync(resolved).filter((f) => f.startsWith(`${BACKUP_FILE_PREFIX}-`) && f.endsWith(".db")).map((f) => {
59
+ const filePath = join(resolved, f);
60
+ return { path: filePath, mtimeMs: lstatSync(filePath).mtimeMs };
61
+ }).sort((a, b) => b.mtimeMs - a.mtimeMs);
62
+ } catch (err) {
63
+ throw new BackupError(
64
+ `Failed to list backup files in '${dir}': ${err instanceof Error ? err.message : String(err)}`
65
+ );
66
+ }
67
+ for (const entry of entries.slice(maxFiles)) {
68
+ try {
69
+ rmSync(entry.path, { force: true });
70
+ } catch {
71
+ }
72
+ }
73
+ }
74
+ };
75
+ var DEFAULT_MAX_FILES = 5;
76
+ var BackupScheduler = class {
77
+ manager;
78
+ constructor(manager) {
79
+ this.manager = manager ?? new BackupManager();
80
+ }
81
+ schedule(conn, options) {
82
+ const { cron: cronExpr, destDir, maxFiles = DEFAULT_MAX_FILES, onError } = options;
83
+ const resolvedDir = resolve(destDir);
84
+ if (!existsSync(resolvedDir)) {
85
+ try {
86
+ mkdirSync(resolvedDir, { recursive: true });
87
+ } catch (err) {
88
+ throw new BackupError(
89
+ `Failed to create backup directory '${destDir}': ${err instanceof Error ? err.message : String(err)}`
90
+ );
91
+ }
92
+ }
93
+ let job;
94
+ try {
95
+ job = new Cron(
96
+ cronExpr,
97
+ {
98
+ unref: true,
99
+ catch: (err) => {
100
+ if (onError) {
101
+ const error = err instanceof Error ? err : new BackupError(typeof err === "string" ? err : "Scheduled backup failed");
102
+ onError(error);
103
+ }
104
+ }
105
+ },
106
+ async () => {
107
+ const filename = this.manager.generateFilename();
108
+ const destPath = join(resolvedDir, filename);
109
+ await this.manager.backup(conn, destPath);
110
+ this.manager.rotate(resolvedDir, maxFiles);
111
+ }
112
+ );
113
+ } catch (err) {
114
+ throw new BackupError(
115
+ `Invalid cron expression '${cronExpr}': ${err instanceof Error ? err.message : String(err)}`
116
+ );
117
+ }
118
+ return () => {
119
+ job.stop();
120
+ };
121
+ }
122
+ };
123
+
124
+ export { BackupManager, BackupScheduler };
@@ -1,5 +1,41 @@
1
- import { P as Params, d as ChangeEvent, f as ClientOptions } from '../types-DArCObcu.js';
2
- import { c as QueryResponse, b as ExecuteResponse, d as TransactionResponse } from '../protocol-BX1H-_Mz.js';
1
+ /** Query parameter types: named (object) or positional (array). */
2
+ type Params = Record<string, unknown> | unknown[];
3
+ /** CDC operation type. */
4
+ type ChangeOperation = 'insert' | 'update' | 'delete';
5
+ /** Event emitted when a watched table row changes. */
6
+ interface ChangeEvent<T = Record<string, unknown>> {
7
+ type: ChangeOperation;
8
+ table: string;
9
+ row: T;
10
+ oldRow?: T;
11
+ seq: bigint;
12
+ timestamp: number;
13
+ }
14
+ /** Options for the client SDK. */
15
+ interface ClientOptions {
16
+ /** Transport to use. Default: 'websocket'. */
17
+ transport?: 'websocket' | 'http';
18
+ /** Custom headers for HTTP requests. */
19
+ headers?: Record<string, string>;
20
+ /** Reconnect on WebSocket disconnect. Default: true. */
21
+ autoReconnect?: boolean;
22
+ /** Reconnect interval in ms. Default: 1000. */
23
+ reconnectInterval?: number;
24
+ }
25
+
26
+ /** Response for a successful query. */
27
+ interface QueryResponse {
28
+ rows: Record<string, unknown>[];
29
+ }
30
+ /** Response for a successful execute. */
31
+ interface ExecuteResponse {
32
+ changes: number;
33
+ lastInsertRowId: number | string;
34
+ }
35
+ /** Response for a successful transaction. */
36
+ interface TransactionResponse {
37
+ results: ExecuteResponse[];
38
+ }
3
39
 
4
40
  /**
5
41
  * Transport layer for communicating with a sirannon-db server.