@delali/sirannon-db 0.1.3 → 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,214 +1,58 @@
1
- import { BackupError, ConnectionPoolError, QueryError, MigrationError, ReadOnlyError, SubscriptionBuilderImpl, ExtensionError, SirannonError, ChangeTracker, SubscriptionManager, startPolling, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError } from '../chunk-VI4UP4RR.mjs';
2
- export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError } from '../chunk-VI4UP4RR.mjs';
3
- import { existsSync, mkdirSync, rmSync, readdirSync, lstatSync, readFileSync, statSync } from 'fs';
4
- import { resolve, dirname, join } from 'path';
5
- import { Cron } from 'croner';
6
- import Database from 'better-sqlite3';
1
+ import { SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-AX66KWBR.mjs';
2
+ export { defineDriver } from '../chunk-74UN4DIE.mjs';
3
+ import { BackupManager, BackupScheduler } from '../chunk-PXKAKK2V.mjs';
4
+ export { BackupManager, BackupScheduler } from '../chunk-PXKAKK2V.mjs';
5
+ import { ConnectionPoolError, QueryError, MigrationError, ReadOnlyError, ExtensionError, SirannonError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError } from '../chunk-O7BHI3CF.mjs';
6
+ export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError } from '../chunk-O7BHI3CF.mjs';
7
7
 
