@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
@@ -1,13 +1,15 @@
1
- import { SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-UTO3ZAFS.mjs';
2
- export { ChangeTracker } from '../chunk-UTO3ZAFS.mjs';
3
- export { defineDriver } from '../chunk-74UN4DIE.mjs';
4
- import { BackupManager, BackupScheduler } from '../chunk-PXKAKK2V.mjs';
5
- export { BackupManager, BackupScheduler } from '../chunk-PXKAKK2V.mjs';
6
- import { Transaction, query, queryOne, execute, executeBatch } from '../chunk-3MCMONVP.mjs';
7
- export { Transaction, execute, executeBatch, query, queryOne } from '../chunk-3MCMONVP.mjs';
8
- import '../chunk-GS7T5YMI.mjs';
9
- import { ConnectionPoolError, MigrationError, ReadOnlyError, SirannonError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError } from '../chunk-O7BHI3CF.mjs';
10
- export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError } from '../chunk-O7BHI3CF.mjs';
1
+ import { runBulkLoad, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-HHRMRFFR.mjs';
2
+ export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-HHRMRFFR.mjs';
3
+ export { defineDriver } from '../chunk-BNUTBHHH.mjs';
4
+ import { DEFAULT_SYNCHRONOUS } from '../chunk-CJLYFDP5.mjs';
5
+ export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-CJLYFDP5.mjs';
6
+ import { Transaction } from '../chunk-TKGHYWQ6.mjs';
7
+ export { Transaction } from '../chunk-TKGHYWQ6.mjs';
8
+ import { query, queryForWire, queryOne, executeBatch, executeBatchSummary, execute, bindParams, executeGroup } from '../chunk-CW6S3WL5.mjs';
9
+ export { execute, executeBatch, query, queryOne } from '../chunk-CW6S3WL5.mjs';
10
+ import { MIGRATIONS_TABLE } from '../chunk-GEZUUIKV.mjs';
11
+ import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, WriteOverloadError, ExtensionError } from '../chunk-YPYVQJ4C.mjs';
12
+ export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-YPYVQJ4C.mjs';
11
13
 
12
14
  // src/core/connection-pool.ts
