@korajs/store 1.0.0-beta.1 → 1.0.0-beta.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.
Files changed (58) hide show
  1. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  2. package/dist/adapters/better-sqlite3.js +1 -1
  3. package/dist/adapters/indexeddb.cjs +196 -39
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +1 -1
  6. package/dist/adapters/indexeddb.d.ts +1 -1
  7. package/dist/adapters/indexeddb.js +15 -8
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs +80 -53
  10. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -1
  11. package/dist/adapters/sqlite-wasm-shared-host.d.cts +63 -1
  12. package/dist/adapters/sqlite-wasm-shared-host.d.ts +63 -1
  13. package/dist/adapters/sqlite-wasm-shared-host.js +56 -52
  14. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -1
  15. package/dist/adapters/sqlite-wasm-worker.cjs +25 -3
  16. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm-worker.js +25 -3
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  19. package/dist/adapters/sqlite-wasm.cjs +186 -34
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm.d.cts +29 -3
  22. package/dist/adapters/sqlite-wasm.d.ts +29 -3
  23. package/dist/adapters/sqlite-wasm.js +3 -3
  24. package/dist/{backup-ZV4Y424B.js → backup-IQE4QC7Q.js} +2 -2
  25. package/dist/{chunk-RBPVEAVM.js → chunk-2ZRQGFEF.js} +179 -36
  26. package/dist/chunk-2ZRQGFEF.js.map +1 -0
  27. package/dist/{chunk-IOEFNHHP.js → chunk-COJSQBXW.js} +2 -2
  28. package/dist/{chunk-X57MWW35.js → chunk-OCOGQRHW.js} +12 -9
  29. package/dist/chunk-OCOGQRHW.js.map +1 -0
  30. package/dist/{chunk-FFYA7J5H.js → chunk-RCTG3IJG.js} +3 -2
  31. package/dist/chunk-RCTG3IJG.js.map +1 -0
  32. package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
  33. package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
  34. package/dist/{chunk-ZLWAYQEQ.js → chunk-UXRWVXBP.js} +32 -28
  35. package/dist/chunk-UXRWVXBP.js.map +1 -0
  36. package/dist/{export-audit-AJ7RORNK.js → export-audit-MT2LTMIN.js} +2 -2
  37. package/dist/index.cjs +167 -141
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.d.cts +10 -1
  40. package/dist/index.d.ts +10 -1
  41. package/dist/index.js +51 -41
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal.cjs +45 -40
  44. package/dist/internal.cjs.map +1 -1
  45. package/dist/internal.js +2 -2
  46. package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-CNHRf-m2.d.cts} +1 -1
  47. package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-CNHRf-m2.d.ts} +1 -1
  48. package/dist/sqlite-wasm-channel-NGSBJR44.js +12 -0
  49. package/package.json +2 -2
  50. package/dist/chunk-FFYA7J5H.js.map +0 -1
  51. package/dist/chunk-RBPVEAVM.js.map +0 -1
  52. package/dist/chunk-X57MWW35.js.map +0 -1
  53. package/dist/chunk-ZLWAYQEQ.js.map +0 -1
  54. package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
  55. /package/dist/{backup-ZV4Y424B.js.map → backup-IQE4QC7Q.js.map} +0 -0
  56. /package/dist/{chunk-IOEFNHHP.js.map → chunk-COJSQBXW.js.map} +0 -0
  57. /package/dist/{export-audit-AJ7RORNK.js.map → export-audit-MT2LTMIN.js.map} +0 -0
  58. /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-NGSBJR44.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, StoreNotOpenError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["import_core"]}
