@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.
- package/dist/adapters/better-sqlite3.cjs +1 -1
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.js +1 -1
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +413 -18
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +23 -5
- package/dist/adapters/indexeddb.d.ts +23 -5
- package/dist/adapters/indexeddb.js +166 -19
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
- package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
- package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
- package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +34 -5
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +249 -2
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +15 -3
- package/dist/adapters/sqlite-wasm.d.ts +15 -3
- package/dist/adapters/sqlite-wasm.js +2 -2
- package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
- package/dist/backup-JN3LB4YT.js +14 -0
- package/dist/backup-JN3LB4YT.js.map +1 -0
- package/dist/chunk-56LL4XP6.js +205 -0
- package/dist/chunk-56LL4XP6.js.map +1 -0
- package/dist/chunk-6AHYGQQT.js +425 -0
- package/dist/chunk-6AHYGQQT.js.map +1 -0
- package/dist/chunk-7BPETY5Y.js +570 -0
- package/dist/chunk-7BPETY5Y.js.map +1 -0
- package/dist/chunk-AGN3CIPB.js +188 -0
- package/dist/chunk-AGN3CIPB.js.map +1 -0
- package/dist/chunk-EZSTD2XC.js +380 -0
- package/dist/chunk-EZSTD2XC.js.map +1 -0
- package/dist/chunk-MYSJN3DF.js +117 -0
- package/dist/chunk-MYSJN3DF.js.map +1 -0
- package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
- package/dist/chunk-XO4LMPAH.js.map +1 -0
- package/dist/export-audit-5GV6VRDZ.js +15 -0
- package/dist/export-audit-5GV6VRDZ.js.map +1 -0
- package/dist/index.cjs +3351 -510
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +822 -50
- package/dist/index.d.ts +822 -50
- package/dist/index.js +1976 -558
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +789 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +154 -0
- package/dist/internal.d.ts +154 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
- package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
- package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
- package/dist/types-BMyHmwvn.d.ts +163 -0
- package/dist/types-CTr00g_d.d.cts +163 -0
- package/dist/types-DepnRDk_.d.cts +212 -0
- package/dist/types-DepnRDk_.d.ts +212 -0
- package/package.json +23 -2
- package/dist/chunk-DXKLAQ6P.js.map +0 -1
- package/dist/chunk-ZP5AXQ3Z.js +0 -179
- package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
- package/dist/types-DF-KDSK1.d.cts +0 -106
- package/dist/types-DF-KDSK1.d.ts +0 -106
- /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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAAgC;;;ACAhC,kBAA0B;AA4BnB,IAAM,eAAN,cAA2B,sBAAU;AAAA,EAC3C,YAAY,SAAiB,SAAmC;AAC/D,UAAM,SAAS,iBAAiB,OAAO;AACvC,SAAK,OAAO;AAAA,EACb;AACD;AAKO,IAAM,oBAAN,cAAgC,sBAAU;AAAA,EAChD,cAAc;AACb,UAAM,sEAAsE,gBAAgB;AAC5F,SAAK,OAAO;AAAA,EACb;AACD;;;AD1BO,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,iBAAa,8BAAgB,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;
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAAgC;;;ACAhC,kBAA0B;AA4BnB,IAAM,eAAN,cAA2B,sBAAU;AAAA,EAC3C,YAAY,SAAiB,SAAmC;AAC/D,UAAM,SAAS,iBAAiB,OAAO;AACvC,SAAK,OAAO;AAAA,EACb;AACD;AAKO,IAAM,oBAAN,cAAgC,sBAAU;AAAA,EAChD,cAAc;AACb,UAAM,sEAAsE,gBAAgB;AAC5F,SAAK,OAAO;AAAA,EACb;AACD;;;AD1BO,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,iBAAa,8BAAgB,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;AAGpC,cACC,CAAC,IAAI,SAAS,uBAAuB,KACrC,CAAC,IAAI,SAAS,sDAAsD,GACnE;AACD,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":["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-
|
|
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-
|
|
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;
|
|
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.
|
|
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({
|
|
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 = () =>
|
|
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 = () =>
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
854
|
+
this.scheduler.schedule();
|
|
487
855
|
}
|
|
488
856
|
async migrate(from, to, migration) {
|
|
489
857
|
await this.inner.migrate(from, to, migration);
|
|
490
|
-
|
|
858
|
+
this.scheduler.schedule();
|
|
491
859
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
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
|
|
934
|
+
throw new PersistenceError(`Unsafe SQL identifier: ${identifier}`, {
|
|
935
|
+
code: "UNSAFE_IDENTIFIER",
|
|
936
|
+
identifier
|
|
937
|
+
});
|
|
543
938
|
}
|
|
544
939
|
return identifier;
|
|
545
940
|
}
|