@korajs/store 0.3.2 → 0.5.0

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 (73) hide show
  1. package/dist/adapters/better-sqlite3.cjs +1 -1
  2. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  3. package/dist/adapters/better-sqlite3.d.cts +1 -1
  4. package/dist/adapters/better-sqlite3.d.ts +1 -1
  5. package/dist/adapters/better-sqlite3.js +1 -1
  6. package/dist/adapters/better-sqlite3.js.map +1 -1
  7. package/dist/adapters/indexeddb.cjs +413 -18
  8. package/dist/adapters/indexeddb.cjs.map +1 -1
  9. package/dist/adapters/indexeddb.d.cts +23 -5
  10. package/dist/adapters/indexeddb.d.ts +23 -5
  11. package/dist/adapters/indexeddb.js +166 -19
  12. package/dist/adapters/indexeddb.js.map +1 -1
  13. package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
  14. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
  15. package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
  16. package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
  17. package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
  18. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
  19. package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
  20. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm-worker.js +34 -5
  22. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  23. package/dist/adapters/sqlite-wasm.cjs +249 -2
  24. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  25. package/dist/adapters/sqlite-wasm.d.cts +15 -3
  26. package/dist/adapters/sqlite-wasm.d.ts +15 -3
  27. package/dist/adapters/sqlite-wasm.js +2 -2
  28. package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
  29. package/dist/backup-JN3LB4YT.js +14 -0
  30. package/dist/backup-JN3LB4YT.js.map +1 -0
  31. package/dist/chunk-56LL4XP6.js +205 -0
  32. package/dist/chunk-56LL4XP6.js.map +1 -0
  33. package/dist/chunk-6AHYGQQT.js +425 -0
  34. package/dist/chunk-6AHYGQQT.js.map +1 -0
  35. package/dist/chunk-7BPETY5Y.js +570 -0
  36. package/dist/chunk-7BPETY5Y.js.map +1 -0
  37. package/dist/chunk-AGN3CIPB.js +188 -0
  38. package/dist/chunk-AGN3CIPB.js.map +1 -0
  39. package/dist/chunk-EZSTD2XC.js +380 -0
  40. package/dist/chunk-EZSTD2XC.js.map +1 -0
  41. package/dist/chunk-MYSJN3DF.js +117 -0
  42. package/dist/chunk-MYSJN3DF.js.map +1 -0
  43. package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
  44. package/dist/chunk-XO4LMPAH.js.map +1 -0
  45. package/dist/export-audit-5GV6VRDZ.js +15 -0
  46. package/dist/export-audit-5GV6VRDZ.js.map +1 -0
  47. package/dist/index.cjs +3351 -510
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +822 -50
  50. package/dist/index.d.ts +822 -50
  51. package/dist/index.js +1976 -558
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal.cjs +789 -0
  54. package/dist/internal.cjs.map +1 -0
  55. package/dist/internal.d.cts +154 -0
  56. package/dist/internal.d.ts +154 -0
  57. package/dist/internal.js +48 -0
  58. package/dist/internal.js.map +1 -0
  59. package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
  60. package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
  61. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
  62. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
  63. package/dist/types-BMyHmwvn.d.ts +163 -0
  64. package/dist/types-CTr00g_d.d.cts +163 -0
  65. package/dist/types-DepnRDk_.d.cts +212 -0
  66. package/dist/types-DepnRDk_.d.ts +212 -0
  67. package/package.json +23 -2
  68. package/dist/chunk-DXKLAQ6P.js.map +0 -1
  69. package/dist/chunk-ZP5AXQ3Z.js +0 -179
  70. package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
  71. package/dist/types-DF-KDSK1.d.cts +0 -106
  72. package/dist/types-DF-KDSK1.d.ts +0 -106
  73. /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.js.map} +0 -0