13
15
  async function closeAllSilently(connections) {
@@ -29,12 +31,22 @@ var ConnectionPool = class _ConnectionPool {
29
31
  this.readers = readers;
30
32
  }
31
33
  static async create(options) {
32
- const { driver, path, readOnly = false, readPoolSize = 4, walMode = true } = options;
34
+ const { driver, path, readOnly = false, readPoolSize = 4, walMode = true, synchronous } = options;
33
35
  let writer = null;
34
36
  const readers = [];
35
37
  try {
36
38
  if (!readOnly) {
37
- writer = await driver.open(path, { walMode });
39
+ if (options.useWriterWorker) {
40
+ if (!driver.startWriterHost) {
41
+ throw new SirannonError(
42
+ "writerWorker is enabled but the driver does not carry a worker entry; use a driver that supports it or disable writerWorker",
43
+ "WRITER_WORKER_UNSUPPORTED"
44
+ );
45
+ }
46
+ writer = await driver.startWriterHost(path, { walMode, synchronous }, options.workerHostOptions);
47
+ } else {
48
+ writer = await driver.open(path, { walMode, synchronous });
49
+ }
38
50
  }
39
51
  const poolSize = driver.capabilities.multipleConnections ? Math.max(readPoolSize, 1) : 0;
40
52
  for (let i = 0; i < poolSize; i++) {
@@ -120,6 +132,48 @@ async function applyDdlSideEffects(tracker, writerConn, sql) {
120
132
  await tracker.pruneDroppedTables(writerConn, [dropped]);
121
133
  }
122
134
  }
135
+ function applyDdlSideEffectsIfRelevant(tracker, writerConn, sql) {
136
+ if (!tracker || !isCdcRelevantDdl(sql)) {
137
+ return Promise.resolve();
138
+ }
139
+ return applyDdlSideEffects(tracker, writerConn, sql);
140
+ }
141
+
142
+ // src/core/database-backup.ts
143
+ var DatabaseBackupController = class {
144
+ constructor(runExclusive, acquireWriter, engine) {
145
+ this.runExclusive = runExclusive;
146
+ this.acquireWriter = acquireWriter;
147
+ this.engine = engine;
148
+ }
149
+ cancellers = [];
150
+ require() {
151
+ if (!this.engine) {
152
+ throw new SirannonError(
153
+ "Backups need a driver that can write files; this driver does not provide one",
154
+ "BACKUP_UNSUPPORTED"
155
+ );
156
+ }
157
+ return this.engine;
158
+ }
159
+ backup(destPath) {
160
+ const engine = this.require();
161
+ return this.runExclusive(() => engine.backup(this.acquireWriter(), destPath));
162
+ }
163
+ schedule(options) {
164
+ const engine = this.require();
165
+ this.cancellers.push(engine.schedule(this.acquireWriter(), options, this.runExclusive));
166
+ }
167
+ cancelAll() {
168
+ for (const cancel of this.cancellers) {
169
+ try {
170
+ cancel();
171
+ } catch {
172
+ }
173
+ }
174
+ this.cancellers.length = 0;
175
+ }
176
+ };
123
177
 
124
178
  // src/core/cdc/cdc-aware-transaction.ts
125
179
  var CdcAwareTransaction = class extends Transaction {
@@ -148,9 +202,169 @@ var CdcAwareTransaction = class extends Transaction {
148
202
  }
149
203
  };
150
204
 
205
+ // src/core/database-cdc.ts
206
+ var DatabaseCdcController = class {
207
+ constructor(runExclusive, acquireWriter, pollInterval, retention) {
208
+ this.runExclusive = runExclusive;
209
+ this.acquireWriter = acquireWriter;
210
+ this.pollInterval = pollInterval;
211
+ this.retention = retention;
212
+ }
213
+ tracker = null;
214
+ subscriptions = null;
215
+ stopPolling = null;
216
+ get changeTracker() {
217
+ return this.tracker;
218
+ }
219
+ async watch(table) {
220
+ const { tracker } = this.ensure();
221
+ await this.runExclusive(() => tracker.watch(this.acquireWriter(), table));
222
+ this.ensurePolling();
223
+ }
224
+ async unwatch(table) {
225
+ const tracker = this.tracker;
226
+ if (!tracker) return;
227
+ await this.runExclusive(() => tracker.unwatch(this.acquireWriter(), table));
228
+ if (tracker.watchedTables.size === 0) {
229
+ this.stop();
230
+ }
231
+ }
232
+ on(table) {
233
+ const { subscriptions } = this.ensure();
234
+ return new SubscriptionBuilderImpl(table, subscriptions);
235
+ }
236
+ async runTransaction(writer, fn) {
237
+ const tracker = this.tracker;
238
+ if (!tracker) {
239
+ return Transaction.run(writer, fn);
240
+ }
241
+ const state = { sawDdl: false, droppedTables: [] };
242
+ const result = await writer.transaction((txConn) => fn(new CdcAwareTransaction(txConn, tracker, state)));
243
+ if (state.sawDdl && state.droppedTables.length > 0) {
244
+ await tracker.pruneDroppedTables(writer, state.droppedTables);
245
+ }
246
+ return result;
247
+ }
248
+ stop() {
249
+ if (this.stopPolling) {
250
+ this.stopPolling();
251
+ this.stopPolling = null;
252
+ }
253
+ }
254
+ ensure() {
255
+ const tracker = this.tracker ?? new ChangeTracker({ retention: this.retention });
256
+ const subscriptions = this.subscriptions ?? new SubscriptionManager();
257
+ this.tracker = tracker;
258
+ this.subscriptions = subscriptions;
259
+ return { tracker, subscriptions };
260
+ }
261
+ ensurePolling() {
262
+ if (this.stopPolling) return;
263
+ if (!this.tracker || !this.subscriptions) return;
264
+ this.stopPolling = startPolling(
265
+ this.acquireWriter(),
266
+ this.tracker,
267
+ this.subscriptions,
268
+ this.pollInterval,
269
+ void 0,
270
+ this.runExclusive
271
+ );
272
+ }
273
+ };
274
+
275
+ // src/core/hooks/query-hooks.ts
276
+ function fireBeforeQueryHooks(parentHooks, localHooks, databaseId, sql, params, options) {
277
+ const hasParent = parentHooks?.has("beforeQuery");
278
+ const hasLocal = localHooks.has("beforeQuery");
279
+ if (!hasParent && !hasLocal) return;
280
+ const ctx = {
281
+ databaseId,
282
+ sql,
283
+ params,
284
+ writeConcern: options?.writeConcern,
285
+ readConcern: options?.readConcern
286
+ };
287
+ parentHooks?.invokeSync("beforeQuery", ctx);
288
+ localHooks.invokeSync("beforeQuery", ctx);
289
+ }
290
+ function fireAfterQueryHooks(parentHooks, localHooks, databaseId, sql, params, durationMs) {
291
+ const hasParent = parentHooks?.has("afterQuery");
292
+ const hasLocal = localHooks.has("afterQuery");
293
+ if (!hasParent && !hasLocal) return;
294
+ const ctx = { databaseId, sql, params, durationMs };
295
+ try {
296
+ parentHooks?.invokeSync("afterQuery", ctx);
297
+ } catch {
298
+ }
299
+ try {
300
+ localHooks.invokeSync("afterQuery", ctx);
301
+ } catch {
302
+ }
303
+ }
304
+
305
+ // src/core/database-observability.ts
306
+ var DatabaseObserver = class {
307
+ constructor(databaseId, localHooks, parentHooks, metrics) {
308
+ this.databaseId = databaseId;
309
+ this.localHooks = localHooks;
310
+ this.parentHooks = parentHooks;
311
+ this.metrics = metrics;
312
+ }
313
+ /** False lets a caller skip the timing calls on a path where nothing would read them. */
314
+ get observesQueries() {
315
+ return this.metrics !== null || this.localHooks.has("beforeQuery") || this.localHooks.has("afterQuery") || (this.parentHooks?.has("beforeQuery") ?? false) || (this.parentHooks?.has("afterQuery") ?? false);
316
+ }
317
+ async withQueryHooks(sql, params, options, op) {
318
+ this.fireBefore(sql, params, options);
319
+ const start = performance.now();
320
+ try {
321
+ return await op();
322
+ } finally {
323
+ this.fireAfter(sql, params, performance.now() - start);
324
+ }
325
+ }
326
+ track(sql, op) {
327
+ if (!this.metrics) return op();
328
+ return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql });
329
+ }
330
+ /**
331
+ * Reports each statement separately, since a hook filtering on SQL has no other
332
+ * way to see them. They share the transaction's duration because the group runs
333
+ * them back to back under one commit and no statement has a duration of its own,
334
+ * which is already what a grouped {@link Database.execute} reports.
335
+ */
336
+ async withTransactionHooks(statements, op) {
337
+ for (const statement of statements) this.fireBefore(statement.sql, statement.params);
338
+ const start = performance.now();
339
+ try {
340
+ return await this.trackEach(statements, op);
341
+ } finally {
342
+ const durationMs = performance.now() - start;
343
+ for (const statement of statements) this.fireAfter(statement.sql, statement.params, durationMs);
344
+ }
345
+ }
346
+ trackEach(statements, op) {
347
+ const metrics = this.metrics;
348
+ if (!metrics) return op();
349
+ let run = op;
350
+ for (const statement of statements) {
351
+ const inner = run;
352
+ const sql = statement.sql;
353
+ run = () => metrics.trackQuery(inner, { databaseId: this.databaseId, sql });
354
+ }
355
+ return run();
356
+ }
357
+ fireBefore(sql, params, options) {
358
+ fireBeforeQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, options);
359
+ }
360
+ fireAfter(sql, params, durationMs) {
361
+ fireAfterQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, durationMs);
362
+ }
363
+ };
364
+
151
365
  // src/core/extension-loader.ts