8
- var BACKUP_FILE_PREFIX = "backup";
9
- function hasControlCharacters(s) {
10
- for (let i = 0; i < s.length; i++) {
11
- const code = s.charCodeAt(i);
12
- if (code <= 31 && code !== 9 && code !== 10 && code !== 13) {
13
- return true;
14
- }
15
- }
16
- return false;
17
- }
18
- var BackupManager = class {
19
- /**
20
- * Creates a one-shot backup of the database using VACUUM INTO.
21
- * Produces a fresh, defragmented copy at the specified destination path.
22
- *
23
- * The destination file must not already exist. Parent directories are
24
- * created automatically when missing.
25
- *
26
- * Note: there is a narrow TOCTOU window between the existence check and
27
- * the VACUUM INTO statement. In the unlikely event that another process
28
- * creates a file at the same path during that window, VACUUM INTO may
29
- * silently overwrite it depending on the SQLite version.
30
- */
31
- backup(db, destPath) {
32
- if (hasControlCharacters(destPath)) {
33
- throw new BackupError("Backup path contains invalid characters");
34
- }
35
- const resolved = resolve(destPath);
36
- const dir = dirname(resolved);
37
- if (!existsSync(dir)) {
38
- try {
39
- mkdirSync(dir, { recursive: true });
40
- } catch (err) {
41
- throw new BackupError(
42
- `Failed to create backup directory '${dir}': ${err instanceof Error ? err.message : String(err)}`
43
- );
44
- }
45
- }
46
- if (existsSync(resolved)) {
47
- throw new BackupError(`Backup destination '${destPath}' already exists`);
48
- }
49
- const escaped = resolved.replace(/'/g, "''");
50
- try {
51
- db.exec(`VACUUM INTO '${escaped}'`);
52
- } catch (err) {
53
- try {
54
- rmSync(resolved, { force: true });
55
- } catch {
56
- }
57
- throw new BackupError(`Backup to '${destPath}' failed: ${err instanceof Error ? err.message : String(err)}`);
58
- }
59
- }
60
- /**
61
- * Generates a timestamped backup filename.
62
- *
63
- * Format: `backup-YYYY-MM-DDTHH-MM-SS-mmmZ.db`
64
- *
65
- * Colons and dots in the ISO timestamp are replaced with hyphens so the
66
- * filename is safe on every major filesystem.
67
- */
68
- generateFilename() {
69
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
70
- return `${BACKUP_FILE_PREFIX}-${ts}.db`;
71
- }
72
- /**
73
- * Removes old backup files in {@link dir}, keeping the {@link maxFiles}
74
- * most recent entries. Only files matching the `backup-*.db` naming
75
- * convention are considered; other files in the directory are left alone.
76
- *
77
- * When {@link maxFiles} is zero or negative the method is a no-op.
78
- * A non-existent directory is silently ignored.
79
- */
80
- rotate(dir, maxFiles) {
81
- if (maxFiles <= 0) return;
82
- const resolved = resolve(dir);
83
- if (!existsSync(resolved)) return;
84
- let entries;
85
- try {
86
- entries = readdirSync(resolved).filter((f) => f.startsWith(`${BACKUP_FILE_PREFIX}-`) && f.endsWith(".db")).map((f) => {
87
- const filePath = join(resolved, f);
88
- return { path: filePath, mtimeMs: lstatSync(filePath).mtimeMs };
89
- }).sort((a, b) => b.mtimeMs - a.mtimeMs);
90
- } catch (err) {
91
- throw new BackupError(
92
- `Failed to list backup files in '${dir}': ${err instanceof Error ? err.message : String(err)}`
93
- );
94
- }
95
- for (const entry of entries.slice(maxFiles)) {
96
- try {
97
- rmSync(entry.path, { force: true });
98
- } catch {
99
- }
100
- }
101
- }
102
- };
103
- var DEFAULT_MAX_FILES = 5;
104
- var BackupScheduler = class {
105
- manager;
106
- constructor(manager) {
107
- this.manager = manager ?? new BackupManager();
108
- }
109
- /**
110
- * Schedules periodic backups on a cron expression.
111
- *
112
- * Each tick creates a timestamped backup file inside {@link options.destDir}
113
- * and rotates old files so no more than {@link options.maxFiles} (default 5)
114
- * are retained.
115
- *
116
- * Provide {@link options.onError} to receive notification when a scheduled
117
- * backup fails. Without it, errors are silently discarded to prevent
118
- * unhandled exceptions from crashing the process.
119
- *
120
- * The underlying timer is unreferenced so it won't keep the Node.js
121
- * process alive on its own (consistent with the CDC polling timer).
122
- *
123
- * Returns a cancel function that stops the scheduled job immediately.
124
- */
125
- schedule(db, options) {
126
- const { cron: cronExpr, destDir, maxFiles = DEFAULT_MAX_FILES, onError } = options;
127
- const resolvedDir = resolve(destDir);
128
- if (!existsSync(resolvedDir)) {
129
- try {
130
- mkdirSync(resolvedDir, { recursive: true });
131
- } catch (err) {
132
- throw new BackupError(
133
- `Failed to create backup directory '${destDir}': ${err instanceof Error ? err.message : String(err)}`
134
- );
135
- }
136
- }
137
- let job;
138
- try {
139
- job = new Cron(
140
- cronExpr,
141
- {
142
- unref: true,
143
- catch: (err) => {
144
- if (onError) {
145
- const error = err instanceof Error ? err : new BackupError(typeof err === "string" ? err : "Scheduled backup failed");
146
- onError(error);
147
- }
148
- }
149
- },
150
- () => {
151
- const filename = this.manager.generateFilename();
152
- const destPath = join(resolvedDir, filename);
153
- this.manager.backup(db, destPath);
154
- this.manager.rotate(resolvedDir, maxFiles);
155
- }
156
- );
157
- } catch (err) {
158
- throw new BackupError(
159
- `Invalid cron expression '${cronExpr}': ${err instanceof Error ? err.message : String(err)}`
160
- );
161
- }
162
- return () => {
163
- job.stop();
164
- };
165
- }
166
- };
167
- function closeAllSilently(connections) {
8
+ // src/core/connection-pool.ts
9
+ async function closeAllSilently(connections) {
168
10
  for (const conn of connections) {
169
11
  if (!conn) continue;
170
12
  try {
171
- conn.close();
13
+ await conn.close();
172
14
  } catch {
173
15
  }
174
16
  }
175
17
  }
176
- var ConnectionPool = class {
18
+ var ConnectionPool = class _ConnectionPool {
177
19
  writer;
178
20
  readers;
179
21
  readerIndex = 0;
180
22
  closed = false;
181
- constructor(options) {
182
- const { path, readOnly = false, readPoolSize = 4, walMode = true } = options;
23
+ constructor(writer, readers) {
24
+ this.writer = writer;
25
+ this.readers = readers;
26
+ }
27
+ static async create(options) {
28
+ const { driver, path, readOnly = false, readPoolSize = 4, walMode = true } = options;
183
29
  let writer = null;
184
30
  const readers = [];
185
31
  try {
186
32
  if (!readOnly) {
187
- writer = new Database(path);
188
- if (walMode) {
189
- writer.pragma("journal_mode = WAL");
190
- }
191
- writer.pragma("synchronous = NORMAL");
192
- writer.pragma("foreign_keys = ON");
193
- writer.pragma("busy_timeout = 5000");
33
+ writer = await driver.open(path, { walMode });
194
34
  }
195
- const poolSize = Math.max(readPoolSize, 1);
35
+ const poolSize = driver.capabilities.multipleConnections ? Math.max(readPoolSize, 1) : 0;
196
36
  for (let i = 0; i < poolSize; i++) {
197
- const reader = new Database(path, { readonly: true });
37
+ const reader = await driver.open(path, { readonly: true, walMode: false });
198
38
  readers.push(reader);
199
- reader.pragma("foreign_keys = ON");
200
39
  }
201
40
  } catch (err) {
202
- closeAllSilently([...readers, writer]);
41
+ await closeAllSilently([...readers, writer]);
203
42
  throw err;
204
43
  }
205
- this.writer = writer;
206
- this.readers = readers;
44
+ return new _ConnectionPool(writer, readers);
207
45
  }
208
46
  acquireReader() {
209
47
  if (this.closed) {
210
48
  throw new ConnectionPoolError("Connection pool is closed");
211
49
  }
50
+ if (this.readers.length === 0) {
51
+ if (!this.writer) {
52
+ throw new ConnectionPoolError("No connections available");
53
+ }
54
+ return this.writer;
55
+ }
212
56
  const reader = this.readers[this.readerIndex % this.readers.length];
213
57
  this.readerIndex = (this.readerIndex + 1) % this.readers.length;
214
58
  return reader;
@@ -228,31 +72,20 @@ var ConnectionPool = class {
228
72
  get isReadOnly() {
229
73
  return this.writer === null;
230
74
  }
231
- loadExtension(extensionPath) {
232
- if (this.closed) {
233
- throw new ConnectionPoolError("Connection pool is closed");
234
- }
235
- if (this.writer) {
236
- this.writer.loadExtension(extensionPath);
237
- }
238
- for (const reader of this.readers) {
239
- reader.loadExtension(extensionPath);
240
- }
241
- }
242
- close() {
75
+ async close() {
243
76
  if (this.closed) return;
244
77
  this.closed = true;
245
78
  const errors = [];
246
79
  for (const reader of this.readers) {
247
80
  try {
248
- reader.close();
81
+ await reader.close();
249
82
  } catch (err) {
250
83
  errors.push(err);
251
84
  }
252
85
  }
253
86
  if (this.writer) {
254
87
  try {
255
- this.writer.close();
88
+ await this.writer.close();
256
89
  } catch (err) {
257
90
  errors.push(err);
258
91
  }
@@ -348,11 +181,11 @@ var HookRegistry = class {
348
181
  // src/core/query-executor.ts
349
182
  var STATEMENT_CACHE_CAPACITY = 128;
350
183
  var statementCaches = /* @__PURE__ */ new WeakMap();
351
- function getStatement(db, sql) {
352
- let cache = statementCaches.get(db);
184
+ async function getStatement(conn, sql) {
185
+ let cache = statementCaches.get(conn);
353
186
  if (!cache) {
354
187
  cache = /* @__PURE__ */ new Map();
355
- statementCaches.set(db, cache);
188
+ statementCaches.set(conn, cache);
356
189
  }
357
190
  const cached = cache.get(sql);
358
191
  if (cached) {
@@ -360,64 +193,66 @@ function getStatement(db, sql) {
360
193
  cache.set(sql, cached);
361
194
  return cached;
362
195
  }
363
- const stmt = db.prepare(sql);
364
- cache.set(sql, stmt);
196
+ const pending = conn.prepare(sql);
197
+ cache.set(sql, pending);
365
198
  if (cache.size > STATEMENT_CACHE_CAPACITY) {
366
199
  const oldest = cache.keys().next().value;
367
200
  if (oldest !== void 0) {
368
201
  cache.delete(oldest);
369
202
  }
370
203
  }
371
- return stmt;
204
+ try {
205
+ return await pending;
206
+ } catch (err) {
207
+ cache.delete(sql);
208
+ throw err;
209
+ }
372
210
  }
373
211
  function bindParams(params) {
374
212
  if (params === void 0) return [];
375
213
  if (Array.isArray(params)) return params;
376
214
  return [params];
377
215
  }
378
- function query(db, sql, params) {
216
+ async function query(conn, sql, params) {
379
217
  try {
380
- const stmt = getStatement(db, sql);
381
- return stmt.all(...bindParams(params));
218
+ const stmt = await getStatement(conn, sql);
219
+ return await stmt.all(...bindParams(params));
382
220
  } catch (err) {
383
221
  throw new QueryError(err instanceof Error ? err.message : String(err), sql);
384
222
  }
385
223
  }
386
- function queryOne(db, sql, params) {
224
+ async function queryOne(conn, sql, params) {
387
225
  try {
388
- const stmt = getStatement(db, sql);
389
- return stmt.get(...bindParams(params));
226
+ const stmt = await getStatement(conn, sql);
227
+ return await stmt.get(...bindParams(params));
390
228
  } catch (err) {
391
229
  throw new QueryError(err instanceof Error ? err.message : String(err), sql);
392
230
  }
393
231
  }
394
- function execute(db, sql, params) {
232
+ async function execute(conn, sql, params) {
395
233
  try {
396
- const stmt = getStatement(db, sql);
397
- const result = stmt.run(...bindParams(params));
234
+ const stmt = await getStatement(conn, sql);
235
+ const result = await stmt.run(...bindParams(params));
398
236
  return {
399
237
  changes: result.changes,
400
- lastInsertRowId: result.lastInsertRowid
238
+ lastInsertRowId: result.lastInsertRowId
401
239
  };
402
240
  } catch (err) {
403
241
  throw new QueryError(err instanceof Error ? err.message : String(err), sql);
404
242
  }
405
243
  }
406
- function executeBatch(db, sql, paramsBatch) {
244
+ async function executeBatch(conn, sql, paramsBatch) {
407
245
  try {
408
- const stmt = getStatement(db, sql);
409
- const run = db.transaction(() => {
410
- const results = [];
411
- for (const params of paramsBatch) {
412
- const result = stmt.run(...bindParams(params));
413
- results.push({
414
- changes: result.changes,
415
- lastInsertRowId: result.lastInsertRowid
416
- });
417
- }
418
- return results;
419
- });
420
- return run();
246
+ const stmt = await getStatement(conn, sql);
247
+ const results = [];
248
+ for (const params of paramsBatch) {
249
+ const result = await stmt.run(...bindParams(params));
250
+ results.push({
251
+ changes: result.changes,
252
+ lastInsertRowId: result.lastInsertRowId
253
+ });
254
+ }
255
+ return results;
421
256
  } catch (err) {
422
257
  throw new QueryError(err instanceof Error ? err.message : String(err), sql);
423
258
  }
@@ -425,20 +260,20 @@ function executeBatch(db, sql, paramsBatch) {
425
260
 
426
261
  // src/core/transaction.ts
427
262
  var Transaction = class _Transaction {
428
- constructor(db) {
429
- this.db = db;
263
+ constructor(conn) {
264
+ this.conn = conn;
430
265
  }
431
266
  _lastInsertRowId = 0;
432
- query(sql, params) {
433
- return query(this.db, sql, params);
267
+ async query(sql, params) {
268
+ return query(this.conn, sql, params);
434
269
  }
435
- execute(sql, params) {
436
- const result = execute(this.db, sql, params);
270
+ async execute(sql, params) {
271
+ const result = await execute(this.conn, sql, params);
437
272
  this._lastInsertRowId = result.lastInsertRowId;
438
273
  return result;
439
274
  }
440
- executeBatch(sql, paramsBatch) {
441
- const results = executeBatch(this.db, sql, paramsBatch);
275
+ async executeBatch(sql, paramsBatch) {
276
+ const results = await executeBatch(this.conn, sql, paramsBatch);
442
277
  if (results.length > 0) {
443
278
  this._lastInsertRowId = results[results.length - 1].lastInsertRowId;
444
279
  }
@@ -447,124 +282,13 @@ var Transaction = class _Transaction {
447
282
  get lastInsertRowId() {
448
283
  return this._lastInsertRowId;
449
284
  }
450
- static run(db, fn) {
451
- const tx = new _Transaction(db);
452
- return db.transaction(() => fn(tx))();
453
- }
454
- };
455
- var MIGRATION_FILENAME_PATTERN = /^(\d+)_(\w+)\.(up|down)\.sql$/;
456
- function scanDirectory(dirPath) {
457
- if (dirPath.includes("\0")) {
458
- throw new MigrationError("Migration path contains null bytes", 0, "MIGRATION_VALIDATION_ERROR");
459
- }
460
- const segments = dirPath.split(/[/\\]/);
461
- if (segments.includes("..")) {
462
- throw new MigrationError(
463
- "Migration path must not contain directory traversal segments",
464
- 0,
465
- "MIGRATION_VALIDATION_ERROR"
466
- );
467
- }
468
- const resolvedPath = resolve(dirPath);
469
- let stat;
470
- try {
471
- stat = statSync(resolvedPath);
472
- } catch {
473
- throw new MigrationError(`Migrations path does not exist: ${resolvedPath}`, 0);
474
- }
475
- if (!stat.isDirectory()) {
476
- throw new MigrationError(`Migrations path is not a directory: ${resolvedPath}`, 0);
477
- }
478
- const entries = readdirSync(resolvedPath, { withFileTypes: true });
479
- const grouped = /* @__PURE__ */ new Map();
480
- for (const entry of entries) {
481
- if (!entry.isFile()) continue;
482
- const match = MIGRATION_FILENAME_PATTERN.exec(entry.name);
483
- if (!match) continue;
484
- const version = parseInt(match[1], 10);
485
- const name = match[2];
486
- const direction = match[3];
487
- if (!Number.isFinite(version) || version <= 0 || !Number.isSafeInteger(version)) {
488
- throw new MigrationError(`Invalid migration version: ${match[1]}`, version, "MIGRATION_VALIDATION_ERROR");
489
- }
490
- const existing = grouped.get(version);
491
- if (existing && existing.name !== name) {
492
- throw new MigrationError(
493
- `Duplicate migration version ${version}: '${existing.name}' and '${name}'`,
494
- version,
495
- "MIGRATION_DUPLICATE_VERSION"
496
- );
497
- }
498
- const filePath = join(resolvedPath, entry.name);
499
- if (!existing) {
500
- grouped.set(version, {
501
- name,
502
- [direction === "up" ? "upPath" : "downPath"]: filePath
503
- });
504
- } else {
505
- if (direction === "up") existing.upPath = filePath;
506
- else existing.downPath = filePath;
507
- }
508
- }
509
- const results = [];
510
- for (const [version, entry] of grouped) {
511
- if (!entry.upPath) {
512
- throw new MigrationError(
513
- `Migration version ${version} (${entry.name}) is missing an .up.sql file`,
514
- version,
515
- "MIGRATION_VALIDATION_ERROR"
516
- );
517
- }
518
- results.push({
519
- version,
520
- name: entry.name,
521
- upPath: entry.upPath,
522
- downPath: entry.downPath ?? null
285
+ static async run(conn, fn) {
286
+ return conn.transaction(async (txConn) => {
287
+ const tx = new _Transaction(txConn);
288
+ return fn(tx);
523
289
  });
524
290
  }
525
- results.sort((a, b) => a.version - b.version);
526
- return results;
527
- }
528
- function readUpMigrations(scanned) {
529
- return scanned.map((entry) => {
530
- const sql = readFileSync(entry.upPath, "utf-8").trim();
531
- if (sql.length === 0) {
532
- throw new MigrationError(`Migration file is empty: ${entry.upPath}`, entry.version, "MIGRATION_VALIDATION_ERROR");
533
- }
534
- return {
535
- version: entry.version,
536
- name: entry.name,
537
- up: sql
538
- };
539
- });
540
- }
541
- function readDownMigrations(scanned, versions) {
542
- const versionSet = new Set(versions);
543
- const filtered = scanned.filter((s) => versionSet.has(s.version));
544
- return filtered.map((entry) => {
545
- if (!entry.downPath) {
546
- throw new MigrationError(
547
- `Migration version ${entry.version} (${entry.name}) has no .down.sql file`,
548
- entry.version,
549
- "MIGRATION_NO_DOWN"
550
- );
551
- }
552
- const downSql = readFileSync(entry.downPath, "utf-8").trim();
553
- if (downSql.length === 0) {
554
- throw new MigrationError(
555
- `Down migration file is empty: ${entry.downPath}`,
556
- entry.version,
557
- "MIGRATION_VALIDATION_ERROR"
558
- );
559
- }
560
- return {
561
- version: entry.version,
562
- name: entry.name,
563
- up: "",
564
- down: downSql
565
- };
566
- });
567
- }
291
+ };
568
292
 
569
293
  // src/core/migrations/runner.ts
570
294
  var CREATE_TRACKING_TABLE = `
@@ -575,23 +299,24 @@ var CREATE_TRACKING_TABLE = `
575
299
  )
576
300
  `;
577
301
  var MigrationRunner = class _MigrationRunner {
578
- static run(db, input) {
579
- db.exec(CREATE_TRACKING_TABLE);
580
- const migrations = typeof input === "string" ? readUpMigrations(scanDirectory(input)) : _MigrationRunner.validateMigrations(input);
581
- const applied = _MigrationRunner.getAppliedVersions(db);
582
- const pending = migrations.filter((m) => !applied.has(m.version));
302
+ static async run(conn, migrations) {
303
+ await conn.exec(CREATE_TRACKING_TABLE);
304
+ const validated = _MigrationRunner.validateMigrations(migrations);
305
+ const applied = await _MigrationRunner.getAppliedVersions(conn);
306
+ const pending = validated.filter((m) => !applied.has(m.version));
583
307
  if (pending.length === 0) {
584
- return { applied: [], skipped: migrations.length };
308
+ return { applied: [], skipped: validated.length };
585
309
  }
586
- const insertMigration = db.prepare("INSERT INTO _sirannon_migrations (version, name) VALUES (?, ?)");
587
310
  const appliedEntries = [];
588
- db.transaction(() => {
311
+ await conn.transaction(async (txConn) => {
312
+ const insertStmt = await txConn.prepare("INSERT INTO _sirannon_migrations (version, name) VALUES (?, ?)");
589
313
  for (const migration of pending) {
590
314
  try {
591
315
  if (typeof migration.up === "string") {
592
- db.exec(migration.up);
316
+ await txConn.exec(migration.up);
593
317
  } else {
594
- migration.up(new Transaction(db));
318
+ const result = migration.up(new Transaction(txConn));
319
+ if (result instanceof Promise) await result;
595
320
  }
596
321
  } catch (err) {
597
322
  if (err instanceof MigrationError) throw err;
@@ -600,16 +325,16 @@ var MigrationRunner = class _MigrationRunner {
600
325
  migration.version
601
326
  );
602
327
  }
603
- insertMigration.run(migration.version, migration.name);
328
+ await insertStmt.run(migration.version, migration.name);
604
329
  appliedEntries.push({ version: migration.version, name: migration.name });
605
330
  }
606
- })();
331
+ });
607
332
  return {
608
333
  applied: appliedEntries,
609
- skipped: migrations.length - pending.length
334
+ skipped: validated.length - pending.length
610
335
  };
611
336
  }
612
- static rollback(db, input, version) {
337
+ static async rollback(conn, migrations, version) {
613
338
  if (version !== void 0 && (!Number.isSafeInteger(version) || version < 0)) {
614
339
  throw new MigrationError(
615
340
  `Invalid rollback target version: ${version}`,
@@ -617,8 +342,9 @@ var MigrationRunner = class _MigrationRunner {
617
342
  "MIGRATION_VALIDATION_ERROR"
618
343
  );
619
344
  }
620
- db.exec(CREATE_TRACKING_TABLE);
621
- const appliedRows = db.prepare("SELECT version, name FROM _sirannon_migrations ORDER BY version DESC").all();
345
+ await conn.exec(CREATE_TRACKING_TABLE);
346
+ const selectStmt = await conn.prepare("SELECT version, name FROM _sirannon_migrations ORDER BY version DESC");
347
+ const appliedRows = await selectStmt.all();
622
348
  if (appliedRows.length === 0) {
623
349
  return { rolledBack: [] };
624
350
  }
@@ -631,26 +357,20 @@ var MigrationRunner = class _MigrationRunner {
631
357
  if (rollbackSet.length === 0) {
632
358
  return { rolledBack: [] };
633
359
  }
360
+ _MigrationRunner.validateMigrations(migrations);
634
361
  const rollbackVersions = rollbackSet.map((r) => r.version);
635
- let downByVersion;
636
- if (typeof input === "string") {
637
- const scanned = scanDirectory(input);
638
- const downMigrations = readDownMigrations(scanned, rollbackVersions);
639
- downByVersion = new Map(downMigrations.map((m) => [m.version, m]));
640
- } else {
641
- const inputByVersion = new Map(input.map((m) => [m.version, m]));
642
- downByVersion = /* @__PURE__ */ new Map();
643
- for (const v of rollbackVersions) {
644
- const m = inputByVersion.get(v);
645
- if (!m || m.down === void 0) {
646
- throw new MigrationError(`Migration version ${v} has no down migration`, v, "MIGRATION_NO_DOWN");
647
- }
648
- downByVersion.set(v, m);
362
+ const inputByVersion = new Map(migrations.map((m) => [m.version, m]));
363
+ const downByVersion = /* @__PURE__ */ new Map();
364
+ for (const v of rollbackVersions) {
365
+ const m = inputByVersion.get(v);
366
+ if (!m || m.down === void 0) {
367
+ throw new MigrationError(`Migration version ${v} has no down migration`, v, "MIGRATION_NO_DOWN");
649
368
  }
369
+ downByVersion.set(v, m);
650
370
  }
651
- const deleteMigration = db.prepare("DELETE FROM _sirannon_migrations WHERE version = ?");
652
371
  const rolledBackEntries = [];
653
- db.transaction(() => {
372
+ await conn.transaction(async (txConn) => {
373
+ const deleteStmt = await txConn.prepare("DELETE FROM _sirannon_migrations WHERE version = ?");
654
374
  for (const entry of rollbackSet) {
655
375
  const migration = downByVersion.get(entry.version);
656
376
  if (!migration) {
@@ -662,9 +382,10 @@ var MigrationRunner = class _MigrationRunner {
662
382
  }
663
383
  try {
664
384
  if (typeof migration.down === "string") {
665
- db.exec(migration.down);
385
+ await txConn.exec(migration.down);
666
386
  } else {
667
- migration.down?.(new Transaction(db));
387
+ const result = migration.down?.(new Transaction(txConn));
388
+ if (result instanceof Promise) await result;
668
389
  }
669
390
  } catch (err) {
670
391
  if (err instanceof MigrationError) throw err;
@@ -674,10 +395,10 @@ var MigrationRunner = class _MigrationRunner {
674
395
  "MIGRATION_ROLLBACK_ERROR"
675
396
  );
676
397
  }
677
- deleteMigration.run(entry.version);
398
+ await deleteStmt.run(entry.version);
678
399
  rolledBackEntries.push({ version: entry.version, name: entry.name });
679
400
  }
680
- })();
401
+ });
681
402
  return { rolledBack: rolledBackEntries };
682
403
  }
683
404
  static validateMigrations(migrations) {
@@ -719,18 +440,20 @@ var MigrationRunner = class _MigrationRunner {
719
440
  }
720
441
  return [...migrations].sort((a, b) => a.version - b.version);
721
442
  }
722
- static getAppliedVersions(db) {
723
- const rows = db.prepare("SELECT version FROM _sirannon_migrations ORDER BY version").all();
443
+ static async getAppliedVersions(conn) {
444
+ const stmt = await conn.prepare("SELECT version FROM _sirannon_migrations ORDER BY version");
445
+ const rows = await stmt.all();
724
446
  return new Set(rows.map((r) => r.version));
725
447
  }
726
448
  };
727
449
 
728
450
  // src/core/database.ts
729
- var Database2 = class {
451
+ var Database = class _Database {
730
452
  id;
731
453
  path;
732
454
  readOnly;
733
455
  pool;
456
+ driver;
734
457
  closeListeners = [];
735
458
  _closed = false;
736
459
  changeTracker = null;
@@ -744,109 +467,119 @@ var Database2 = class {
744
467
  backupManager = new BackupManager();
745
468
  backupScheduler = new BackupScheduler(this.backupManager);
746
469
  scheduledBackupCancellers = [];
747
- constructor(id, path, options, internals) {
470
+ constructor(id, path, pool, driver, options, internals) {
748
471
  this.id = id;
749
472
  this.path = path;
473
+ this.pool = pool;
474
+ this.driver = driver;
750
475
  this.readOnly = options?.readOnly ?? false;
751
476
  this.cdcPollInterval = options?.cdcPollInterval ?? 50;
752
477
  this.cdcRetention = options?.cdcRetention ?? 36e5;
753
478
  this.parentHooks = internals?.parentHooks ?? null;
754
479
  this.metricsCollector = internals?.metrics ?? null;
755
- this.pool = new ConnectionPool({
480
+ }
481
+ static async create(id, path, driver, options, internals) {
482
+ const pool = await ConnectionPool.create({
483
+ driver,
756
484
  path,
757
- readOnly: this.readOnly,
485
+ readOnly: options?.readOnly,
758
486
  readPoolSize: options?.readPoolSize ?? 4,
759
487
  walMode: options?.walMode ?? true
760
488
  });
489
+ return new _Database(id, path, pool, driver, options, internals);
761
490
  }
762
- query(sql, params) {
491
+ async query(sql, params) {
763
492
  this.ensureOpen();
764
493
  this.fireBeforeQueryHooks(sql, params);
765
494
  const start = performance.now();
766
495
  try {
767
496
  const reader = this.pool.acquireReader();
768
497
  if (this.metricsCollector) {
769
- return this.metricsCollector.trackQuery(() => query(reader, sql, params), {
498
+ return await this.metricsCollector.trackQuery(() => query(reader, sql, params), {
770
499
  databaseId: this.id,
771
500
  sql
772
501
  });
773
502
  }
774
- return query(reader, sql, params);
503
+ return await query(reader, sql, params);
775
504
  } finally {
776
505
  this.fireAfterQueryHooks(sql, params, performance.now() - start);
777
506
  }
778
507
  }
779
- queryOne(sql, params) {
508
+ async queryOne(sql, params) {
780
509
  this.ensureOpen();
781
510
  this.fireBeforeQueryHooks(sql, params);
782
511
  const start = performance.now();
783
512
  try {
784
513
  const reader = this.pool.acquireReader();
785
514
  if (this.metricsCollector) {
786
- return this.metricsCollector.trackQuery(() => queryOne(reader, sql, params), {
515
+ return await this.metricsCollector.trackQuery(() => queryOne(reader, sql, params), {
787
516
  databaseId: this.id,
788
517
  sql
789
518
  });
790
519
  }
791
- return queryOne(reader, sql, params);
520
+ return await queryOne(reader, sql, params);
792
521
  } finally {
793
522
  this.fireAfterQueryHooks(sql, params, performance.now() - start);
794
523
  }
795
524
  }
796
- execute(sql, params) {
525
+ async execute(sql, params) {
797
526
  this.ensureOpen();
527
+ if (this.readOnly) throw new ReadOnlyError(this.id);
798
528
  this.fireBeforeQueryHooks(sql, params);
799
529
  const start = performance.now();
800
530
  try {
801
531
  const writer = this.pool.acquireWriter();
802
532
  if (this.metricsCollector) {
803
- return this.metricsCollector.trackQuery(() => execute(writer, sql, params), {
533
+ return await this.metricsCollector.trackQuery(() => execute(writer, sql, params), {
804
534
  databaseId: this.id,
805
535
  sql
806
536
  });
807
537
  }
808
- return execute(writer, sql, params);
538
+ return await execute(writer, sql, params);
809
539
  } finally {
810
540
  this.fireAfterQueryHooks(sql, params, performance.now() - start);
811
541
  }
812
542
  }
813
- executeBatch(sql, paramsBatch) {
543
+ async executeBatch(sql, paramsBatch) {
814
544
  this.ensureOpen();
545
+ if (this.readOnly) throw new ReadOnlyError(this.id);
815
546
  this.fireBeforeQueryHooks(sql);
816
547
  const start = performance.now();
817
548
  try {
818
549
  const writer = this.pool.acquireWriter();
550
+ const batchFn = () => writer.transaction(async (txConn) => executeBatch(txConn, sql, paramsBatch));
819
551
  if (this.metricsCollector) {
820
- return this.metricsCollector.trackQuery(() => executeBatch(writer, sql, paramsBatch), {
552
+ return await this.metricsCollector.trackQuery(batchFn, {
821
553
  databaseId: this.id,
822
554
  sql
823
555
  });
824
556
  }
825
- return executeBatch(writer, sql, paramsBatch);
557
+ return await batchFn();
826
558
  } finally {
827
559
  this.fireAfterQueryHooks(sql, void 0, performance.now() - start);
828
560
  }
829
561
  }
830
- transaction(fn) {
562
+ async transaction(fn) {
831
563
  this.ensureOpen();
564
+ if (this.readOnly) throw new ReadOnlyError(this.id);
832
565
  const writer = this.pool.acquireWriter();
833
566
  return Transaction.run(writer, fn);
834
567
  }
835
- watch(table) {
568
+ async watch(table) {
836
569
  this.ensureOpen();
837
570
  if (this.readOnly) {
838
571
  throw new ReadOnlyError(this.id);
839
572
  }
840
573
  this.ensureCdc();
841
574
  const writer = this.pool.acquireWriter();
842
- this.changeTracker?.watch(writer, table);
575
+ await this.changeTracker?.watch(writer, table);
843
576
  this.ensureCdcPolling();
844
577
  }
845
- unwatch(table) {
578
+ async unwatch(table) {
846
579
  this.ensureOpen();
847
580
  if (!this.changeTracker) return;
848
581
  const writer = this.pool.acquireWriter();
849
- this.changeTracker.unwatch(writer, table);
582
+ await this.changeTracker.unwatch(writer, table);
850
583
  if (this.changeTracker.watchedTables.size === 0) {
851
584
  this.stopCdcPollingLoop();
852
585
  }
@@ -858,20 +591,20 @@ var Database2 = class {
858
591
  if (!manager) throw new Error("subscriptionManager not initialized");
859
592
  return new SubscriptionBuilderImpl(table, manager);
860
593
  }
861
- migrate(input) {
594
+ async migrate(migrations) {
862
595
  this.ensureOpen();
863
596
  const writer = this.pool.acquireWriter();
864
- return MigrationRunner.run(writer, input);
597
+ return MigrationRunner.run(writer, migrations);
865
598
  }
866
- rollback(input, version) {
599
+ async rollback(migrations, version) {
867
600
  this.ensureOpen();
868
601
  const writer = this.pool.acquireWriter();
869
- return MigrationRunner.rollback(writer, input, version);
602
+ return MigrationRunner.rollback(writer, migrations, version);
870
603
  }
871
- backup(destPath) {
604
+ async backup(destPath) {
872
605
  this.ensureOpen();
873
606
  const writer = this.pool.acquireWriter();
874
- this.backupManager.backup(writer, destPath);
607
+ await this.backupManager.backup(writer, destPath);
875
608
  }
876
609
  scheduleBackup(options) {
877
610
  this.ensureOpen();
@@ -879,18 +612,29 @@ var Database2 = class {
879
612
  const cancel = this.backupScheduler.schedule(writer, options);
880
613
  this.scheduledBackupCancellers.push(cancel);
881
614
  }
882
- loadExtension(extensionPath) {
615
+ async loadExtension(extensionPath) {
883
616
  this.ensureOpen();
617
+ if (!this.driver.capabilities.extensions) {
618
+ throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
619
+ }
884
620
  if (!extensionPath || extensionPath.includes("\0")) {
885
621
  throw new ExtensionError(extensionPath || "", "Extension path is empty or contains null bytes");
886
622
  }
623
+ for (let i = 0; i < extensionPath.length; i++) {
624
+ if (extensionPath.charCodeAt(i) <= 31) {
625
+ throw new ExtensionError(extensionPath, "Extension path contains control characters");
626
+ }
627
+ }
887
628
  const segments = extensionPath.split(/[/\\]/);
888
629
  if (segments.includes("..")) {
889
630
  throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
890
631
  }
632
+ const { resolve } = await import('path');
891
633
  const resolved = resolve(extensionPath);
892
634
  try {
893
- this.pool.loadExtension(resolved);
635
+ const writer = this.pool.acquireWriter();
636
+ const escaped = resolved.replace(/'/g, "''");
637
+ await writer.exec(`SELECT load_extension('${escaped}')`);
894
638
  } catch (err) {
895
639
  throw new ExtensionError(extensionPath, err instanceof Error ? err.message : String(err));
896
640
  }
@@ -905,7 +649,7 @@ var Database2 = class {
905
649
  this.ensureOpen();
906
650
  this.closeListeners.push(fn);
907
651
  }
908
- close() {
652
+ async close() {
909
653
  if (this._closed) return;
910
654
  this._closed = true;
911
655
  this.stopCdcPollingLoop();
@@ -918,13 +662,13 @@ var Database2 = class {
918
662
  this.scheduledBackupCancellers.length = 0;
919
663
  let poolError;
920
664
  try {
921
- this.pool.close();
665
+ await this.pool.close();
922
666
  } catch (err) {
923
667
  poolError = err;
924
668
  }
925
669
  for (const fn of this.closeListeners) {
926
670
  try {
927
- fn();
671
+ await fn();
928
672
  } catch {
929
673
  }
930
674
  }
@@ -994,27 +738,22 @@ var LifecycleManager = class {
994
738
  lastAccess = /* @__PURE__ */ new Map();
995
739
  idleTimer = null;
996
740
  _disposed = false;
741
+ #idleCheckPromise = null;
997
742
  constructor(config, callbacks) {
998
743
  this.config = config;
999
744
  this.callbacks = callbacks;
1000
745
  const timeout = config.idleTimeout;
1001
746
  if (timeout && timeout > 0) {
1002
747
  const interval = Math.min(Math.max(Math.floor(timeout / 2), 100), 6e4);
1003
- this.idleTimer = setInterval(() => this.checkIdle(), interval);
748
+ this.idleTimer = setInterval(async () => {
749
+ await this.#runIdleCheck();
750
+ }, interval);
1004
751
  if (typeof this.idleTimer === "object" && "unref" in this.idleTimer) {
1005
752
  this.idleTimer.unref();
1006
753
  }
1007
754
  }
1008
755
  }
1009
- /**
1010
- * Attempt to auto-open a database by ID using the configured resolver.
1011
- * Returns the opened Database, or `undefined` when no resolver is
1012
- * configured or the resolver does not recognise the ID.
1013
- *
1014
- * Throws {@link MaxDatabasesError} when the registry is at capacity and
1015
- * eviction cannot free a slot.
1016
- */
1017
- resolve(id) {
756
+ async resolve(id) {
1018
757
  this.ensureNotDisposed();
1019
758
  const resolver = this.config.autoOpen?.resolver;
1020
759
  if (!resolver) return void 0;
@@ -1022,27 +761,21 @@ var LifecycleManager = class {
1022
761
  if (!resolved) return void 0;
1023
762
  const maxOpen = this.config.maxOpen;
1024
763
  if (maxOpen && maxOpen > 0 && this.callbacks.count() >= maxOpen) {
1025
- this.evict();
764
+ await this.evict();
1026
765
  if (this.callbacks.count() >= maxOpen) {
1027
766
  throw new MaxDatabasesError(maxOpen);
1028
767
  }
1029
768
  }
1030
- const db = this.callbacks.open(id, resolved.path, resolved.options);
769
+ const db = await this.callbacks.open(id, resolved.path, resolved.options);
1031
770
  this.markActive(id);
1032
771
  return db;
1033
772
  }
1034
- /** Record an access for the given database ID. */
1035
773
  markActive(id) {
1036
774
  if (!this._disposed) {
1037
775
  this.lastAccess.set(id, Date.now());
1038
776
  }
1039
777
  }
1040
- /**
1041
- * Close every database whose last access was longer ago than the
1042
- * configured idle timeout. Also cleans up tracking entries for
1043
- * databases that were closed externally.
1044
- */
1045
- checkIdle() {
778
+ async checkIdle() {
1046
779
  const timeout = this.config.idleTimeout;
1047
780
  if (!timeout || timeout <= 0) return;
1048
781
  const now = Date.now();
@@ -1059,7 +792,7 @@ var LifecycleManager = class {
1059
792
  }
1060
793
  for (const id of toClose) {
1061
794
  try {
1062
- this.callbacks.close(id);
795
+ await this.callbacks.close(id);
1063
796
  } catch {
1064
797
  }
1065
798
  toRemove.push(id);
@@ -1068,11 +801,7 @@ var LifecycleManager = class {
1068
801
  this.lastAccess.delete(id);
1069
802
  }
1070
803
  }
1071
- /**
1072
- * Close the least-recently-used tracked database. Called internally
1073
- * by {@link resolve} when `maxOpen` capacity is reached.
1074
- */
1075
- evict() {
804
+ async evict() {
1076
805
  let oldestId = null;
1077
806
  let oldestTime = Infinity;
1078
807
  const stale = [];
@@ -1091,25 +820,21 @@ var LifecycleManager = class {
1091
820
  }
1092
821
  if (oldestId) {
1093
822
  try {
1094
- this.callbacks.close(oldestId);
823
+ await this.callbacks.close(oldestId);
1095
824
  } catch {
1096
825
  }
1097
826
  this.lastAccess.delete(oldestId);
1098
827
  }
1099
828
  }
1100
- /** Remove a database from idle tracking (e.g. after an explicit close). */
1101
829
  untrack(id) {
1102
830
  this.lastAccess.delete(id);
1103
831
  }
1104
- /** Whether this manager has been disposed. */
1105
832
  get disposed() {
1106
833
  return this._disposed;
1107
834
  }
1108
- /** The number of databases currently tracked for idle management. */
1109
835
  get trackedCount() {
1110
836
  return this.lastAccess.size;
1111
837
  }
1112
- /** Shut down the manager: stop the idle timer and clear all state. */
1113
838
  dispose() {
1114
839
  if (this._disposed) return;
1115
840
  this._disposed = true;
@@ -1124,7 +849,22 @@ var LifecycleManager = class {
1124
849
  throw new SirannonError("LifecycleManager has been disposed", "LIFECYCLE_DISPOSED");
1125
850
  }
1126
851
  }
852
+ async #runIdleCheck() {
853
+ if (this.#idleCheckPromise) {
854
+ await this.#idleCheckPromise;
855
+ return;
856
+ }
857
+ this.#idleCheckPromise = this.checkIdle();
858
+ try {
859
+ await this.#idleCheckPromise;
860
+ } catch {
861
+ } finally {
862
+ this.#idleCheckPromise = null;
863
+ }
864
+ }
1127
865
  };
866
+
867
+ // src/core/lifecycle/tenant.ts
1128
868
  var SAFE_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1129
869
  var MAX_ID_LENGTH = 255;
1130
870
  var MAX_FILENAME_LENGTH = 255;
@@ -1142,7 +882,7 @@ function tenantPath(basePath, tenantId, extension = ".db") {
1142
882
  if (filename.length > MAX_FILENAME_LENGTH) {
1143
883
  throw new Error(`Tenant filename exceeds maximum length of ${MAX_FILENAME_LENGTH} characters`);
1144
884
  }
1145
- return join(basePath, filename);
885
+ return `${basePath}/${filename}`;
1146
886
  }
1147
887
  function createTenantResolver(options) {
1148
888
  const ext = options.extension ?? ".db";
@@ -1153,7 +893,7 @@ function createTenantResolver(options) {
1153
893
  const filename = `${sanitized}${ext}`;
1154
894
  if (filename.length > MAX_FILENAME_LENGTH) return void 0;
1155
895
  return {
1156
- path: join(options.basePath, filename),
896
+ path: `${options.basePath}/${filename}`,
1157
897
  options: defaultOpts
1158
898
  };
1159
899
  };
@@ -1165,14 +905,14 @@ var MetricsCollector = class {
1165
905
  constructor(config) {
1166
906
  this.config = config ?? {};
1167
907
  }
1168
- trackQuery(fn, context) {
908
+ async trackQuery(fn, context) {
1169
909
  if (!this.config.onQueryComplete) {
1170
910
  return fn();
1171
911
  }
1172
912
  const start = performance.now();
1173
913
  let failed = false;
1174
914
  try {
1175
- return fn();
915
+ return await fn();
1176
916
  } catch (err) {
1177
917
  failed = true;
1178
918
  throw err;
@@ -1213,9 +953,10 @@ var MetricsCollector = class {
1213
953
  var Sirannon = class {
1214
954
  constructor(options) {
1215
955
  this.options = options;
1216
- this.hookRegistry = new HookRegistry(options?.hooks);
1217
- this.metricsCollector = options?.metrics ? new MetricsCollector(options.metrics) : null;
1218
- this.lifecycleManager = options?.lifecycle ? new LifecycleManager(options.lifecycle, {
956
+ this._driver = options.driver;
957
+ this.hookRegistry = new HookRegistry(options.hooks);
958
+ this.metricsCollector = options.metrics ? new MetricsCollector(options.metrics) : null;
959
+ this.lifecycleManager = options.lifecycle ? new LifecycleManager(options.lifecycle, {
1219
960
  open: (id, path, opts) => this.open(id, path, opts),
1220
961
  close: (id) => this.close(id),
1221
962
  count: () => this.dbs.size,
@@ -1223,31 +964,44 @@ var Sirannon = class {
1223
964
  }) : null;
1224
965
  }
1225
966
  dbs = /* @__PURE__ */ new Map();
967
+ opening = /* @__PURE__ */ new Set();
1226
968
  _shutdown = false;
969
+ _driver;
1227
970
  hookRegistry;
1228
971
  metricsCollector;
1229
972
  lifecycleManager;
1230
- open(id, path, options) {
973
+ get driver() {
974
+ return this._driver;
975
+ }
976
+ async open(id, path, options) {
1231
977
  this.ensureRunning();
1232
- if (this.dbs.has(id)) {
978
+ if (this.dbs.has(id) || this.opening.has(id)) {
1233
979
  throw new DatabaseAlreadyExistsError(id);
1234
980
  }
1235
- if (this.hookRegistry.has("beforeConnect")) {
1236
- this.hookRegistry.invokeSync("beforeConnect", { databaseId: id, path });
1237
- }
981
+ this.opening.add(id);
1238
982
  let db;
1239
983
  try {
1240
- db = new Database2(id, path, options, {
984
+ if (this.hookRegistry.has("beforeConnect")) {
985
+ this.hookRegistry.invokeSync("beforeConnect", { databaseId: id, path });
986
+ }
987
+ db = await Database.create(id, path, this._driver, options, {
1241
988
  parentHooks: this.hookRegistry,
1242
989
  metrics: this.metricsCollector ?? void 0
1243
990
  });
1244
991
  } catch (err) {
992
+ this.opening.delete(id);
1245
993
  if (err instanceof SirannonError) throw err;
1246
994
  throw new SirannonError(
1247
995
  `Failed to open database '${id}' at '${path}': ${err instanceof Error ? err.message : String(err)}`,
1248
996
  "DATABASE_OPEN_FAILED"
1249
997
  );
1250
998
  }
999
+ this.opening.delete(id);
1000
+ if (this._shutdown) {
1001
+ await db.close().catch(() => {
1002
+ });
1003
+ throw new SirannonError("Sirannon has been shut down", "SHUTDOWN");
1004
+ }
1251
1005
  db.addCloseListener(() => {
1252
1006
  this.dbs.delete(id);
1253
1007
  this.lifecycleManager?.untrack(id);
@@ -1280,13 +1034,13 @@ var Sirannon = class {
1280
1034
  });
1281
1035
  return db;
1282
1036
  }
1283
- close(id) {
1037
+ async close(id) {
1284
1038
  this.ensureRunning();
1285
1039
  const db = this.dbs.get(id);
1286
1040
  if (!db) {
1287
1041
  throw new DatabaseNotFoundError(id);
1288
1042
  }
1289
- db.close();
1043
+ await db.close();
1290
1044
  }
1291
1045
  get(id) {
1292
1046
  const db = this.dbs.get(id);
@@ -1295,6 +1049,12 @@ var Sirannon = class {
1295
1049
  return db;
1296
1050
  }
1297
1051
  if (this._shutdown) return void 0;
1052
+ return void 0;
1053
+ }
1054
+ async resolve(id) {
1055
+ const db = this.get(id);
1056
+ if (db) return db;
1057
+ if (this._shutdown) return void 0;
1298
1058
  return this.lifecycleManager?.resolve(id);
1299
1059
  }
1300
1060
  has(id) {
@@ -1303,7 +1063,7 @@ var Sirannon = class {
1303
1063
  databases() {
1304
1064
  return new Map(this.dbs);
1305
1065
  }
1306
- shutdown() {
1066
+ async shutdown() {
1307
1067
  if (this._shutdown) return;
1308
1068
  this._shutdown = true;
1309
1069
  this.lifecycleManager?.dispose();
@@ -1311,7 +1071,7 @@ var Sirannon = class {
1311
1071
  const snapshot = [...this.dbs.values()];
1312
1072
  for (const db of snapshot) {
1313
1073
  try {
1314
- db.close();
1074
+ await db.close();
1315
1075
  } catch (err) {
1316
1076
  errors.push(err);
1317
1077
  }
@@ -1343,4 +1103,4 @@ var Sirannon = class {
1343
1103
  }
1344
1104
  };
1345
1105
 
1346
- export { BackupManager, BackupScheduler, ConnectionPool, Database2 as Database, HookRegistry, LifecycleManager, MetricsCollector, MigrationRunner, Sirannon, Transaction, createTenantResolver, execute, executeBatch, query, queryOne, sanitizeTenantId, tenantPath };
1106
+ export { ConnectionPool, Database, HookRegistry, LifecycleManager, MetricsCollector, MigrationRunner, Sirannon, Transaction, createTenantResolver, execute, executeBatch, query, queryOne, sanitizeTenantId, tenantPath };