@korajs/store 1.0.0-beta.2 → 1.0.0-beta.5
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.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/indexeddb.cjs +161 -35
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +2 -2
- package/dist/adapters/indexeddb.d.ts +2 -2
- package/dist/adapters/indexeddb.js +3 -3
- package/dist/adapters/sqlite-wasm-shared-host.cjs +333 -56
- package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.cts +81 -1
- package/dist/adapters/sqlite-wasm-shared-host.d.ts +81 -1
- package/dist/adapters/sqlite-wasm-shared-host.js +51 -56
- package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +206 -208
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +8 -257
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +161 -35
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +14 -3
- package/dist/adapters/sqlite-wasm.d.ts +14 -3
- package/dist/adapters/sqlite-wasm.js +3 -3
- package/dist/{backup-IQE4QC7Q.js → backup-5S7K2Z35.js} +3 -3
- package/dist/chunk-3Y2IERBF.js +255 -0
- package/dist/chunk-3Y2IERBF.js.map +1 -0
- package/dist/{chunk-IOEFNHHP.js → chunk-COJSQBXW.js} +2 -2
- package/dist/{chunk-RCTG3IJG.js → chunk-CYO3YKFL.js} +2 -2
- package/dist/{chunk-6NKLDBTY.js → chunk-GSJ7EEUC.js} +2 -1
- package/dist/chunk-GSJ7EEUC.js.map +1 -0
- package/dist/{chunk-OCOGQRHW.js → chunk-HS6DBMST.js} +2 -2
- package/dist/{chunk-QUJGXSZ6.js → chunk-Q57CHIIN.js} +154 -37
- package/dist/chunk-Q57CHIIN.js.map +1 -0
- package/dist/{chunk-2VRPF25R.js → chunk-RL43UYQZ.js} +3 -3
- package/dist/{chunk-QL2XVZRN.js → chunk-TUR7RGEP.js} +12 -1
- package/dist/{chunk-QL2XVZRN.js.map → chunk-TUR7RGEP.js.map} +1 -1
- package/dist/{export-audit-MT2LTMIN.js → export-audit-ODGQTQIK.js} +3 -3
- package/dist/index.cjs +132 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -5
- package/dist/index.d.ts +37 -5
- package/dist/index.js +131 -12
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +1 -0
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.cts → sqlite-wasm-channel-Bx57s8Cq.d.cts} +1 -1
- package/dist/{sqlite-wasm-channel-DUDPclTE.d.ts → sqlite-wasm-channel-Bx57s8Cq.d.ts} +1 -1
- package/dist/sqlite-wasm-channel-NGSBJR44.js +12 -0
- package/dist/{types-5h4Hqcwc.d.ts → types-C1kigrWz.d.ts} +1 -1
- package/dist/{types-k6AZgiH4.d.ts → types-D1MzKVmk.d.cts} +10 -0
- package/dist/{types-k6AZgiH4.d.cts → types-D1MzKVmk.d.ts} +10 -0
- package/dist/{types-DfPiPwKr.d.cts → types-DAo9oduO.d.cts} +1 -1
- package/package.json +2 -2
- package/dist/chunk-6NKLDBTY.js.map +0 -1
- package/dist/chunk-QUJGXSZ6.js.map +0 -1
- package/dist/sqlite-wasm-channel-H43KEZLM.js +0 -12
- /package/dist/{backup-IQE4QC7Q.js.map → backup-5S7K2Z35.js.map} +0 -0
- /package/dist/{chunk-IOEFNHHP.js.map → chunk-COJSQBXW.js.map} +0 -0
- /package/dist/{chunk-RCTG3IJG.js.map → chunk-CYO3YKFL.js.map} +0 -0
- /package/dist/{chunk-OCOGQRHW.js.map → chunk-HS6DBMST.js.map} +0 -0
- /package/dist/{chunk-2VRPF25R.js.map → chunk-RL43UYQZ.js.map} +0 -0
- /package/dist/{export-audit-MT2LTMIN.js.map → export-audit-ODGQTQIK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-H43KEZLM.js.map → sqlite-wasm-channel-NGSBJR44.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, StoreNotOpenError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["import_core"]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/better-sqlite3.ts","../../src/adapters/better-sqlite3-adapter.ts","../../src/errors.ts","../../src/adapters/mutex.ts"],"sourcesContent":["// Entry point for @korajs/store/better-sqlite3\nexport { BetterSqlite3Adapter } from './better-sqlite3-adapter'\n","import { generateFullDDL } from '@korajs/core'\nimport type { SchemaDefinition } from '@korajs/core'\nimport type Database from 'better-sqlite3'\nimport { AdapterError, StoreNotOpenError } from '../errors'\nimport type { MigrationPlan, StorageAdapter, Transaction } from '../types'\nimport { Mutex } from './mutex'\n\n/**\n * Storage adapter backed by better-sqlite3 for Node.js environments.\n * Used for testing and server-side usage.\n *\n * @example\n * ```typescript\n * import { BetterSqlite3Adapter } from '@korajs/store/better-sqlite3'\n *\n * const adapter = new BetterSqlite3Adapter(':memory:')\n * ```\n */\nexport class BetterSqlite3Adapter implements StorageAdapter {\n\tprivate db: Database.Database | null = null\n\n\t/**\n\t * Serializes transactions. better-sqlite3 is synchronous, but our\n\t * `transaction()` is async and yields the event loop at each `await` inside\n\t * the callback. Without this mutex, a transaction started while another is\n\t * mid-flight (e.g. a relayed remote operation applied during a local write)\n\t * issues a nested `BEGIN`, which SQLite rejects, and the operation is lost.\n\t */\n\tprivate readonly txMutex = new Mutex()\n\n\t/**\n\t * @param path - Database file path, or ':memory:' for in-memory database\n\t */\n\tconstructor(private readonly path: string = ':memory:') {}\n\n\tasync open(schema: SchemaDefinition): Promise<void> {\n\t\t// Dynamic import so better-sqlite3 is only loaded when this adapter is used\n\t\tconst BetterSqlite3 = (await import('better-sqlite3')).default\n\t\tthis.db = new BetterSqlite3(this.path)\n\n\t\t// WAL mode for better concurrent read/write performance\n\t\tthis.db.pragma('journal_mode = WAL')\n\t\t// Enable foreign keys\n\t\tthis.db.pragma('foreign_keys = ON')\n\n\t\tconst statements = generateFullDDL(schema)\n\t\tfor (const sql of statements) {\n\t\t\tif (sql.startsWith('--kora:safe-alter')) {\n\t\t\t\t// Safe ALTER TABLE — ignore \"duplicate column name\" errors for existing columns\n\t\t\t\ttry {\n\t\t\t\t\tthis.db.exec(sql.replace('--kora:safe-alter\\n', ''))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = (e as Error).message || ''\n\t\t\t\t\t// Tolerate duplicate columns (already exists) and NOT NULL without defaults\n\t\t\t\t\t// (column may be added or renamed by a migration step instead)\n\t\t\t\t\tif (\n\t\t\t\t\t\t!msg.includes('duplicate column name') &&\n\t\t\t\t\t\t!msg.includes('Cannot add a NOT NULL column with default value NULL')\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow e\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.db.exec(sql)\n\t\t\t}\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.db) {\n\t\t\tthis.db.close()\n\t\t\tthis.db = null\n\t\t}\n\t}\n\n\tasync execute(sql: string, params?: unknown[]): Promise<void> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Execute failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync query<T>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst db = this.getDb()\n\t\ttry {\n\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t} catch (error) {\n\t\t\tthrow new AdapterError(`Query failed: ${(error as Error).message}`, {\n\t\t\t\tsql,\n\t\t\t\tparams,\n\t\t\t})\n\t\t}\n\t}\n\n\tasync transaction(fn: (tx: Transaction) => Promise<void>): Promise<void> {\n\t\tconst db = this.getDb()\n\n\t\t// Serialize with any other in-flight transaction so BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\t// better-sqlite3's transaction() is synchronous, but our interface is async.\n\t\t\t// We use BEGIN/COMMIT/ROLLBACK manually for the async callback.\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tconst tx: Transaction = {\n\t\t\t\t\texecute: async (sql: string, params?: unknown[]): Promise<void> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tdb.prepare(sql).run(...(params ?? []))\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction execute failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tquery: async <T>(sql: string, params?: unknown[]): Promise<T[]> => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn db.prepare(sql).all(...(params ?? [])) as T[]\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tthrow new AdapterError(`Transaction query failed: ${(error as Error).message}`, {\n\t\t\t\t\t\t\t\tsql,\n\t\t\t\t\t\t\t\tparams,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tawait fn(tx)\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tasync migrate(from: number, to: number, migration: MigrationPlan): Promise<void> {\n\t\tconst db = this.getDb()\n\t\t// Serialize against transactions so migration's BEGIN is never nested.\n\t\tconst release = await this.txMutex.acquire()\n\t\ttry {\n\t\t\tdb.exec('BEGIN')\n\t\t\ttry {\n\t\t\t\tfor (const sql of migration.statements) {\n\t\t\t\t\tdb.exec(sql)\n\t\t\t\t}\n\t\t\t\tdb.exec('COMMIT')\n\t\t\t} catch (error) {\n\t\t\t\tdb.exec('ROLLBACK')\n\t\t\t\tthrow new AdapterError(\n\t\t\t\t\t`Migration from v${from} to v${to} failed: ${(error as Error).message}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t\tto,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t} finally {\n\t\t\trelease()\n\t\t}\n\t}\n\n\tprivate getDb(): Database.Database {\n\t\tif (!this.db) {\n\t\t\tthrow new StoreNotOpenError()\n\t\t}\n\t\treturn this.db\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n/**\n * Thrown when a query is invalid (bad field names, invalid operators, etc.).\n */\nexport class QueryError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'QUERY_ERROR', context)\n\t\tthis.name = 'QueryError'\n\t}\n}\n\n/**\n * Thrown when a record is not found by ID (findById, update, delete on missing record).\n */\nexport class RecordNotFoundError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(`Record \"${recordId}\" not found in collection \"${collection}\"`, 'RECORD_NOT_FOUND', {\n\t\t\tcollection,\n\t\t\trecordId,\n\t\t})\n\t\tthis.name = 'RecordNotFoundError'\n\t}\n}\n\n/**\n * Thrown inside `applyRemoteOperation` when the row's version state changed\n * between the caller's snapshot read and the guarded write (a concurrent local\n * mutation landed in the window). The transaction is rolled back with nothing\n * written; the caller re-reads fresh state, recomputes its merge, and retries.\n * This is the optimistic-concurrency guard that keeps merge-engine results\n * (richtext, add-wins arrays, constraint resolutions) from clobbering newer\n * local edits they never saw.\n */\nexport class OptimisticLockError extends KoraError {\n\tconstructor(collection: string, recordId: string) {\n\t\tsuper(\n\t\t\t`Row version changed while merging record \"${recordId}\" in \"${collection}\"; retry with fresh state`,\n\t\t\t'OPTIMISTIC_LOCK',\n\t\t\t{ collection, recordId },\n\t\t)\n\t\tthis.name = 'OptimisticLockError'\n\t}\n}\n\n/**\n * Thrown when a storage adapter operation fails.\n */\nexport class AdapterError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ADAPTER_ERROR', context)\n\t\tthis.name = 'AdapterError'\n\t}\n}\n\n/**\n * Thrown when an operation is attempted on a store that has not been opened.\n */\nexport class StoreNotOpenError extends KoraError {\n\tconstructor() {\n\t\tsuper('Store is not open. Call store.open() before performing operations.', 'STORE_NOT_OPEN')\n\t\tthis.name = 'StoreNotOpenError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker fails to initialize (WASM load failure, OPFS unavailable, etc.).\n */\nexport class WorkerInitError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Worker initialization failed: ${message}`, 'WORKER_INIT_ERROR', context)\n\t\tthis.name = 'WorkerInitError'\n\t}\n}\n\n/**\n * Thrown when the Web Worker does not respond within the configured timeout.\n */\nexport class WorkerTimeoutError extends KoraError {\n\tconstructor(operation: string, timeoutMs: number) {\n\t\tsuper(\n\t\t\t`Worker did not respond within ${timeoutMs}ms for operation \"${operation}\"`,\n\t\t\t'WORKER_TIMEOUT',\n\t\t\t{ operation, timeoutMs },\n\t\t)\n\t\tthis.name = 'WorkerTimeoutError'\n\t}\n}\n\n/**\n * Thrown when a follower tab cannot reach a live leader tab over the multi-tab\n * broadcast channel (the leader tab was closed, crashed, or is unresponsive).\n * Distinct from {@link WorkerTimeoutError}: this fails fast on a confirmed-absent\n * leader instead of waiting out the full RPC timeout.\n */\nexport class NoLeaderError extends KoraError {\n\tconstructor(operation: string) {\n\t\tsuper(\n\t\t\t`No live leader tab is answering multi-tab storage RPC for operation \"${operation}\"`,\n\t\t\t'NO_LEADER',\n\t\t\t{ operation },\n\t\t)\n\t\tthis.name = 'NoLeaderError'\n\t}\n}\n\n/**\n * Thrown when IndexedDB persistence operations fail (serialize/deserialize).\n */\nexport class PersistenceError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(`Persistence error: ${message}`, 'PERSISTENCE_ERROR', context)\n\t\tthis.name = 'PersistenceError'\n\t}\n}\n","/**\n * Async mutex for serializing access to a resource across `await` boundaries.\n *\n * This exists because an adapter's `transaction()` is async (its callback may\n * await), but the underlying database transaction (BEGIN…COMMIT) is not\n * reentrant: starting a second transaction while one is open is an error. When\n * a client applies a relayed remote operation while a local write is in flight,\n * the two `transaction()` calls interleave at their `await` points, and without\n * serialization the second `BEGIN` collides with the first, silently dropping\n * the operation. Serializing every transaction through this mutex guarantees at\n * most one is active at a time.\n */\nexport class Mutex {\n\tprivate locked = false\n\tprivate readonly waiters: Array<() => void> = []\n\n\t/**\n\t * Acquire the mutex. Resolves with a release function once the lock is held.\n\t * If the mutex is already held, the caller waits (FIFO) until it is released.\n\t */\n\tasync acquire(): Promise<() => void> {\n\t\tif (!this.locked) {\n\t\t\tthis.locked = true\n\t\t\treturn this.createRelease()\n\t\t}\n\n\t\treturn new Promise<() => void>((resolve) => {\n\t\t\tthis.waiters.push(() => {\n\t\t\t\tresolve(this.createRelease())\n\t\t\t})\n\t\t})\n\t}\n\n\tprivate createRelease(): () => void {\n\t\tlet released = false\n\t\treturn () => {\n\t\t\tif (released) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treleased = true\n\t\t\tconst next = this.waiters.shift()\n\t\t\tif (next) {\n\t\t\t\tnext()\n\t\t\t} else {\n\t\t\t\tthis.locked = false\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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","names":["import_core"]}
|
|
@@ -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-D1MzKVmk.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-D1MzKVmk.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Storage adapter backed by better-sqlite3 for Node.js environments.
|
|
@@ -21,7 +21,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
21
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
22
|
|
|
23
23
|
// src/errors.ts
|
|
24
|
-
var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, PersistenceError;
|
|
24
|
+
var import_core, AdapterError, StoreNotOpenError, WorkerTimeoutError, NoLeaderError, PersistenceError;
|
|
25
25
|
var init_errors = __esm({
|
|
26
26
|
"src/errors.ts"() {
|
|
27
27
|
"use strict";
|
|
@@ -48,6 +48,16 @@ var init_errors = __esm({
|
|
|
48
48
|
this.name = "WorkerTimeoutError";
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
NoLeaderError = class extends import_core.KoraError {
|
|
52
|
+
constructor(operation) {
|
|
53
|
+
super(
|
|
54
|
+
`No live leader tab is answering multi-tab storage RPC for operation "${operation}"`,
|
|
55
|
+
"NO_LEADER",
|
|
56
|
+
{ operation }
|
|
57
|
+
);
|
|
58
|
+
this.name = "NoLeaderError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
51
61
|
PersistenceError = class extends import_core.KoraError {
|
|
52
62
|
constructor(message, context) {
|
|
53
63
|
super(`Persistence error: ${message}`, "PERSISTENCE_ERROR", context);
|
|
@@ -353,45 +363,73 @@ var SharedWorkerClientBridge = class {
|
|
|
353
363
|
init_errors();
|
|
354
364
|
var RPC_REQUEST = "kora-worker-request";
|
|
355
365
|
var RPC_RESPONSE = "kora-worker-response";
|
|
366
|
+
var LEADER_PING = "kora-leader-ping";
|
|
367
|
+
var LEADER_PONG = "kora-leader-pong";
|
|
368
|
+
var DEFAULT_LIVENESS_PROBE_MS = 2e3;
|
|
356
369
|
function isSharedWorkerStorageSupported() {
|
|
357
370
|
return typeof globalThis !== "undefined" && typeof SharedWorker !== "undefined";
|
|
358
371
|
}
|
|
359
372
|
function isMultiTabStorageSupported() {
|
|
360
373
|
return typeof globalThis !== "undefined" && typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
|
|
361
374
|
}
|
|
362
|
-
async function acquireTabStorageSession(dbName) {
|
|
375
|
+
async function acquireTabStorageSession(dbName, options) {
|
|
363
376
|
const channelName = `kora-storage-${dbName}`;
|
|
377
|
+
const lockName = `kora-leader-${dbName}`;
|
|
364
378
|
if (!isMultiTabStorageSupported()) {
|
|
365
379
|
return { role: "leader", channelName };
|
|
366
380
|
}
|
|
367
381
|
return new Promise((resolve) => {
|
|
368
382
|
let releaseHeld;
|
|
369
|
-
void navigator.locks.request(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (lock === null) {
|
|
374
|
-
resolve({ role: "follower", channelName });
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
resolve({
|
|
378
|
-
role: "leader",
|
|
379
|
-
channelName,
|
|
380
|
-
releaseLock: async () => {
|
|
381
|
-
releaseHeld?.();
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
return new Promise((release) => {
|
|
385
|
-
releaseHeld = release;
|
|
386
|
-
});
|
|
383
|
+
void navigator.locks.request(lockName, { mode: "exclusive", ifAvailable: true }, (lock) => {
|
|
384
|
+
if (lock === null) {
|
|
385
|
+
resolve(startFollowerSession(channelName, lockName, options));
|
|
386
|
+
return;
|
|
387
387
|
}
|
|
388
|
-
|
|
388
|
+
resolve({
|
|
389
|
+
role: "leader",
|
|
390
|
+
channelName,
|
|
391
|
+
releaseLock: async () => {
|
|
392
|
+
releaseHeld?.();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return new Promise((release) => {
|
|
396
|
+
releaseHeld = release;
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function startFollowerSession(channelName, lockName, options) {
|
|
402
|
+
let promoted = false;
|
|
403
|
+
let releasePromotedLock;
|
|
404
|
+
const abort = new AbortController();
|
|
405
|
+
void navigator.locks.request(lockName, { mode: "exclusive", signal: abort.signal }, () => {
|
|
406
|
+
promoted = true;
|
|
407
|
+
options?.onPromote?.();
|
|
408
|
+
return new Promise((release) => {
|
|
409
|
+
releasePromotedLock = release;
|
|
410
|
+
});
|
|
411
|
+
}).catch(() => {
|
|
389
412
|
});
|
|
413
|
+
return {
|
|
414
|
+
role: "follower",
|
|
415
|
+
channelName,
|
|
416
|
+
cancelPromotionWatch: () => {
|
|
417
|
+
if (promoted) {
|
|
418
|
+
releasePromotedLock?.();
|
|
419
|
+
} else {
|
|
420
|
+
abort.abort();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
390
424
|
}
|
|
391
425
|
function startLeaderRpcRelay(channelName, bridge) {
|
|
392
426
|
const channel = new BroadcastChannel(channelName);
|
|
393
427
|
const onMessage = (event) => {
|
|
394
428
|
const data = event.data;
|
|
429
|
+
if (data?.type === LEADER_PING) {
|
|
430
|
+
channel.postMessage({ type: LEADER_PONG });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
395
433
|
if (data?.type !== RPC_REQUEST) {
|
|
396
434
|
return;
|
|
397
435
|
}
|
|
@@ -427,9 +465,11 @@ var FollowerBroadcastBridge = class {
|
|
|
427
465
|
channel;
|
|
428
466
|
pending = /* @__PURE__ */ new Map();
|
|
429
467
|
timeoutMs;
|
|
468
|
+
livenessProbeMs;
|
|
430
469
|
terminated = false;
|
|
431
|
-
constructor(channelName, timeoutMs = 3e4) {
|
|
470
|
+
constructor(channelName, timeoutMs = 3e4, livenessProbeMs = DEFAULT_LIVENESS_PROBE_MS) {
|
|
432
471
|
this.timeoutMs = timeoutMs;
|
|
472
|
+
this.livenessProbeMs = Math.min(livenessProbeMs, timeoutMs);
|
|
433
473
|
this.channel = new BroadcastChannel(channelName);
|
|
434
474
|
this.channel.addEventListener("message", (event) => {
|
|
435
475
|
const data = event.data;
|
|
@@ -443,6 +483,48 @@ var FollowerBroadcastBridge = class {
|
|
|
443
483
|
}
|
|
444
484
|
});
|
|
445
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Readiness handshake: resolves `true` as soon as a live leader answers a ping,
|
|
488
|
+
* or `false` if none answers within the budget. The adapter calls this before
|
|
489
|
+
* its first RPC so a follower created before a leader relay is live retries the
|
|
490
|
+
* handshake instead of firing into the void and waiting out the full timeout.
|
|
491
|
+
*/
|
|
492
|
+
async waitForLeader(timeoutMs = 3e3, attempts = 3) {
|
|
493
|
+
const perAttempt = Math.max(50, Math.floor(timeoutMs / Math.max(1, attempts)));
|
|
494
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
495
|
+
if (this.terminated) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
if (await this.pingLeader(perAttempt)) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
/** Sends one liveness ping and resolves whether a leader answered in time. */
|
|
505
|
+
pingLeader(timeoutMs) {
|
|
506
|
+
if (this.terminated) {
|
|
507
|
+
return Promise.resolve(false);
|
|
508
|
+
}
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
const onPong = (event) => {
|
|
511
|
+
if (event.data?.type === LEADER_PONG) {
|
|
512
|
+
cleanup();
|
|
513
|
+
resolve(true);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const cleanup = () => {
|
|
517
|
+
clearTimeout(timer);
|
|
518
|
+
this.channel.removeEventListener("message", onPong);
|
|
519
|
+
};
|
|
520
|
+
const timer = setTimeout(() => {
|
|
521
|
+
cleanup();
|
|
522
|
+
resolve(false);
|
|
523
|
+
}, timeoutMs);
|
|
524
|
+
this.channel.addEventListener("message", onPong);
|
|
525
|
+
this.channel.postMessage({ type: LEADER_PING });
|
|
526
|
+
});
|
|
527
|
+
}
|
|
446
528
|
async send(request) {
|
|
447
529
|
if (this.terminated) {
|
|
448
530
|
return {
|
|
@@ -455,19 +537,25 @@ var FollowerBroadcastBridge = class {
|
|
|
455
537
|
const requestId = crypto.randomUUID();
|
|
456
538
|
const msg = { type: RPC_REQUEST, requestId, request };
|
|
457
539
|
return new Promise((resolve, reject) => {
|
|
458
|
-
const
|
|
540
|
+
const settle = (fn) => {
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
clearTimeout(probe);
|
|
459
543
|
this.pending.delete(requestId);
|
|
460
|
-
|
|
544
|
+
fn();
|
|
545
|
+
};
|
|
546
|
+
const timer = setTimeout(() => {
|
|
547
|
+
settle(() => reject(new WorkerTimeoutError(`follower-rpc:${request.type}`, this.timeoutMs)));
|
|
461
548
|
}, this.timeoutMs);
|
|
549
|
+
const probe = setTimeout(() => {
|
|
550
|
+
void this.pingLeader(this.livenessProbeMs).then((alive) => {
|
|
551
|
+
if (!alive && this.pending.has(requestId)) {
|
|
552
|
+
settle(() => reject(new NoLeaderError(`follower-rpc:${request.type}`)));
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}, this.livenessProbeMs);
|
|
462
556
|
this.pending.set(requestId, {
|
|
463
|
-
resolve: (response) =>
|
|
464
|
-
|
|
465
|
-
resolve(response);
|
|
466
|
-
},
|
|
467
|
-
reject: (error) => {
|
|
468
|
-
clearTimeout(timer);
|
|
469
|
-
reject(error);
|
|
470
|
-
}
|
|
557
|
+
resolve: (response) => settle(() => resolve(response)),
|
|
558
|
+
reject: (error) => settle(() => reject(error))
|
|
471
559
|
});
|
|
472
560
|
this.channel.postMessage(msg);
|
|
473
561
|
});
|
|
@@ -499,6 +587,9 @@ var SqliteWasmAdapter = class {
|
|
|
499
587
|
emitter;
|
|
500
588
|
tabSession = null;
|
|
501
589
|
sharedWorker = null;
|
|
590
|
+
/** Retained so a follower promoted to leader can re-open its own worker. */
|
|
591
|
+
schema = null;
|
|
592
|
+
promoting = false;
|
|
502
593
|
constructor(options = {}) {
|
|
503
594
|
this.injectedBridge = options.bridge;
|
|
504
595
|
this.workerUrl = options.workerUrl;
|
|
@@ -514,25 +605,35 @@ var SqliteWasmAdapter = class {
|
|
|
514
605
|
} else if (this.sharedWorkerUrl && this.workerUrl && isSharedWorkerStorageSupported()) {
|
|
515
606
|
const workerUrlString = typeof this.workerUrl === "string" ? this.workerUrl : this.workerUrl.href;
|
|
516
607
|
const sharedUrl = typeof this.sharedWorkerUrl === "string" ? this.sharedWorkerUrl : this.sharedWorkerUrl.href;
|
|
517
|
-
this.sharedWorker = new SharedWorker(sharedUrl, {
|
|
608
|
+
this.sharedWorker = new SharedWorker(sharedUrl, {
|
|
609
|
+
type: "module",
|
|
610
|
+
name: `kora-sw-${this.dbName}`
|
|
611
|
+
});
|
|
518
612
|
this.bridge = new SharedWorkerClientBridge(this.sharedWorker, this.dbName, workerUrlString);
|
|
519
613
|
} else if (this.workerUrl) {
|
|
520
|
-
this.
|
|
614
|
+
this.schema = schema;
|
|
615
|
+
this.tabSession = await acquireTabStorageSession(this.dbName, {
|
|
616
|
+
onPromote: () => {
|
|
617
|
+
void this.promoteToLeader();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
521
620
|
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
522
621
|
if (this.tabSession.role === "leader") {
|
|
523
622
|
const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
|
|
524
623
|
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
525
624
|
this.bridge = workerBridge;
|
|
526
625
|
} else {
|
|
527
|
-
|
|
626
|
+
const followerBridge = new FollowerBroadcastBridge(
|
|
528
627
|
this.tabSession.channelName,
|
|
529
628
|
this.workerResponseTimeoutMs
|
|
530
629
|
);
|
|
630
|
+
this.bridge = followerBridge;
|
|
531
631
|
this.emitter?.emit({
|
|
532
632
|
type: "store:db-name-collision",
|
|
533
633
|
dbName: this.dbName,
|
|
534
634
|
message: `Another runtime on this origin is already using database "${this.dbName}"; this runtime is sharing it as a follower. If these are separate apps, give each a distinct store name.`
|
|
535
635
|
});
|
|
636
|
+
await followerBridge.waitForLeader();
|
|
536
637
|
}
|
|
537
638
|
} else {
|
|
538
639
|
throw new AdapterError(
|
|
@@ -582,6 +683,7 @@ var SqliteWasmAdapter = class {
|
|
|
582
683
|
await this.sendRequest({ id: 0, type: "close" });
|
|
583
684
|
} finally {
|
|
584
685
|
this.tabSession?.stopRelay?.();
|
|
686
|
+
this.tabSession?.cancelPromotionWatch?.();
|
|
585
687
|
if (this.tabSession?.releaseLock) {
|
|
586
688
|
await this.tabSession.releaseLock();
|
|
587
689
|
}
|
|
@@ -592,6 +694,30 @@ var SqliteWasmAdapter = class {
|
|
|
592
694
|
this.opened = false;
|
|
593
695
|
}
|
|
594
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* Promotes a follower to leader after the previous leader released the storage
|
|
699
|
+
* lock (its tab closed or crashed). The lock is only granted once the old leader
|
|
700
|
+
* is gone, so opening our own worker here is safe from the OPFS single-writer
|
|
701
|
+
* rule. In-flight follower RPCs are rejected and left to the caller / reactive
|
|
702
|
+
* layer to retry against the new leader.
|
|
703
|
+
*/
|
|
704
|
+
async promoteToLeader() {
|
|
705
|
+
if (this.promoting || !this.opened || this.workerUrl === void 0 || this.schema === null) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.promoting = true;
|
|
709
|
+
const { WebWorkerBridge: WebWorkerBridge2 } = await Promise.resolve().then(() => (init_sqlite_wasm_channel(), sqlite_wasm_channel_exports));
|
|
710
|
+
const workerBridge = new WebWorkerBridge2(this.workerUrl, this.workerResponseTimeoutMs);
|
|
711
|
+
const previousBridge = this.bridge;
|
|
712
|
+
this.bridge = workerBridge;
|
|
713
|
+
if (this.tabSession) {
|
|
714
|
+
this.tabSession.role = "leader";
|
|
715
|
+
this.tabSession.stopRelay = startLeaderRpcRelay(this.tabSession.channelName, workerBridge);
|
|
716
|
+
}
|
|
717
|
+
previousBridge?.terminate();
|
|
718
|
+
const ddlStatements = (0, import_core2.generateFullDDL)(this.schema);
|
|
719
|
+
await this.sendRequest({ id: 0, type: "open", ddlStatements, dbName: this.dbName });
|
|
720
|
+
}
|
|
595
721
|
async execute(sql, params) {
|
|
596
722
|
this.guardOpen();
|
|
597
723
|
const response = await this.sendRequest({ id: 0, type: "execute", sql, params });
|