152
366
  async function loadExtension(driver, writer, extensionPath) {
153
- if (!driver.capabilities.extensions) {
367
+ if (!driver.capabilities.extensions || !driver.resolveExtensionPath) {
154
368
  throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
155
369
  }
156
370
  if (!extensionPath || extensionPath.includes("\0")) {
@@ -165,8 +379,7 @@ async function loadExtension(driver, writer, extensionPath) {
165
379
  if (segments.includes("..")) {
166
380
  throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
167
381
  }
168
- const { resolve } = await import('path');
169
- const resolved = resolve(extensionPath);
382
+ const resolved = driver.resolveExtensionPath(extensionPath);
170
383
  try {
171
384
  const escaped = resolved.replace(/'/g, "''");
172
385
  await writer.exec(`SELECT load_extension('${escaped}')`);
@@ -175,6 +388,137 @@ async function loadExtension(driver, writer, extensionPath) {
175
388
  }
176
389
  }
177
390
 
391
+ // src/core/group-committer.ts
392
+ var DEFAULT_MAX_GROUP_SIZE = 1e3;
393
+ var DML_PREFIX_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE)\b/i;
394
+ function isGroupable(sql) {
395
+ return DML_PREFIX_RE.test(sql);
396
+ }
397
+ function canGroupTransaction(statements) {
398
+ for (const statement of statements) {
399
+ if (!isGroupable(statement.sql)) return false;
400
+ }
401
+ return true;
402
+ }
403
+ var GroupCommitter = class {
404
+ constructor(writerLock, hooks, maxGroup = DEFAULT_MAX_GROUP_SIZE) {
405
+ this.writerLock = writerLock;
406
+ this.hooks = hooks;
407
+ this.maxGroup = maxGroup;
408
+ }
409
+ pending = [];
410
+ running = false;
411
+ scheduled = false;
412
+ loopPromise = Promise.resolve();
413
+ submit(sql, params) {
414
+ return new Promise((resolve, reject) => {
415
+ this.pending.push({
416
+ statements: [{ sql, params }],
417
+ resolve: (values) => resolve(values[0]),
418
+ reject
419
+ });
420
+ this.schedule();
421
+ });
422
+ }
423
+ submitTransaction(statements) {
424
+ return new Promise((resolve, reject) => {
425
+ this.pending.push({ statements, resolve, reject });
426
+ this.schedule();
427
+ });
428
+ }
429
+ async drain() {
430
+ while (this.running || this.pending.length > 0) {
431
+ if (!this.running) this.schedule();
432
+ await this.loopPromise;
433
+ }
434
+ }
435
+ runUngrouped(sql, params) {
436
+ return this.writerLock.run(() => this.runSingle({ sql, params }));
437
+ }
438
+ schedule() {
439
+ if (this.running || this.scheduled) return;
440
+ this.scheduled = true;
441
+ queueMicrotask(() => {
442
+ this.scheduled = false;
443
+ this.writerLock.detached(() => this.runLoop());
444
+ });
445
+ }
446
+ runLoop() {
447
+ if (this.running) return;
448
+ this.running = true;
449
+ this.loopPromise = (async () => {
450
+ try {
451
+ while (this.pending.length > 0) {
452
+ await this.flush(this.takeBatch());
453
+ }
454
+ } finally {
455
+ this.running = false;
456
+ if (this.pending.length > 0) this.schedule();
457
+ }
458
+ })();
459
+ }
460
+ takeBatch() {
461
+ const pending = this.pending;
462
+ if (!canGroupTransaction(pending[0].statements)) return pending.splice(0, 1);
463
+ let n = 1;
464
+ let statements = pending[0].statements.length;
465
+ while (n < pending.length && canGroupTransaction(pending[n].statements)) {
466
+ const size = pending[n].statements.length;
467
+ if (statements + size > this.maxGroup) break;
468
+ statements += size;
469
+ n++;
470
+ }
471
+ return pending.splice(0, n);
472
+ }
473
+ async flush(batch) {
474
+ const lone = batch[0];
475
+ if (batch.length === 1 && lone.statements.length === 1) {
476
+ try {
477
+ lone.resolve([await this.writerLock.run(() => this.runSingle(lone.statements[0]))]);
478
+ } catch (err) {
479
+ lone.reject(err);
480
+ }
481
+ return;
482
+ }
483
+ let outcomes;
484
+ try {
485
+ outcomes = await this.writerLock.run(() => this.runGroup(batch));
486
+ } catch (err) {
487
+ for (const job of batch) job.reject(err);
488
+ return;
489
+ }
490
+ for (let i = 0; i < batch.length; i++) {
491
+ const outcome = outcomes[i];
492
+ if (outcome.ok) batch[i].resolve(outcome.values);
493
+ else batch[i].reject(outcome.error);
494
+ }
495
+ }
496
+ async runSingle(statement) {
497
+ const writer = this.hooks.acquireWriter();
498
+ const result = await execute(writer, statement.sql, statement.params);
499
+ await this.hooks.afterCommit(writer, statement.sql);
500
+ return result;
501
+ }
502
+ async runGroup(batch) {
503
+ const writer = this.hooks.acquireWriter();
504
+ const units = batch.map((job) => ({
505
+ statements: job.statements.map((statement) => ({ sql: statement.sql, params: bindParams(statement.params) }))
506
+ }));
507
+ if (writer.runGroup) {
508
+ const raw = await writer.runGroup(units);
509
+ return raw.map(
510
+ (outcome) => outcome.ok ? { ok: true, values: outcome.results } : { ok: false, error: toError(outcome.error) }
511
+ );
512
+ }
513
+ return executeGroup(writer, units);
514
+ }
515
+ };
516
+ function toError(error) {
517
+ const err = error.code ? new SirannonError(error.message, error.code) : new Error(error.message);
518
+ if (error.name) err.name = error.name;
519
+ return err;
520
+ }
521
+
178
522
  // src/core/hooks/registry.ts
179
523
  var HOOK_CONFIG_MAP = {
180
524
  onBeforeQuery: "beforeQuery",
@@ -259,7 +603,7 @@ var HookRegistry = class {
259
603
 
260
604
  // src/core/migrations/runner.ts
261
605
  var CREATE_TRACKING_TABLE = `
262
- CREATE TABLE IF NOT EXISTS _sirannon_migrations (
606
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
263
607
  version INTEGER PRIMARY KEY,
264
608
  name TEXT NOT NULL,
265
609
  applied_at REAL NOT NULL DEFAULT (unixepoch('subsec'))
@@ -276,7 +620,7 @@ var MigrationRunner = class _MigrationRunner {
276
620
  }
277
621
  const appliedEntries = [];
278
622
  await conn.transaction(async (txConn) => {
279
- const insertStmt = await txConn.prepare("INSERT INTO _sirannon_migrations (version, name) VALUES (?, ?)");
623
+ const insertStmt = await txConn.prepare(`INSERT INTO ${MIGRATIONS_TABLE} (version, name) VALUES (?, ?)`);
280
624
  for (const migration of pending) {
281
625
  try {
282
626
  if (typeof migration.up === "string") {
@@ -310,7 +654,7 @@ var MigrationRunner = class _MigrationRunner {
310
654
  );
311
655
  }
312
656
  await conn.exec(CREATE_TRACKING_TABLE);
313
- const selectStmt = await conn.prepare("SELECT version, name FROM _sirannon_migrations ORDER BY version DESC");
657
+ const selectStmt = await conn.prepare(`SELECT version, name FROM ${MIGRATIONS_TABLE} ORDER BY version DESC`);
314
658
  const appliedRows = await selectStmt.all();
315
659
  if (appliedRows.length === 0) {
316
660
  return { rolledBack: [] };
@@ -337,7 +681,7 @@ var MigrationRunner = class _MigrationRunner {
337
681
  }
338
682
  const rolledBackEntries = [];
339
683
  await conn.transaction(async (txConn) => {
340
- const deleteStmt = await txConn.prepare("DELETE FROM _sirannon_migrations WHERE version = ?");
684
+ const deleteStmt = await txConn.prepare(`DELETE FROM ${MIGRATIONS_TABLE} WHERE version = ?`);
341
685
  for (const entry of rollbackSet) {
342
686
  const migration = downByVersion.get(entry.version);
343
687
  if (!migration) {
@@ -408,12 +752,90 @@ var MigrationRunner = class _MigrationRunner {
408
752
  return [...migrations].sort((a, b) => a.version - b.version);
409
753
  }
410
754
  static async getAppliedVersions(conn) {
411
- const stmt = await conn.prepare("SELECT version FROM _sirannon_migrations ORDER BY version");
755
+ const stmt = await conn.prepare(`SELECT version FROM ${MIGRATIONS_TABLE} ORDER BY version`);
412
756
  const rows = await stmt.all();
413
757
  return new Set(rows.map((r) => r.version));
414
758
  }
415
759
  };
416
760
 
761
+ // src/core/worker/config.ts
762
+ var DEFAULT_MAX_PENDING_WRITES = 1024;
763
+ var DEFAULT_RETRY_AFTER_MS = 1e3;
764
+ function requireCount(value, name, min) {
765
+ if (!Number.isInteger(value) || value < min) {
766
+ throw new SirannonError(`writerWorker.${name} must be an integer >= ${min}`, "INVALID_WRITER_WORKER");
767
+ }
768
+ return value;
769
+ }
770
+ function resolveWriterWorkerConfig(option) {
771
+ if (!option) {
772
+ return { enabled: false, maxPendingWrites: 0, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host: {} };
773
+ }
774
+ const opts = option === true ? {} : option;
775
+ const host = {};
776
+ if (opts.writeTimeoutMs !== void 0) host.writeTimeoutMs = requireCount(opts.writeTimeoutMs, "writeTimeoutMs", 0);
777
+ if (opts.maxRestarts !== void 0) host.maxRestarts = requireCount(opts.maxRestarts, "maxRestarts", 0);
778
+ const maxPendingWrites = opts.maxPendingWrites === void 0 ? DEFAULT_MAX_PENDING_WRITES : requireCount(opts.maxPendingWrites, "maxPendingWrites", 1);
779
+ return { enabled: true, maxPendingWrites, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host };
780
+ }
781
+
782
+ // src/core/worker/gate.ts
783
+ var WriteGate = class {
784
+ constructor(limit, retryAfterMs) {
785
+ this.limit = limit;
786
+ this.retryAfterMs = retryAfterMs;
787
+ }
788
+ inFlight = 0;
789
+ run(op) {
790
+ if (this.limit > 0 && this.inFlight >= this.limit) {
791
+ return Promise.reject(new WriteOverloadError(this.limit, this.retryAfterMs));
792
+ }
793
+ this.inFlight++;
794
+ return op().finally(() => {
795
+ this.inFlight--;
796
+ });
797
+ }
798
+ };
799
+
800
+ // src/core/writer-lock.ts
801
+ var UNTRACKED = {
802
+ run: (operation) => operation(),
803
+ isActive: () => false,
804
+ exit: (operation) => operation()
805
+ };
806
+ var WriterLock = class {
807
+ tail = Promise.resolve();
808
+ context;
809
+ constructor(context) {
810
+ this.context = context ?? UNTRACKED;
811
+ }
812
+ run(operation) {
813
+ if (this.context.isActive()) {
814
+ return operation();
815
+ }
816
+ const enter = () => this.context.run(operation);
817
+ const ticket = this.tail.then(enter, enter);
818
+ this.tail = ticket.then(swallow, swallow);
819
+ return ticket;
820
+ }
821
+ isHeld() {
822
+ return this.context.isActive();
823
+ }
824
+ async settle() {
825
+ await this.run(() => Promise.resolve());
826
+ }
827
+ /**
828
+ * Work scheduled from inside a held operation inherits it, so shared
829
+ * machinery must detach or it runs every caller's work inside one caller's
830
+ * transaction.
831
+ */
832
+ detached(operation) {
833
+ return this.context.exit(operation);
834
+ }
835
+ };
836
+ function swallow() {
837
+ }
838
+
417
839
  // src/core/database.ts
418
840
  var Database = class _Database {
419
841
  id;
@@ -421,184 +843,252 @@ var Database = class _Database {
421
843
  readOnly;
422
844
  pool;
423
845
  driver;
846
+ synchronous;
847
+ walMode;
848
+ writerLock;
849
+ writeGate;
850
+ groupCommitter;
424
851
  closeListeners = [];
425
852
  _closed = false;
426
- changeTracker = null;
427
- subscriptionManager = null;
428
- stopCdcPolling = null;
429
- cdcPollInterval;
430
- cdcRetention;
853
+ cdc;
431
854
  hookRegistry = new HookRegistry();
432
- parentHooks;
433
- metricsCollector;
434
- backupManager = new BackupManager();
435
- backupScheduler = new BackupScheduler(this.backupManager);
436
- scheduledBackupCancellers = [];
437
- constructor(id, path, pool, driver, options, internals) {
855
+ observer;
856
+ backups;
857
+ constructor(id, path, pool, driver, writeGate, options, internals) {
438
858
  this.id = id;
439
859
  this.path = path;
440
860
  this.pool = pool;
441
861
  this.driver = driver;
862
+ this.writeGate = writeGate;
863
+ this.writerLock = new WriterLock(driver.createWriterContext?.());
864
+ this.backups = new DatabaseBackupController(
865
+ (op) => this.writerLock.run(op),
866
+ () => this.pool.acquireWriter(),
867
+ driver.createBackupEngine?.()
868
+ );
442
869
  this.readOnly = options?.readOnly ?? false;
443
- this.cdcPollInterval = options?.cdcPollInterval ?? 50;
444
- this.cdcRetention = options?.cdcRetention ?? 36e5;
445
- this.parentHooks = internals?.parentHooks ?? null;
446
- this.metricsCollector = internals?.metrics ?? null;
870
+ this.synchronous = options?.synchronous ?? DEFAULT_SYNCHRONOUS;
871
+ this.walMode = options?.walMode ?? true;
872
+ this.observer = new DatabaseObserver(
873
+ id,
874
+ this.hookRegistry,
875
+ internals?.parentHooks ?? null,
876
+ internals?.metrics ?? null
877
+ );
878
+ this.cdc = new DatabaseCdcController(
879
+ (op) => this.writerLock.run(op),
880
+ () => this.pool.acquireWriter(),
881
+ options?.cdcPollInterval ?? 50,
882
+ options?.cdcRetention ?? 36e5
883
+ );
884
+ this.groupCommitter = new GroupCommitter(this.writerLock, {
885
+ acquireWriter: () => this.pool.acquireWriter(),
886
+ afterCommit: (writer, sql) => applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql)
887
+ });
447
888
  }
448
889
  static async create(id, path, driver, options, internals) {
890
+ const writerWorker = resolveWriterWorkerConfig(options?.writerWorker);
891
+ const readOnly = options?.readOnly ?? false;
892
+ if (writerWorker.enabled && !readOnly && !driver.startWriterHost) {
893
+ throw new SirannonError(
894
+ `writerWorker is enabled for database '${id}' but the driver does not carry a worker entry; use a driver that supports worker offload or disable writerWorker`,
895
+ "WRITER_WORKER_UNSUPPORTED"
896
+ );
897
+ }
449
898
  const pool = await ConnectionPool.create({
450
899
  driver,
451
900
  path,
452
901
  readOnly: options?.readOnly,
453
902
  readPoolSize: options?.readPoolSize ?? 4,
454
- walMode: options?.walMode ?? true
903
+ walMode: options?.walMode ?? true,
904
+ synchronous: options?.synchronous,
905
+ useWriterWorker: writerWorker.enabled && !readOnly,
906
+ workerHostOptions: writerWorker.host
455
907
  });
456
- return new _Database(id, path, pool, driver, options, internals);
908
+ const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
909
+ return new _Database(id, path, pool, driver, writeGate, options, internals);
457
910
  }
458
911
  async query(sql, params, options) {
459
912
  this.ensureOpen();
460
- this.fireBeforeQueryHooks(sql, params, options);
461
- const start = performance.now();
462
- try {
463
- const reader = this.pool.acquireReader();
464
- if (this.metricsCollector) {
465
- return await this.metricsCollector.trackQuery(() => query(reader, sql, params), {
466
- databaseId: this.id,
467
- sql
468
- });
469
- }
470
- return await query(reader, sql, params);
471
- } finally {
472
- this.fireAfterQueryHooks(sql, params, performance.now() - start);
473
- }
913
+ return this.observer.withQueryHooks(
914
+ sql,
915
+ params,
916
+ options,
917
+ () => this.runRead(sql, (conn) => query(conn, sql, params))
918
+ );
919
+ }
920
+ /**
921
+ * Returns query rows already encoded for the wire (safe-range integers as
922
+ * plain numbers, larger integers and BLOBs as tagged envelopes) in a single
923
+ * pass. The server response path uses this so a read walks its values once
924
+ * rather than narrowing on the driver and re-scanning to tag.
925
+ */
926
+ async queryForWire(sql, params, options) {
927
+ this.ensureOpen();
928
+ return this.observer.withQueryHooks(
929
+ sql,
930
+ params,
931
+ options,
932
+ () => this.runRead(sql, (conn) => queryForWire(conn, sql, params))
933
+ );
474
934
  }
475
935
  async queryOne(sql, params, options) {
476
936
  this.ensureOpen();
477
- this.fireBeforeQueryHooks(sql, params, options);
478
- const start = performance.now();
479
- try {
480
- const reader = this.pool.acquireReader();
481
- if (this.metricsCollector) {
482
- return await this.metricsCollector.trackQuery(() => queryOne(reader, sql, params), {
483
- databaseId: this.id,
484
- sql
485
- });
486
- }
487
- return await queryOne(reader, sql, params);
488
- } finally {
489
- this.fireAfterQueryHooks(sql, params, performance.now() - start);
937
+ return this.observer.withQueryHooks(
938
+ sql,
939
+ params,
940
+ options,
941
+ () => this.runRead(sql, (conn) => queryOne(conn, sql, params))
942
+ );
943
+ }
944
+ /**
945
+ * On a single-connection driver the reader is the writer, so serialise the
946
+ * read through the writer lock to avoid a bulk load's uncommitted rows.
947
+ */
948
+ runRead(sql, op) {
949
+ if (this.pool.readerCount === 0) {
950
+ return this.writerLock.run(() => this.observer.track(sql, () => op(this.pool.acquireWriter())));
490
951
  }
952
+ return this.observer.track(sql, () => op(this.pool.acquireReader()));
491
953
  }
492
954
  async execute(sql, params, options) {
493
955
  this.ensureOpen();
494
956
  if (this.readOnly) throw new ReadOnlyError(this.id);
495
- this.fireBeforeQueryHooks(sql, params, options);
496
- const start = performance.now();
497
- try {
498
- const writer = this.pool.acquireWriter();
499
- const result = this.metricsCollector ? await this.metricsCollector.trackQuery(() => execute(writer, sql, params), {
500
- databaseId: this.id,
501
- sql
502
- }) : await execute(writer, sql, params);
503
- await this.maybeApplyDdlSideEffects(writer, sql);
504
- return result;
505
- } finally {
506
- this.fireAfterQueryHooks(sql, params, performance.now() - start);
507
- }
957
+ return this.observer.withQueryHooks(
958
+ sql,
959
+ params,
960
+ options,
961
+ () => this.writeGate.run(
962
+ () => this.observer.track(
963
+ sql,
964
+ () => this.writerLock.isHeld() ? this.groupCommitter.runUngrouped(sql, params) : this.groupCommitter.submit(sql, params)
965
+ )
966
+ )
967
+ );
508
968
  }
509
969
  async executeBatch(sql, paramsBatch, options) {
510
970
  this.ensureOpen();
511
971
  if (this.readOnly) throw new ReadOnlyError(this.id);
512
- this.fireBeforeQueryHooks(sql, void 0, options);
513
- const start = performance.now();
514
- try {
515
- const writer = this.pool.acquireWriter();
516
- const batchFn = () => writer.transaction(async (txConn) => executeBatch(txConn, sql, paramsBatch));
517
- const results = this.metricsCollector ? await this.metricsCollector.trackQuery(batchFn, {
518
- databaseId: this.id,
519
- sql
520
- }) : await batchFn();
521
- await this.maybeApplyDdlSideEffects(writer, sql);
522
- return results;
523
- } finally {
524
- this.fireAfterQueryHooks(sql, void 0, performance.now() - start);
525
- }
972
+ return this.observer.withQueryHooks(
973
+ sql,
974
+ void 0,
975
+ options,
976
+ () => this.writeGate.run(
977
+ () => this.writerLock.run(
978
+ () => this.runInTransaction(this.pool.acquireWriter(), sql, (txConn) => executeBatch(txConn, sql, paramsBatch))
979
+ )
980
+ )
981
+ );
982
+ }
983
+ async runInTransaction(writer, sql, run) {
984
+ const result = await this.observer.track(sql, () => writer.transaction(run));
985
+ await applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql);
986
+ return result;
526
987
  }
527
- async transaction(fn) {
988
+ /**
989
+ * Load rows with relaxed writer durability. The load holds the writer lock
990
+ * for its whole duration, so no other write commits under the relaxed level
991
+ * and no two loads race on the shared `synchronous` setting; the configured
992
+ * level is restored before this resolves, whether the load succeeds or
993
+ * fails. The load runs in one transaction, so one commit and one durability
994
+ * barrier cover the whole batch. Rows are summed rather than returned
995
+ * per-row to bound memory on large loads. Like `execute` and `transaction`,
996
+ * this writes only to the local database; under replication the server routes
997
+ * loads through the engine, not through this method.
998
+ */
999
+ async bulkLoad(sql, paramsBatch, options) {
528
1000
  this.ensureOpen();
529
1001
  if (this.readOnly) throw new ReadOnlyError(this.id);
530
- const writer = this.pool.acquireWriter();
531
- const tracker = this.changeTracker;
532
- if (!tracker) {
533
- return Transaction.run(writer, fn);
534
- }
535
- const state = { sawDdl: false, droppedTables: [] };
536
- const result = await writer.transaction(async (txConn) => {
537
- const tx = new CdcAwareTransaction(txConn, tracker, state);
538
- return fn(tx);
1002
+ return this.observer.withQueryHooks(
1003
+ sql,
1004
+ void 0,
1005
+ void 0,
1006
+ () => this.writeGate.run(
1007
+ () => this.writerLock.run(() => {
1008
+ const writer = this.pool.acquireWriter();
1009
+ return runBulkLoad({
1010
+ writer,
1011
+ configuredSynchronous: this.synchronous,
1012
+ walMode: this.walMode,
1013
+ durability: options?.durability,
1014
+ checkpoint: options?.checkpoint ?? true,
1015
+ loadRows: () => this.runInTransaction(writer, sql, (txConn) => executeBatchSummary(txConn, sql, paramsBatch))
1016
+ });
1017
+ })
1018
+ )
1019
+ );
1020
+ }
1021
+ /**
1022
+ * Takes the statements up front rather than a callback, since a group cannot wait on an
1023
+ * arbitrary caller-supplied callback without delaying every transaction beside it.
1024
+ */
1025
+ async executeTransaction(statements) {
1026
+ this.ensureOpen();
1027
+ if (this.readOnly) throw new ReadOnlyError(this.id);
1028
+ if (statements.length === 0) return [];
1029
+ const owned = statements.map((statement) => ({ sql: statement.sql, params: statement.params }));
1030
+ const run = canGroupTransaction(owned) ? () => this.writeGate.run(() => this.groupCommitter.submitTransaction(owned)) : () => this.runStatementsAlone(owned);
1031
+ if (!this.observer.observesQueries) return run();
1032
+ return this.observer.withTransactionHooks(owned, run);
1033
+ }
1034
+ runStatementsAlone(statements) {
1035
+ return this.transaction(async (tx) => {
1036
+ const results = new Array(statements.length);
1037
+ for (let i = 0; i < statements.length; i++) {
1038
+ results[i] = await tx.execute(statements[i].sql, statements[i].params);
1039
+ }
1040
+ return results;
539
1041
  });
540
- if (state.sawDdl && state.droppedTables.length > 0) {
541
- await tracker.pruneDroppedTables(writer, state.droppedTables);
542
- }
543
- return result;
544
1042
  }
545
- async maybeApplyDdlSideEffects(writer, sql) {
546
- const tracker = this.changeTracker;
547
- if (!tracker) return;
548
- if (!isCdcRelevantDdl(sql)) return;
549
- await applyDdlSideEffects(tracker, writer, sql);
1043
+ async transaction(fn) {
1044
+ this.ensureOpen();
1045
+ if (this.readOnly) throw new ReadOnlyError(this.id);
1046
+ return this.writeGate.run(() => this.writerLock.run(() => this.cdc.runTransaction(this.pool.acquireWriter(), fn)));
550
1047
  }
551
1048
  async watch(table) {
552
1049
  this.ensureOpen();
553
1050
  if (this.readOnly) {
554
1051
  throw new ReadOnlyError(this.id);
555
1052
  }
556
- this.ensureCdc();
557
- const writer = this.pool.acquireWriter();
558
- await this.changeTracker?.watch(writer, table);
559
- this.ensureCdcPolling();
1053
+ await this.cdc.watch(table);
1054
+ }
1055
+ /**
1056
+ * Runs a CDC maintenance write (change-log pruning) on the shared writer
1057
+ * under the writer lock. Serialising it with application writes keeps it
1058
+ * from becoming a second writer that contends for SQLite's single write
1059
+ * lock and stalls the event loop on `busy_timeout`.
1060
+ */
1061
+ async runCdcMaintenance(op) {
1062
+ if (this._closed) return;
1063
+ await this.writerLock.run(() => op(this.pool.acquireWriter()));
560
1064
  }
561
1065
  async unwatch(table) {
562
1066
  this.ensureOpen();
563
- if (!this.changeTracker) return;
564
- const writer = this.pool.acquireWriter();
565
- await this.changeTracker.unwatch(writer, table);
566
- if (this.changeTracker.watchedTables.size === 0) {
567
- this.stopCdcPollingLoop();
568
- }
1067
+ await this.cdc.unwatch(table);
569
1068
  }
570
1069
  on(table) {
571
1070
  this.ensureOpen();
572
- this.ensureCdc();
573
- const manager = this.subscriptionManager;
574
- if (!manager) throw new Error("subscriptionManager not initialized");
575
- return new SubscriptionBuilderImpl(table, manager);
1071
+ return this.cdc.on(table);
576
1072
  }
577
1073
  async migrate(migrations) {
578
1074
  this.ensureOpen();
579
- const writer = this.pool.acquireWriter();
580
- return MigrationRunner.run(writer, migrations);
1075
+ return this.writerLock.run(() => MigrationRunner.run(this.pool.acquireWriter(), migrations));
581
1076
  }
582
1077
  async rollback(migrations, version) {
583
1078
  this.ensureOpen();
584
- const writer = this.pool.acquireWriter();
585
- return MigrationRunner.rollback(writer, migrations, version);
1079
+ return this.writerLock.run(() => MigrationRunner.rollback(this.pool.acquireWriter(), migrations, version));
586
1080
  }
587
1081
  async backup(destPath) {
588
1082
  this.ensureOpen();
589
- const writer = this.pool.acquireWriter();
590
- await this.backupManager.backup(writer, destPath);
1083
+ await this.backups.backup(destPath);
591
1084
  }
592
1085
  scheduleBackup(options) {
593
1086
  this.ensureOpen();
594
- const writer = this.pool.acquireWriter();
595
- const cancel = this.backupScheduler.schedule(writer, options);
596
- this.scheduledBackupCancellers.push(cancel);
1087
+ this.backups.schedule(options);
597
1088
  }
598
1089
  async loadExtension(extensionPath) {
599
1090
  this.ensureOpen();
600
- const writer = this.pool.acquireWriter();
601
- await loadExtension(this.driver, writer, extensionPath);
1091
+ await this.writerLock.run(() => loadExtension(this.driver, this.pool.acquireWriter(), extensionPath));
602
1092
  }
603
1093
  onBeforeQuery(hook) {
604
1094
  this.hookRegistry.register("beforeQuery", hook);
@@ -613,16 +1103,12 @@ var Database = class _Database {
613
1103
  async close() {
614
1104
  if (this._closed) return;
615
1105
  this._closed = true;
616
- this.stopCdcPollingLoop();
617
- for (const cancel of this.scheduledBackupCancellers) {
618
- try {
619
- cancel();
620
- } catch {
621
- }
622
- }
623
- this.scheduledBackupCancellers.length = 0;
1106
+ this.cdc.stop();
1107
+ this.backups.cancelAll();
624
1108
  let poolError;
625
1109
  try {
1110
+ await this.groupCommitter.drain();
1111
+ await this.writerLock.settle();
626
1112
  await this.pool.close();
627
1113
  } catch (err) {
628
1114
  poolError = err;
@@ -648,54 +1134,6 @@ var Database = class _Database {
648
1134
  throw new SirannonError(`Database '${this.id}' is closed`, "DATABASE_CLOSED");
649
1135
  }
650
1136
  }
651
- ensureCdc() {
652
- if (!this.changeTracker) {
653
- this.changeTracker = new ChangeTracker({ retention: this.cdcRetention });
654
- }
655
- if (!this.subscriptionManager) {
656
- this.subscriptionManager = new SubscriptionManager();
657
- }
658
- }
659
- ensureCdcPolling() {
660
- if (this.stopCdcPolling) return;
661
- if (!this.changeTracker || !this.subscriptionManager) return;
662
- const writer = this.pool.acquireWriter();
663
- this.stopCdcPolling = startPolling(writer, this.changeTracker, this.subscriptionManager, this.cdcPollInterval);
664
- }
665
- stopCdcPollingLoop() {
666
- if (this.stopCdcPolling) {
667
- this.stopCdcPolling();
668
- this.stopCdcPolling = null;
669
- }
670
- }
671
- fireBeforeQueryHooks(sql, params, options) {
672
- const hasParent = this.parentHooks?.has("beforeQuery");
673
- const hasLocal = this.hookRegistry.has("beforeQuery");
674
- if (!hasParent && !hasLocal) return;
675
- const ctx = {
676
- databaseId: this.id,
677
- sql,
678
- params,
679
- writeConcern: options?.writeConcern,
680
- readConcern: options?.readConcern
681
- };
682
- this.parentHooks?.invokeSync("beforeQuery", ctx);
683
- this.hookRegistry.invokeSync("beforeQuery", ctx);
684
- }
685
- fireAfterQueryHooks(sql, params, durationMs) {
686
- const hasParent = this.parentHooks?.has("afterQuery");
687
- const hasLocal = this.hookRegistry.has("afterQuery");
688
- if (!hasParent && !hasLocal) return;
689
- const ctx = { databaseId: this.id, sql, params, durationMs };
690
- try {
691
- this.parentHooks?.invokeSync("afterQuery", ctx);
692
- } catch {
693
- }
694
- try {
695
- this.hookRegistry.invokeSync("afterQuery", ctx);
696
- } catch {
697
- }
698
- }
699
1137
  };
700
1138
 
701
1139
  // src/core/lifecycle/manager.ts
@@ -950,7 +1388,7 @@ var Sirannon = class {
950
1388
  if (this.hookRegistry.has("beforeConnect")) {
951
1389
  this.hookRegistry.invokeSync("beforeConnect", { databaseId: id, path });
952
1390
  }
953
- db = await Database.create(id, path, this._driver, options, {
1391
+ db = await Database.create(id, path, this._driver, this.withRegistryDefaults(options), {
954
1392
  parentHooks: this.hookRegistry,
955
1393
  metrics: this.metricsCollector ?? void 0
956
1394
  });
@@ -1000,6 +1438,11 @@ var Sirannon = class {
1000
1438
  });
1001
1439
  return db;
1002
1440
  }
1441
+ withRegistryDefaults(options) {
1442
+ const fallback = this.options.writerWorker;
1443
+ if (fallback === void 0 || options?.writerWorker !== void 0) return options;
1444
+ return { ...options, writerWorker: fallback };
1445
+ }
1003
1446
  async close(id) {
1004
1447
  this.ensureRunning();
1005
1448
  const db = this.dbs.get(id);