@@ -74,7 +74,7 @@ var BetterSqlite3Adapter = class {
74
74
  this.db.exec(sql.replace("--kora:safe-alter\n", ""));
75
75
  } catch (e) {
76
76
  const msg = e.message || "";
77
- if (!msg.includes("duplicate column name")) {
77
+ if (!msg.includes("duplicate column name") && !msg.includes("Cannot add a NOT NULL column with default value NULL")) {
78
78
  throw e;
79
79
  }
80
80
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.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'\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 * @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\tif (!msg.includes('duplicate column name')) {\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// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tconst tx: Transaction = {\n\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t\tawait fn(tx)\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow error\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\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tfor (const sql of migration.statements) {\n\t\t\t\tdb.exec(sql)\n\t\t\t}\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow new AdapterError(\n\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t{\n\t\t\t\t\tfrom,\n\t\t\t\t\tto,\n\t\t\t\t},\n\t\t\t)\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 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"],"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"],"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'\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 * @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// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tconst tx: Transaction = {\n\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t\tawait fn(tx)\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow error\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\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tfor (const sql of migration.statements) {\n\t\t\t\tdb.exec(sql)\n\t\t\t}\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow new AdapterError(\n\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t{\n\t\t\t\t\tfrom,\n\t\t\t\t\tto,\n\t\t\t\t},\n\t\t\t)\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 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"],"mappings":"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","names":["import_core"]}
@@ -1,5 +1,5 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-DF-KDSK1.cjs';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-DepnRDk_.cjs';
3
3
 
4
4
  /**
5
5
  * Storage adapter backed by better-sqlite3 for Node.js environments.
@@ -1,5 +1,5 @@
1
1
  import { SchemaDefinition } from '@korajs/core';
2
- import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-DF-KDSK1.js';
2
+ import { S as StorageAdapter, T as Transaction, M as MigrationPlan } from '../types-DepnRDk_.js';
3
3
 
4
4
  /**
5
5
  * Storage adapter backed by better-sqlite3 for Node.js environments.
@@ -26,7 +26,7 @@ var BetterSqlite3Adapter = class {
26
26
  this.db.exec(sql.replace("--kora:safe-alter\n", ""));
27
27
  } catch (e) {
28
28
  const msg = e.message || "";
29
- if (!msg.includes("duplicate column name")) {
29
+ if (!msg.includes("duplicate column name") && !msg.includes("Cannot add a NOT NULL column with default value NULL")) {
30
30
  throw e;
31
31
  }
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/better-sqlite3-adapter.ts"],"sourcesContent":["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'\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 * @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\tif (!msg.includes('duplicate column name')) {\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// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tconst tx: Transaction = {\n\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t\tawait fn(tx)\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow error\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\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tfor (const sql of migration.statements) {\n\t\t\t\tdb.exec(sql)\n\t\t\t}\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow new AdapterError(\n\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t{\n\t\t\t\t\tfrom,\n\t\t\t\t\tto,\n\t\t\t\t},\n\t\t\t)\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"],"mappings":";;;;;;AAAA,SAAS,uBAAuB;AAiBzB,IAAM,uBAAN,MAAqD;AAAA;AAAA;AAAA;AAAA,EAM3D,YAA6B,OAAe,YAAY;AAA3B;AAAA,EAA4B;AAAA,EAA5B;AAAA,EALrB,KAA+B;AAAA,EAOvC,MAAM,KAAK,QAAyC;AAEnD,UAAM,iBAAiB,MAAM,OAAO,gBAAgB,GAAG;AACvD,SAAK,KAAK,IAAI,cAAc,KAAK,IAAI;AAGrC,SAAK,GAAG,OAAO,oBAAoB;AAEnC,SAAK,GAAG,OAAO,mBAAmB;AAElC,UAAM,aAAa,gBAAgB,MAAM;AACzC,eAAW,OAAO,YAAY;AAC7B,UAAI,IAAI,WAAW,mBAAmB,GAAG;AAExC,YAAI;AACH,eAAK,GAAG,KAAK,IAAI,QAAQ,uBAAuB,EAAE,CAAC;AAAA,QACpD,SAAS,GAAG;AACX,gBAAM,MAAO,EAAY,WAAW;AACpC,cAAI,CAAC,IAAI,SAAS,uBAAuB,GAAG;AAC3C,kBAAM;AAAA,UACP;AAAA,QACD;AAAA,MACD,OAAO;AACN,aAAK,GAAG,KAAK,GAAG;AAAA,MACjB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,QAAuB;AAC5B,QAAI,KAAK,IAAI;AACZ,WAAK,GAAG,MAAM;AACd,WAAK,KAAK;AAAA,IACX;AAAA,EACD;AAAA,EAEA,MAAM,QAAQ,KAAa,QAAmC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI;AACH,SAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IACtC,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,mBAAoB,MAAgB,OAAO,IAAI;AAAA,QACrE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAM,MAAS,KAAa,QAAkC;AAC7D,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI;AACH,aAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,IAC7C,SAAS,OAAO;AACf,YAAM,IAAI,aAAa,iBAAkB,MAAgB,OAAO,IAAI;AAAA,QACnE;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,IAAuD;AACxE,UAAM,KAAK,KAAK,MAAM;AAItB,OAAG,KAAK,OAAO;AACf,QAAI;AACH,YAAM,KAAkB;AAAA,QACvB,SAAS,OAAO,KAAa,WAAsC;AAClE,cAAI;AACH,eAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,UACtC,SAAS,OAAO;AACf,kBAAM,IAAI,aAAa,+BAAgC,MAAgB,OAAO,IAAI;AAAA,cACjF;AAAA,cACA;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD;AAAA,QACA,OAAO,OAAU,KAAa,WAAqC;AAClE,cAAI;AACH,mBAAO,GAAG,QAAQ,GAAG,EAAE,IAAI,GAAI,UAAU,CAAC,CAAE;AAAA,UAC7C,SAAS,OAAO;AACf,kBAAM,IAAI,aAAa,6BAA8B,MAAgB,OAAO,IAAI;AAAA,cAC/E;AAAA,cACA;AAAA,YACD,CAAC;AAAA,UACF;AAAA,QACD;AAAA,MACD;AACA,YAAM,GAAG,EAAE;AACX,SAAG,KAAK,QAAQ;AAAA,IACjB,SAAS,OAAO;AACf,SAAG,KAAK,UAAU;AAClB,YAAM;AAAA,IACP;AAAA,EACD;AAAA,EAEA,MAAM,QAAQ,MAAc,IAAY,WAAyC;AAChF,UAAM,KAAK,KAAK,MAAM;AACtB,OAAG,KAAK,OAAO;AACf,QAAI;AACH,iBAAW,OAAO,UAAU,YAAY;AACvC,WAAG,KAAK,GAAG;AAAA,MACZ;AACA,SAAG,KAAK,QAAQ;AAAA,IACjB,SAAS,OAAO;AACf,SAAG,KAAK,UAAU;AAClB,YAAM,IAAI;AAAA,QACT,mBAAmB,IAAI,QAAQ,EAAE,YAAa,MAAgB,OAAO;AAAA,QACrE;AAAA,UACC;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA,EAEQ,QAA2B;AAClC,QAAI,CAAC,KAAK,IAAI;AACb,YAAM,IAAI,kBAAkB;AAAA,IAC7B;AACA,WAAO,KAAK;AAAA,EACb;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/adapters/better-sqlite3-adapter.ts"],"sourcesContent":["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'\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 * @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// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tconst tx: Transaction = {\n\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t\tawait fn(tx)\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow error\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\tdb.exec('BEGIN')\n\t\ttry {\n\t\t\tfor (const sql of migration.statements) {\n\t\t\t\tdb.exec(sql)\n\t\t\t}\n\t\t\tdb.exec('COMMIT')\n\t\t} catch (error) {\n\t\t\tdb.exec('ROLLBACK')\n\t\t\tthrow new AdapterError(\n\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t{\n\t\t\t\t\tfrom,\n\t\t\t\t\tto,\n\t\t\t\t},\n\t\t\t)\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"],"mappings":"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","names":[]}
@@ -181,9 +181,299 @@ module.exports = __toCommonJS(indexeddb_exports);
181
181
  // src/adapters/indexeddb-adapter.ts
182
182
  init_errors();
183
183
 
184
+ // src/adapters/indexeddb-persistence-scheduler.ts
185
+ var DEFAULT_DEBOUNCE_MS = 500;
186
+ var IndexedDbPersistenceScheduler = class {
187
+ debounceMs;
188
+ flush;
189
+ onError;
190
+ timer = null;
191
+ inFlight = null;
192
+ disposed = false;
193
+ onVisibilityChange;
194
+ constructor(options) {
195
+ this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
196
+ this.flush = options.flush;
197
+ this.onError = options.onError;
198
+ this.onVisibilityChange = () => {
199
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
200
+ void this.flushNow();
201
+ }
202
+ };
203
+ if (typeof document !== "undefined") {
204
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
205
+ }
206
+ }
207
+ /** Schedule a debounced snapshot write. */
208
+ schedule() {
209
+ if (this.disposed) return;
210
+ if (this.debounceMs <= 0) {
211
+ void this.flushNow();
212
+ return;
213
+ }
214
+ if (this.timer !== null) {
215
+ clearTimeout(this.timer);
216
+ }
217
+ this.timer = setTimeout(() => {
218
+ this.timer = null;
219
+ void this.flushNow();
220
+ }, this.debounceMs);
221
+ }
222
+ /** Cancel any pending debounce and persist immediately. */
223
+ async flushNow() {
224
+ if (this.disposed) return;
225
+ if (this.timer !== null) {
226
+ clearTimeout(this.timer);
227
+ this.timer = null;
228
+ }
229
+ if (this.inFlight) {
230
+ await this.inFlight;
231
+ return;
232
+ }
233
+ this.inFlight = this.runFlush();
234
+ try {
235
+ await this.inFlight;
236
+ } finally {
237
+ this.inFlight = null;
238
+ }
239
+ }
240
+ dispose() {
241
+ this.disposed = true;
242
+ if (this.timer !== null) {
243
+ clearTimeout(this.timer);
244
+ this.timer = null;
245
+ }
246
+ if (typeof document !== "undefined") {
247
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
248
+ }
249
+ }
250
+ async runFlush() {
251
+ try {
252
+ await this.flush();
253
+ } catch (error) {
254
+ this.onError?.(error);
255
+ }
256
+ }
257
+ };
258
+
184
259
  // src/adapters/sqlite-wasm-adapter.ts
185
260
  var import_core2 = require("@korajs/core");
186
261
  init_errors();
262
+
263
+ // src/multi-tab/shared-worker-bridge.ts
264
+ init_errors();
265
+ var SW_REQUEST = "kora-sw-request";
266
+ var SW_RESPONSE = "kora-sw-response";
267
+ var SharedWorkerClientBridge = class {
268
+ constructor(sharedWorker, dbName, workerUrl, timeoutMs = 3e4) {
269
+ this.dbName = dbName;
270
+ this.workerUrl = workerUrl;
271
+ this.timeoutMs = timeoutMs;
272
+ this.port = sharedWorker.port;
273
+ this.port.start();
274
+ this.port.addEventListener("message", (event) => {
275
+ const data = event.data;
276
+ if (data?.type !== SW_RESPONSE) {
277
+ return;
278
+ }
279
+ const entry = this.pending.get(data.requestId);
280
+ if (entry) {
281
+ this.pending.delete(data.requestId);
282
+ entry.resolve(data.response);
283
+ }
284
+ });
285
+ }
286
+ dbName;
287
+ workerUrl;
288
+ port;
289
+ pending = /* @__PURE__ */ new Map();
290
+ timeoutMs;
291
+ terminated = false;
292
+ async send(request) {
293
+ if (this.terminated) {
294
+ return {
295
+ id: request.id,
296
+ type: "error",
297
+ message: "SharedWorker bridge terminated",
298
+ code: "BRIDGE_TERMINATED"
299
+ };
300
+ }
301
+ const requestId = crypto.randomUUID();
302
+ const msg = {
303
+ type: SW_REQUEST,
304
+ requestId,
305
+ dbName: this.dbName,
306
+ workerUrl: this.workerUrl,
307
+ request
308
+ };
309
+ return new Promise((resolve, reject) => {
310
+ const timer = setTimeout(() => {
311
+ this.pending.delete(requestId);
312
+ reject(new WorkerTimeoutError(`shared-worker:${request.type}`, this.timeoutMs));
313
+ }, this.timeoutMs);
314
+ this.pending.set(requestId, {
315
+ resolve: (response) => {
316
+ clearTimeout(timer);
317
+ resolve(response);
318
+ },
319
+ reject: (error) => {
320
+ clearTimeout(timer);
321
+ reject(error);
322
+ }
323
+ });
324
+ this.port.postMessage(msg);
325
+ });
326
+ }
327
+ terminate() {
328
+ if (this.terminated) {
329
+ return;
330
+ }
331
+ this.terminated = true;
332
+ this.port.close();
333
+ for (const [, entry] of this.pending) {
334
+ entry.reject(new Error("SharedWorker bridge terminated"));
335
+ }
336
+ this.pending.clear();
337
+ }
338
+ };
339
+
340
+ // src/multi-tab/tab-storage.ts
341
+ init_errors();
342
+ var RPC_REQUEST = "kora-worker-request";
343
+ var RPC_RESPONSE = "kora-worker-response";
344
+ function isSharedWorkerStorageSupported() {
345
+ return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
346
+ }
347
+ function isMultiTabStorageSupported() {
348
+ return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
349
+ }
350
+ async function acquireTabStorageSession(dbName) {
351
+ const channelName = `kora-storage-${dbName}`;
352
+ if (!isMultiTabStorageSupported()) {
353
+ return { role: "leader", channelName };
354
+ }
355
+ return new Promise((resolve) => {
356
+ let releaseHeld;
357
+ void navigator.locks.request(
358
+ `kora-leader-${dbName}`,
359
+ { mode: "exclusive", ifAvailable: true },
360
+ (lock) => {
361
+ if (lock === null) {
362
+ resolve({ role: "follower", channelName });
363
+ return;
364
+ }
365
+ resolve({
366
+ role: "leader",
367
+ channelName,
368
+ releaseLock: async () => {
369
+ releaseHeld?.();
370
+ }
371
+ });
372
+ return new Promise((release) => {
373
+ releaseHeld = release;
374
+ });
375
+ }
376
+ );
377
+ });
378
+ }
379
+ function startLeaderRpcRelay(channelName, bridge) {
380
+ const channel = new BroadcastChannel(channelName);
381
+ const onMessage = (event) => {
382
+ const data = event.data;
383
+ if (data?.type !== RPC_REQUEST) {
384
+ return;
385
+ }
386
+ void bridge.send(data.request).then((response) => {
387
+ const msg = {
388
+ type: RPC_RESPONSE,
389
+ requestId: data.requestId,
390
+ response
391
+ };
392
+ channel.postMessage(msg);
393
+ }).catch((error) => {
394
+ const message = error instanceof Error ? error.message : "Worker RPC failed";
395
+ const msg = {
396
+ type: RPC_RESPONSE,
397
+ requestId: data.requestId,
398
+ response: {
399
+ id: data.request.id,
400
+ type: "error",
401
+ message,
402
+ code: "LEADER_RPC_ERROR"
403
+ }
404
+ };
405
+ channel.postMessage(msg);
406
+ });
407
+ };
408
+ channel.addEventListener("message", onMessage);
409
+ return () => {
410
+ channel.removeEventListener("message", onMessage);
411
+ channel.close();
412
+ };
413
+ }
414
+ var FollowerBroadcastBridge = class {
415
+ channel;
416
+ pending = /* @__PURE__ */ new Map();
417
+ timeoutMs;
418
+ terminated = false;
419
+ constructor(channelName, timeoutMs = 3e4) {
420
+ this.timeoutMs = timeoutMs;
421
+ this.channel = new BroadcastChannel(channelName);
422
+ this.channel.addEventListener("message", (event) => {
423
+ const data = event.data;
424
+ if (data?.type !== RPC_RESPONSE) {
425
+ return;
426
+ }
427
+ const entry = this.pending.get(data.requestId);
428
+ if (entry) {
429
+ this.pending.delete(data.requestId);
430
+ entry.resolve(data.response);
431
+ }
432
+ });
433
+ }
434
+ async send(request) {
435
+ if (this.terminated) {
436
+ return {
437
+ id: request.id,
438
+ type: "error",
439
+ message: "Follower bridge terminated",
440
+ code: "BRIDGE_TERMINATED"
441
+ };
442
+ }
443
+ const requestId = crypto.randomUUID();
444
+ const msg = { type: RPC_REQUEST, requestId, request };
445
+ return new Promise((resolve, reject) => {
446
+ const timer = setTimeout(() => {
447
+ this.pending.delete(requestId);
448
+ reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs));
449
+ }, this.timeoutMs);
450
+ this.pending.set(requestId, {
451
+ resolve: (response) => {
452
+ clearTimeout(timer);
453
+ resolve(response);
454
+ },
455
+ reject: (error) => {
456
+ clearTimeout(timer);
457
+ reject(error);
458
+ }
459
+ });
460
+ this.channel.postMessage(msg);
461
+ });
462
+ }
463
+ terminate() {
464
+ if (this.terminated) {
465
+ return;
466
+ }
467
+ this.terminated = true;
468
+ this.channel.close();
469
+ for (const [, entry] of this.pending) {
470
+ entry.reject(new Error("Follower bridge terminated"));
471
+ }
472
+ this.pending.clear();
473
+ }
474
+ };
475
+
476
+ // src/adapters/sqlite-wasm-adapter.ts
187
477
  init_sqlite_wasm_channel();