1
+ {"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, StoreNotOpenError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when a follower tab cannot reach a live leader tab over the multi-tab\n * broadcast channel (the leader tab was closed, crashed, or is unresponsive).\n * Distinct from {@link WorkerTimeoutError}: this fails fast on a confirmed-absent\n * leader instead of waiting out the full RPC timeout.\n */\nexport class NoLeaderError extends KoraError {\n\tconstructor(operation: string) {\n\t\tsuper(\n\t\t\t`No live leader tab is answering multi-tab storage RPC for operation \"${operation}\"`,\n\t\t\t'NO_LEADER',\n\t\t\t{ operation },\n\t\t)\n\t\tthis.name = 'NoLeaderError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["import_core"]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  AdapterError,
6
6
  StoreNotOpenError
7
- } from "../chunk-QL2XVZRN.js";
7
+ } from "../chunk-TUR7RGEP.js";
8
8
 
9
9
  // src/adapters/better-sqlite3-adapter.ts
10
10
  import { generateFullDDL } from "@korajs/core";
@@ -21,7 +21,7 @@ var __copyProps = (to, from, except, desc) => {
21
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
22
 
23
23
  // src/errors.ts
24
- var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, PersistenceError;
24
+ var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError, PersistenceError;
25
25
  var init_errors = __esm({
26
26
  "src/errors.ts"() {
27
27
  "use strict";
@@ -48,6 +48,16 @@ var init_errors = __esm({
48
48
  this.name = "WorkerTimeoutError";
49
49
  }
50
50
  };
51
+ NoLeaderError = class extends import_core.KoraError {
52
+ constructor(operation) {
53
+ super(
54
+ `No live leader tab is answering multi-tab storage RPC for operation "${operation}"`,
55
+ "NO_LEADER",
56
+ { operation }
57
+ );
58
+ this.name = "NoLeaderError";
59
+ }
60
+ };
51
61
  PersistenceError = class extends import_core.KoraError {
52
62
  constructor(message, context) {
53
63
  super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
@@ -190,6 +200,7 @@ __export(indexeddb_exports, {
190
200
  module.exports = __toCommonJS(indexeddb_exports);
191
201
 
192
202
  // src/adapters/indexeddb-adapter.ts
203
+ var import_core3 = require("@korajs/core");
193
204
  init_errors();
194
205
 
195
206
  // src/adapters/indexeddb-persistence-scheduler.ts
@@ -352,45 +363,73 @@ var SharedWorkerClientBridge = class {
352
363
  init_errors();
353
364
  var RPC_REQUEST = "kora-worker-request";
354
365
  var RPC_RESPONSE = "kora-worker-response";
366
+ var LEADER_PING = "kora-leader-ping";
367
+ var LEADER_PONG = "kora-leader-pong";
368
+ var DEFAULT_LIVENESS_PROBE_MS = 2e3;
355
369
  function isSharedWorkerStorageSupported() {
356
370
  return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
357
371
  }
358
372
  function isMultiTabStorageSupported() {
359
373
  return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
360
374
  }
361
- async function acquireTabStorageSession(dbName) {
375
+ async function acquireTabStorageSession(dbName, options) {
362
376
  const channelName = `kora-storage-${dbName}`;
377
+ const lockName = `kora-leader-${dbName}`;
363
378
  if (!isMultiTabStorageSupported()) {
364
379
  return { role: "leader", channelName };
365
380
  }
366
381
  return new Promise((resolve) => {
367
382
  let releaseHeld;
368
- void navigator.locks.request(
369
- `kora-leader-${dbName}`,
370
- { mode: "exclusive", ifAvailable: true },
371
- (lock) => {
372
- if (lock === null) {
373
- resolve({ role: "follower", channelName });
374
- return;
375
- }
376
- resolve({
377
- role: "leader",
378
- channelName,
379
- releaseLock: async () => {
380
- releaseHeld?.();
381
- }
382
- });
383
- return new Promise((release) => {
384
- releaseHeld = release;
385
- });
383
+ void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
384
+ if (lock === null) {
385
+ resolve(startFollowerSession(channelName, lockName, options));
386
+ return;
386
387
  }
387
- );
388
+ resolve({
389
+ role: "leader",
390
+ channelName,
391
+ releaseLock: async () => {
392
+ releaseHeld?.();
393
+ }
394
+ });
395
+ return new Promise((release) => {
396
+ releaseHeld = release;
397
+ });
398
+ });
388
399
  });
389
400
  }
401
+ function startFollowerSession(channelName, lockName, options) {
402
+ let promoted = false;
403
+ let releasePromotedLock;
404
+ const abort = new AbortController();
405
+ void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
406
+ promoted = true;
407
+ options?.onPromote?.();
408
+ return new Promise((release) => {
409
+ releasePromotedLock = release;
410
+ });
411
+ }).catch(() => {
412
+ });
413
+ return {
414
+ role: "follower",
415
+ channelName,
416
+ cancelPromotionWatch: () => {
417
+ if (promoted) {
418
+ releasePromotedLock?.();
419
+ } else {
420
+ abort.abort();
421
+ }
422
+ }
423
+ };
424
+ }
390
425
  function startLeaderRpcRelay(channelName, bridge) {
391
426
  const channel = new BroadcastChannel(channelName);
392
427
  const onMessage = (event) => {
393
428
  const data = event.data;
429
+ if (data?.type === LEADER_PING) {
430
+ channel.postMessage({ type: LEADER_PONG });
431
+ return;
432
+ }
394
433
  if (data?.type !== RPC_REQUEST) {
395
434
  return;
396
435
  }
@@ -426,9 +465,11 @@ var FollowerBroadcastBridge = class {
426
465
  channel;
427
466
  pending = /* @__PURE__ */ new Map();
428
467
  timeoutMs;
468
+ livenessProbeMs;
429
469
  terminated = false;
430
- constructor(channelName, timeoutMs = 3e4) {
470
+ constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
431
471
  this.timeoutMs = timeoutMs;
472
+ this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
432
473
  this.channel = new BroadcastChannel(channelName);
433
474
  this.channel.addEventListener("message", (event) => {
434
475
  const data = event.data;
@@ -442,6 +483,48 @@ var FollowerBroadcastBridge = class {
442
483
  }
443
484
  });
444
485
  }
486
+ /**
487
+ * Readiness handshake: resolves `true` as soon as a live leader answers a ping,
488
+ * or `false` if none answers within the budget. The adapter calls this before
489
+ * its first RPC so a follower created before a leader relay is live retries the
490
+ * handshake instead of firing into the void and waiting out the full timeout.
491
+ */
492
+ async waitForLeader(timeoutMs = 3e3, attempts = 3) {
493
+ const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
494
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
495
+ if (this.terminated) {
496
+ return false;
497
+ }
498
+ if (await this.pingLeader(perAttempt)) {
499
+ return true;
500
+ }
501
+ }
502
+ return false;
503
+ }
504
+ /** Sends one liveness ping and resolves whether a leader answered in time. */
505
+ pingLeader(timeoutMs) {
506
+ if (this.terminated) {
507
+ return Promise.resolve(false);
508
+ }
509
+ return new Promise((resolve) => {
510
+ const onPong = (event) => {
511
+ if (event.data?.type === LEADER_PONG) {
512
+ cleanup();
513
+ resolve(true);
514
+ }
515
+ };
516
+ const cleanup = () => {
517
+ clearTimeout(timer);
518
+ this.channel.removeEventListener("message", onPong);
519
+ };
520
+ const timer = setTimeout(() => {
521
+ cleanup();
522
+ resolve(false);
523
+ }, timeoutMs);
524
+ this.channel.addEventListener("message", onPong);
525
+ this.channel.postMessage({ type: LEADER_PING });
526
+ });
527
+ }
445
528
  async send(request) {
446
529
  if (this.terminated) {
447
530
  return {
@@ -454,19 +537,25 @@ var FollowerBroadcastBridge = class {
454
537
  const requestId = crypto.randomUUID();
455
538
  const msg = { type: RPC_REQUEST, requestId, request };
456
539
  return new Promise((resolve, reject) => {
457
- const timer = setTimeout(() => {
540
+ const settle = (fn) => {
541
+ clearTimeout(timer);
542
+ clearTimeout(probe);
458
543
  this.pending.delete(requestId);
459
- reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs));
544
+ fn();
545
+ };
546
+ const timer = setTimeout(() => {
547
+ settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
460
548
  }, this.timeoutMs);
549
+ const probe = setTimeout(() => {
550
+ void this.pingLeader(this.livenessProbeMs).then((alive) => {
551
+ if (!alive && this.pending.has(requestId)) {
552
+ settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
553
+ }
554
+ });
555
+ }, this.livenessProbeMs);
461
556
  this.pending.set(requestId, {
462
- resolve: (response) => {
463
- clearTimeout(timer);
464
- resolve(response);
465
- },
466
- reject: (error) => {
467
- clearTimeout(timer);
468
- reject(error);
469
- }
557
+ resolve: (response) => settle(() => resolve(response)),
558
+ reject: (error) => settle(() => reject(error))
470
559
  });
471
560
  this.channel.postMessage(msg);
472
561
  });
@@ -495,14 +584,19 @@ var SqliteWasmAdapter = class {
495
584
  sharedWorkerUrl;
496
585
  workerResponseTimeoutMs;
497
586
  dbName;
587
+ emitter;
498
588
  tabSession = null;
499
589
  sharedWorker = null;
590
+ /** Retained so a follower promoted to leader can re-open its own worker. */
591
+ schema = null;
592
+ promoting = false;
500
593
  constructor(options = {}) {
501
594
  this.injectedBridge = options.bridge;
502
595
  this.workerUrl = options.workerUrl;
503
596
  this.sharedWorkerUrl = options.sharedWorkerUrl;
504
597
  this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
505
598
  this.dbName = options.dbName ?? "kora-db";
599
+ this.emitter = options.emitter;
506
600
  }
507
601
  async open(schema) {
508
602
  if (this.opened) return;
@@ -514,17 +608,29 @@ var SqliteWasmAdapter = class {
514
608
  this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
515
609
  this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
516
610
  } else if (this.workerUrl) {
517
- this.tabSession = await acquireTabStorageSession(this.dbName);
611
+ this.schema = schema;
612
+ this.tabSession = await acquireTabStorageSession(this.dbName, {
613
+ onPromote: () => {
614
+ void this.promoteToLeader();
615
+ }
616
+ });
518
617
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
519
618
  if (this.tabSession.role === "leader") {
520
619
  const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
521
620
  this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
522
621
  this.bridge = workerBridge;
523
622
  } else {
524
- this.bridge = new FollowerBroadcastBridge(
623
+ const followerBridge = new FollowerBroadcastBridge(
525
624
  this.tabSession.channelName,
526
625
  this.workerResponseTimeoutMs
527
626
  );
627
+ this.bridge = followerBridge;
628
+ this.emitter?.emit({
629
+ type: "store:db-name-collision",
630
+ dbName: this.dbName,
631
+ message: `Another runtime on this origin is already using database "${this.dbName}"; this runtime is sharing it as a follower. If these are separate apps, give each a distinct store name.`
632
+ });
633
+ await followerBridge.waitForLeader();
528
634
  }
529
635
  } else {
530
636
  throw new AdapterError(
@@ -544,14 +650,37 @@ var SqliteWasmAdapter = class {
544
650
  dbName: this.dbName
545
651
  });
546
652
  }
653
+ this.reportStorageMode(response.data);
547
654
  this.opened = true;
548
655
  }
656
+ /**
657
+ * Surface a `store:opfs-unavailable` diagnostic when the worker reported that
658
+ * persistence silently degraded to an in-memory database, so a data-loss
659
+ * condition is observable instead of failing silently. A persistent open (or a
660
+ * bridge that does not report a mode, e.g. the Node mock) emits nothing.
661
+ */
662
+ reportStorageMode(data) {
663
+ if (!this.emitter || typeof data !== "object" || data === null) {
664
+ return;
665
+ }
666
+ const mode = data;
667
+ if (mode.persistent === false) {
668
+ const reason = mode.fallbackReason ?? "unsupported";
669
+ this.emitter.emit({
670
+ type: "store:opfs-unavailable",
671
+ dbName: this.dbName,
672
+ reason,
673
+ message: `OPFS persistence is unavailable (${reason}) for database "${this.dbName}"; the store is running in memory and data will not survive a reload.`
674
+ });
675
+ }
676
+ }
549
677
  async close() {
550
678
  if (!this.bridge) return;
551
679
  try {
552
680
  await this.sendRequest({ id: 0, type: "close" });
553
681
  } finally {
554
682
  this.tabSession?.stopRelay?.();
683
+ this.tabSession?.cancelPromotionWatch?.();
555
684
  if (this.tabSession?.releaseLock) {
556
685
  await this.tabSession.releaseLock();
557
686
  }
@@ -562,6 +691,30 @@ var SqliteWasmAdapter = class {
562
691
  this.opened = false;
563
692
  }
564
693
  }
694
+ /**
695
+ * Promotes a follower to leader after the previous leader released the storage
696
+ * lock (its tab closed or crashed). The lock is only granted once the old leader
697
+ * is gone, so opening our own worker here is safe from the OPFS single-writer
698
+ * rule. In-flight follower RPCs are rejected and left to the caller / reactive
699
+ * layer to retry against the new leader.
700
+ */
701
+ async promoteToLeader() {
702
+ if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
703
+ return;
704
+ }
705
+ this.promoting = true;
706
+ const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
707
+ const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
708
+ const previousBridge = this.bridge;
709
+ this.bridge = workerBridge;
710
+ if (this.tabSession) {
711
+ this.tabSession.role = "leader";
712
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
713
+ }
714
+ previousBridge?.terminate();
715
+ const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
716
+ await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
717
+ }
565
718
  async execute(sql, params) {
566
719
  this.guardOpen();
567
720
  const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
@@ -904,7 +1057,7 @@ var IndexedDbAdapter = class {
904
1057
  if (!dump) return;
905
1058
  for (const table of dump.tables) {
906
1059
  const name = ensureSafeIdentifier(table.name);
907
- await this.inner.execute(`DELETE FROM ${name}`);
1060
+ await this.inner.execute(`DELETE FROM ${(0, import_core3.quoteIdent)(name)}`);
908
1061
  if (table.rows.length === 0) continue;
909
1062
  for (const row of table.rows) {
910
1063
  const columns = table.columns.filter(
@@ -912,10 +1065,10 @@ var IndexedDbAdapter = class {
912
1065
  );
913
1066
  if (columns.length === 0) continue;
914
1067
  const placeholders = columns.map(() => "?").join(", ");
915
- const quotedColumns = columns.map((column) => ensureSafeIdentifier(column)).join(", ");
1068
+ const quotedColumns = columns.map((column) => (0, import_core3.quoteIdent)(ensureSafeIdentifier(column))).join(", ");
916
1069
  const values = columns.map((column) => row[column]);
917
1070
  await this.inner.execute(
918
- `INSERT INTO ${name} (${quotedColumns}) VALUES (${placeholders})`,
1071
+ `INSERT INTO ${(0, import_core3.quoteIdent)(name)} (${quotedColumns}) VALUES (${placeholders})`,
919
1072
  values
920
1073
  );
921
1074
  }
@@ -928,9 +1081,13 @@ var IndexedDbAdapter = class {
928
1081
  const tables = [];
929
1082
  for (const tableRow of tableRows) {
930
1083
  const tableName = ensureSafeIdentifier(tableRow.name);
931
- const columns = await this.inner.query(`PRAGMA table_info(${tableName})`);
1084
+ const columns = await this.inner.query(
1085
+ `PRAGMA table_info(${(0, import_core3.quoteIdent)(tableName)})`
1086
+ );
932
1087
  const columnNames = columns.map((column) => column.name);
933
- const rows = await this.inner.query(`SELECT * FROM ${tableName}`);
1088
+ const rows = await this.inner.query(
1089
+ `SELECT * FROM ${(0, import_core3.quoteIdent)(tableName)}`
1090
+ );
934
1091
  tables.push({
935
1092
  name: tableName,
936
1093
  columns: columnNames,