188
478
  var SqliteWasmAdapter = class {
189
479
  bridge = null;
@@ -191,26 +481,52 @@ var SqliteWasmAdapter = class {
191
481
  mutex = new Mutex();
192
482
  injectedBridge;
193
483
  workerUrl;
484
+ sharedWorkerUrl;
485
+ workerResponseTimeoutMs;
194
486
  dbName;
487
+ tabSession = null;
488
+ sharedWorker = null;
195
489
  constructor(options = {}) {
196
490
  this.injectedBridge = options.bridge;
197
491
  this.workerUrl = options.workerUrl;
492
+ this.sharedWorkerUrl = options.sharedWorkerUrl;
493
+ this.workerResponseTimeoutMs = options.workerResponseTimeoutMs ?? 3e4;
198
494
  this.dbName = options.dbName ?? "kora-db";
199
495
  }
200
496
  async open(schema) {
201
497
  if (this.opened) return;
202
498
  if (this.injectedBridge) {
203
499
  this.bridge = this.injectedBridge;
500
+ } else if (this.sharedWorkerUrl && this.workerUrl && isSharedWorkerStorageSupported()) {
501
+ const workerUrlString = typeof this.workerUrl === "string" ? this.workerUrl : this.workerUrl.href;
502
+ const sharedUrl = typeof this.sharedWorkerUrl === "string" ? this.sharedWorkerUrl : this.sharedWorkerUrl.href;
503
+ this.sharedWorker = new SharedWorker(sharedUrl, { name: `kora-sw-${this.dbName}` });
504
+ this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
204
505
  } else if (this.workerUrl) {
506
+ this.tabSession = await acquireTabStorageSession(this.dbName);
205
507
  const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
206
- this.bridge = new WebWorkerBridge2(this.workerUrl);
508
+ if (this.tabSession.role === "leader") {
509
+ const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
510
+ this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
511
+ this.bridge = workerBridge;
512
+ } else {
513
+ this.bridge = new FollowerBroadcastBridge(
514
+ this.tabSession.channelName,
515
+ this.workerResponseTimeoutMs
516
+ );
517
+ }
207
518
  } else {
208
519
  throw new AdapterError(
209
520
  'SqliteWasmAdapter requires either a bridge (for testing) or a workerUrl (for browsers). Pass { bridge: new MockWorkerBridge() } for tests, or { workerUrl: "/worker.js" } for browsers.'
210
521
  );
211
522
  }
212
523
  const ddlStatements = (0, import_core2.generateFullDDL)(schema);
213
- const response = await this.sendRequest({ id: 0, type: "open", ddlStatements });
524
+ const response = await this.sendRequest({
525
+ id: 0,
526
+ type: "open",
527
+ ddlStatements,
528
+ dbName: this.dbName
529
+ });
214
530
  if (response.type === "error") {
215
531
  throw new AdapterError(`Failed to open database: ${response.message}`, {
216
532
  code: response.code,
@@ -224,6 +540,12 @@ var SqliteWasmAdapter = class {
224
540
  try {
225
541
  await this.sendRequest({ id: 0, type: "close" });
226
542
  } finally {
543
+ this.tabSession?.stopRelay?.();
544
+ if (this.tabSession?.releaseLock) {
545
+ await this.tabSession.releaseLock();
546
+ }
547
+ this.tabSession = null;
548
+ this.sharedWorker = null;
227
549
  this.bridge.terminate();
228
550
  this.bridge = null;
229
551
  this.opened = false;
@@ -356,6 +678,32 @@ var IDB_DATABASE_NAME = "kora-persistence";
356
678
  var IDB_STORE_NAME = "databases";
357
679
  var IDB_VERSION = 1;
358
680
  var DUMP_SUFFIX = "::dump";
681
+ function isIndexedDbQuotaError(error) {
682
+ if (error instanceof DOMException && error.name === "QuotaExceededError") {
683
+ return true;
684
+ }
685
+ if (error instanceof PersistenceError && error.context?.quotaExceeded === true) {
686
+ return true;
687
+ }
688
+ if (error instanceof Error && /quota/i.test(error.message)) {
689
+ return true;
690
+ }
691
+ return false;
692
+ }
693
+ function rejectWithIdbError(reject, message, context, event) {
694
+ const domError = event.target?.error;
695
+ if (domError && isIndexedDbQuotaError(domError)) {
696
+ reject(
697
+ new PersistenceError(message, {
698
+ ...context,
699
+ quotaExceeded: true,
700
+ cause: domError.message
701
+ })
702
+ );
703
+ return;
704
+ }
705
+ reject(new PersistenceError(message, context));
706
+ }
359
707
  function openIdb() {
360
708
  return new Promise((resolve, reject) => {
361
709
  const request = indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
@@ -382,7 +730,12 @@ async function saveToIndexedDB(dbName, data) {
382
730
  const store = tx.objectStore(IDB_STORE_NAME);
383
731
  store.put(data, dbName);
384
732
  tx.oncomplete = () => resolve();
385
- tx.onerror = () => reject(new PersistenceError(`Failed to save database "${dbName}" to IndexedDB`, { dbName }));
733
+ tx.onerror = (event) => rejectWithIdbError(
734
+ reject,
735
+ `Failed to save database "${dbName}" to IndexedDB`,
736
+ { dbName },
737
+ event
738
+ );
386
739
  });
387
740
  } finally {
388
741
  idb.close();
@@ -396,7 +749,12 @@ async function saveDumpToIndexedDB(dbName, dump) {
396
749
  const store = tx.objectStore(IDB_STORE_NAME);
397
750
  store.put(dump, `${dbName}${DUMP_SUFFIX}`);
398
751
  tx.oncomplete = () => resolve();
399
- tx.onerror = () => reject(new PersistenceError(`Failed to save dump for database "${dbName}"`, { dbName }));
752
+ tx.onerror = (event) => rejectWithIdbError(
753
+ reject,
754
+ `Failed to save dump for database "${dbName}"`,
755
+ { dbName },
756
+ event
757
+ );
400
758
  });
401
759
  } finally {
402
760
  idb.close();
@@ -452,12 +810,21 @@ async function loadDumpFromIndexedDB(dbName) {
452
810
  var IndexedDbAdapter = class {
453
811
  inner;
454
812
  dbName;
813
+ emitter;
814
+ scheduler;
455
815
  constructor(options = {}) {
456
816
  this.dbName = options.dbName ?? "kora-db";
817
+ this.emitter = options.emitter;
457
818
  this.inner = new SqliteWasmAdapter({
458
819
  bridge: options.bridge,
459
820
  workerUrl: options.workerUrl,
460
- dbName: this.dbName
821
+ dbName: this.dbName,
822
+ workerResponseTimeoutMs: options.workerResponseTimeoutMs
823
+ });
824
+ this.scheduler = new IndexedDbPersistenceScheduler({
825
+ debounceMs: options.persistenceDebounceMs,
826
+ flush: () => this.writeSnapshot(),
827
+ onError: (error) => this.handlePersistenceError(error)
461
828
  });
462
829
  }
463
830
  async open(schema) {
@@ -471,32 +838,55 @@ var IndexedDbAdapter = class {
471
838
  }
472
839
  }
473
840
  async close() {
474
- await this.persistSnapshot();
841
+ await this.scheduler.flushNow();
842
+ this.scheduler.dispose();
475
843
  await this.inner.close();
476
844
  }
477
845
  async execute(sql, params) {
478
846
  await this.inner.execute(sql, params);
479
- await this.persistSnapshot();
847
+ this.scheduler.schedule();
480
848
  }
481
849
  async query(sql, params) {
482
850
  return this.inner.query(sql, params);
483
851
  }
484
852
  async transaction(fn) {
485
853
  await this.inner.transaction(fn);
486
- await this.persistSnapshot();
854
+ this.scheduler.schedule();
487
855
  }
488
856
  async migrate(from, to, migration) {
489
857
  await this.inner.migrate(from, to, migration);
490
- await this.persistSnapshot();
858
+ this.scheduler.schedule();
491
859
  }
492
- async persistSnapshot() {
493
- try {
494
- const data = await this.inner.exportDatabase();
495
- await saveToIndexedDB(this.dbName, data);
496
- const dump = await this.exportDump();
497
- await saveDumpToIndexedDB(this.dbName, dump);
498
- } catch {
860
+ /**
861
+ * Force an immediate snapshot write to IndexedDB (skips debounce).
862
+ * Useful before tab unload or in tests.
863
+ */
864
+ async flushPersistence() {
865
+ await this.scheduler.flushNow();
866
+ }
867
+ async writeSnapshot() {
868
+ const data = await this.inner.exportDatabase();
869
+ await saveToIndexedDB(this.dbName, data);
870
+ const dump = await this.exportDump();
871
+ await saveDumpToIndexedDB(this.dbName, dump);
872
+ }
873
+ handlePersistenceError(error) {
874
+ const message = error instanceof Error ? error.message : "IndexedDB persistence failed";
875
+ const code = error instanceof PersistenceError ? error.code : "PERSISTENCE_FAILED";
876
+ const quotaExceeded = isIndexedDbQuotaError(error);
877
+ if (quotaExceeded) {
878
+ this.emitter?.emit({
879
+ type: "store:quota-exceeded",
880
+ dbName: this.dbName,
881
+ message
882
+ });
499
883
  }
884
+ this.emitter?.emit({
885
+ type: "store:persistence-error",
886
+ dbName: this.dbName,
887
+ message,
888
+ code: quotaExceeded ? "QUOTA_EXCEEDED" : code
889
+ });
500
890
  }
501
891
  async restoreFromDumpFallback() {
502
892
  const dump = await loadDumpFromIndexedDB(this.dbName);
@@ -506,7 +896,9 @@ var IndexedDbAdapter = class {
506
896
  await this.inner.execute(`DELETE FROM ${name}`);
507
897
  if (table.rows.length === 0) continue;
508
898
  for (const row of table.rows) {
509
- const columns = table.columns.filter((column) => Object.prototype.hasOwnProperty.call(row, column));
899
+ const columns = table.columns.filter(
900
+ (column) => Object.prototype.hasOwnProperty.call(row, column)
901
+ );
510
902
  if (columns.length === 0) continue;
511
903
  const placeholders = columns.map(() => "?").join(", ");
512
904
  const quotedColumns = columns.map((column) => ensureSafeIdentifier(column)).join(", ");
@@ -539,7 +931,10 @@ var IndexedDbAdapter = class {
539
931
  };
540
932
  function ensureSafeIdentifier(identifier) {
541
933
  if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
542
- throw new AdapterError(`Unsafe SQL identifier: ${identifier}`);
934
+ throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
935
+ code: "UNSAFE_IDENTIFIER",
936
+ identifier
937
+ });
543
938
  }
544
939
  return identifier;
545
940
  }