@loro-dev/unisqlite
Advanced tools
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return s}}); |
| {"version":3,"file":"node3.cjs","names":["BaseAdapter","Database"],"sources":["../src/adapters/node.ts"],"sourcesContent":["import { BaseAdapter } from \"./base.js\";\nimport type {\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n UniStoreConnection,\n UniStoreOptions,\n ConnectionType,\n} from \"../types.js\";\nimport Database from \"better-sqlite3\";\n\nexport class NodeAdapter extends BaseAdapter {\n private db: Database.Database;\n private _closed: boolean = false;\n\n constructor(options: UniStoreOptions) {\n super(options);\n // Open database with WAL mode for better concurrency\n this.db = new Database(options.path, { fileMustExist: false });\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\"); // 5 second timeout for locks\n }\n\n private normalizeParams(params?: SQLiteParams): unknown[] {\n if (!params) return [];\n if (Array.isArray(params)) return params;\n // Convert object params to array format for better-sqlite3\n // This is a simplified conversion - in practice you'd need to handle named parameters\n return Object.values(params);\n }\n\n private convertBuffersToUint8Array<T>(value: T): T {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Buffer.isBuffer(value)) {\n return new Uint8Array(value) as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => this.convertBuffersToUint8Array(item)) as T;\n }\n\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = this.convertBuffersToUint8Array(val);\n }\n return result as T;\n }\n\n return value;\n }\n\n private checkConnection(): void {\n if (this._closed || !this.db.open) {\n throw new Error(\"Database connection is closed\");\n }\n }\n\n async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n return this.convertBuffersToUint8Array(rows);\n }\n\n async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const columns = stmt.columns().map((col) => col.name);\n\n return {\n rows: this.convertBuffersToUint8Array(rows),\n columns,\n rowsAffected: 0,\n lastInsertRowId: 0,\n };\n }\n\n async run(sql: string, params?: SQLiteParams): Promise<RunResult> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n return {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n }\n\n async exec(sql: string): Promise<void> {\n this.checkConnection();\n this.db.exec(sql);\n }\n\n transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n this.checkConnection();\n // better-sqlite3 supports synchronous transactions\n this.db.exec(\"BEGIN\");\n const transactionAdapter = new SyncTransactionAdapter(this.db);\n return (async () => {\n try {\n const result = await fn(transactionAdapter);\n this.db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n })();\n }\n\n /**\n * Execute an async transaction with manual transaction management.\n * This allows async operations within the transaction but comes with timeout support\n * to prevent long-running transactions from blocking the database.\n *\n * @param fn Transaction function that can contain async operations\n * @param options Transaction options including timeout (default: 30000ms)\n */\n async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, options?: { timeoutMs?: number }): Promise<T> {\n this.checkConnection();\n\n const timeoutMs = options?.timeoutMs ?? 30000; // Default 30 seconds\n let alreadyInTransaction = false;\n\n // Manual transaction management using BEGIN/COMMIT/ROLLBACK\n try {\n this.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n if ((e as Error).message.includes(\"cannot start a transaction within a transaction\")) {\n alreadyInTransaction = true;\n } else {\n throw e;\n }\n }\n\n let timeoutHandle: NodeJS.Timeout | undefined;\n let transactionCompleted = false;\n\n try {\n // Set up timeout\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => {\n if (!transactionCompleted) {\n reject(new Error(`Async transaction timeout after ${timeoutMs}ms`));\n }\n }, timeoutMs);\n });\n\n const transactionAdapter = new AsyncTransactionAdapter(this.db);\n\n // Race between transaction execution and timeout\n const result = await Promise.race([fn(transactionAdapter), timeoutPromise]);\n\n transactionCompleted = true;\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n }\n\n // If we get here, the transaction completed successfully\n if (!alreadyInTransaction) {\n this.db.exec(\"COMMIT\");\n }\n return result;\n } catch (error) {\n transactionCompleted = true;\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n }\n\n try {\n this.db.exec(\"ROLLBACK\");\n } catch (rollbackError) {\n console.error(\"Rollback failed:\", rollbackError);\n }\n\n throw error;\n }\n }\n\n getConnectionType(): ConnectionType {\n return \"direct\";\n }\n\n async close(): Promise<void> {\n if (!this._closed && this.db.open) {\n this.db.close();\n this._closed = true;\n }\n }\n\n get isOpen(): boolean {\n return !this._closed && this.db.open;\n }\n}\n\nabstract class BaseTransactionAdapter implements UniStoreConnection {\n public readonly inTransaction = true;\n\n constructor(protected db: Database.Database) {}\n\n private normalizeParams(params?: SQLiteParams): unknown[] {\n if (!params) return [];\n if (Array.isArray(params)) return params;\n return Object.values(params);\n }\n\n private convertBuffersToUint8Array<T>(value: T): T {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Buffer.isBuffer(value)) {\n return new Uint8Array(value) as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => this.convertBuffersToUint8Array(item)) as T;\n }\n\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = this.convertBuffersToUint8Array(val);\n }\n return result as T;\n }\n\n return value;\n }\n\n // Synchronous database methods for use in transactions\n query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const result = this.convertBuffersToUint8Array(rows);\n return Promise.resolve(result);\n }\n\n queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const columns = stmt.columns().map((col) => col.name);\n\n const result = {\n rows: this.convertBuffersToUint8Array(rows),\n columns,\n rowsAffected: 0,\n lastInsertRowId: 0,\n };\n return Promise.resolve(result);\n }\n\n run(sql: string, params?: SQLiteParams): Promise<RunResult> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n const result = {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n return Promise.resolve(result);\n }\n\n exec(sql: string): Promise<void> {\n this.db.exec(sql);\n return Promise.resolve();\n }\n\n // Synchronous versions for use in sync transactions\n querySync<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): T[] {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n return this.convertBuffersToUint8Array(rows);\n }\n\n runSync(sql: string, params?: SQLiteParams): RunResult {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n return {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n }\n\n execSync(sql: string): void {\n this.db.exec(sql);\n }\n\n abstract getConnectionType(): ConnectionType;\n abstract transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T>;\n abstract asyncTransaction<T>(\n fn: (tx: UniStoreConnection) => Promise<T>,\n options?: { timeoutMs?: number }\n ): Promise<T>;\n\n async close(): Promise<void> {\n // No-op for transaction\n }\n}\n\nclass SyncTransactionAdapter extends BaseTransactionAdapter {\n getConnectionType(): ConnectionType {\n return \"syncTxn\";\n }\n\n async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n // For syncTxn, we can execute the transaction using itself as the connection\n // This allows nested operations within the same transaction\n return await fn(this);\n }\n\n async asyncTransaction<T>(\n _fn: (tx: UniStoreConnection) => Promise<T>,\n _options?: { timeoutMs?: number }\n ): Promise<T> {\n // syncTxn connections cannot run asyncTransaction\n throw new Error(\n \"asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.\"\n );\n }\n}\n\nclass AsyncTransactionAdapter extends BaseTransactionAdapter {\n getConnectionType(): ConnectionType {\n return \"asyncTxn\";\n }\n\n async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n // For asyncTxn, we can execute the transaction using itself as the connection\n // This allows nested operations within the same transaction\n return await fn(this);\n }\n\n async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, _options?: { timeoutMs?: number }): Promise<T> {\n // For asyncTxn, we can execute asyncTransaction using itself as the connection\n // This allows nested async operations within the same transaction\n return await fn(this);\n }\n}\n"],"mappings":"gGAYA,IAAa,EAAb,cAAiCA,EAAAA,CAAY,CAI3C,YAAY,EAA0B,CACpC,MAAM,EAAQ,cAHW,GAKzB,KAAK,GAAK,IAAIC,EAAAA,QAAS,EAAQ,KAAM,CAAE,cAAe,GAAO,CAAC,CAC9D,KAAK,GAAG,OAAO,qBAAqB,CACpC,KAAK,GAAG,OAAO,sBAAsB,CAGvC,gBAAwB,EAAkC,CAKxD,OAJK,EACD,MAAM,QAAQ,EAAO,CAAS,EAG3B,OAAO,OAAO,EAAO,CAJR,EAAE,CAOxB,2BAAsC,EAAa,CACjD,GAAI,GAAU,KACZ,OAAO,EAGT,GAAI,OAAO,SAAS,EAAM,CACxB,OAAO,IAAI,WAAW,EAAM,CAG9B,GAAI,MAAM,QAAQ,EAAM,CACtB,OAAO,EAAM,IAAK,GAAS,KAAK,2BAA2B,EAAK,CAAC,CAGnE,GAAI,OAAO,GAAU,SAAU,CAC7B,IAAM,EAAkC,EAAE,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAQ,OAAO,QAAQ,EAAM,CAC5C,EAAO,GAAO,KAAK,2BAA2B,EAAI,CAEpD,OAAO,EAGT,OAAO,EAGT,iBAAgC,CAC9B,GAAI,KAAK,SAAW,CAAC,KAAK,GAAG,KAC3B,MAAU,MAAM,gCAAgC,CAIpD,MAAM,MAAuC,EAAa,EAAqC,CAC7F,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAClC,OAAO,KAAK,2BAA2B,EAAK,CAG9C,MAAM,SAA0C,EAAa,EAAgD,CAC3G,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAU,EAAK,SAAS,CAAC,IAAK,GAAQ,EAAI,KAAK,CAErD,MAAO,CACL,KAAM,KAAK,2BAA2B,EAAK,CAC3C,UACA,aAAc,EACd,gBAAiB,EAClB,CAGH,MAAM,IAAI,EAAa,EAA2C,CAChE,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAElC,MAAO,CACL,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CAGH,MAAM,KAAK,EAA4B,CACrC,KAAK,iBAAiB,CACtB,KAAK,GAAG,KAAK,EAAI,CAGnB,YAAe,EAA4D,CACzE,KAAK,iBAAiB,CAEtB,KAAK,GAAG,KAAK,QAAQ,CACrB,IAAM,EAAqB,IAAI,EAAuB,KAAK,GAAG,CAC9D,OAAQ,SAAY,CAClB,GAAI,CACF,IAAM,EAAS,MAAM,EAAG,EAAmB,CAE3C,OADA,KAAK,GAAG,KAAK,SAAS,CACf,QACA,EAAO,CAEd,MADA,KAAK,GAAG,KAAK,WAAW,CAClB,MAEN,CAWN,MAAM,iBAAoB,EAA4C,EAA8C,CAClH,KAAK,iBAAiB,CAEtB,IAAM,EAAY,GAAS,WAAa,IACpC,EAAuB,GAG3B,GAAI,CACF,KAAK,GAAG,KAAK,kBAAkB,OACxB,EAAG,CACV,GAAK,EAAY,QAAQ,SAAS,kDAAkD,CAClF,EAAuB,QAEvB,MAAM,EAIV,IAAI,EACA,EAAuB,GAE3B,GAAI,CAEF,IAAM,EAAiB,IAAI,SAAgB,EAAG,IAAW,CACvD,EAAgB,eAAiB,CAC1B,GACH,EAAW,MAAM,mCAAmC,EAAU,IAAI,CAAC,EAEpE,EAAU,EACb,CAEI,EAAqB,IAAI,EAAwB,KAAK,GAAG,CAGzD,EAAS,MAAM,QAAQ,KAAK,CAAC,EAAG,EAAmB,CAAE,EAAe,CAAC,CAW3E,MATA,GAAuB,GACnB,GACF,aAAa,EAAc,CAIxB,GACH,KAAK,GAAG,KAAK,SAAS,CAEjB,QACA,EAAO,CACd,EAAuB,GACnB,GACF,aAAa,EAAc,CAG7B,GAAI,CACF,KAAK,GAAG,KAAK,WAAW,OACjB,EAAe,CACtB,QAAQ,MAAM,mBAAoB,EAAc,CAGlD,MAAM,GAIV,mBAAoC,CAClC,MAAO,SAGT,MAAM,OAAuB,CACvB,CAAC,KAAK,SAAW,KAAK,GAAG,OAC3B,KAAK,GAAG,OAAO,CACf,KAAK,QAAU,IAInB,IAAI,QAAkB,CACpB,MAAO,CAAC,KAAK,SAAW,KAAK,GAAG,OAIrB,EAAf,KAAoE,CAGlE,YAAY,EAAiC,CAAvB,KAAA,GAAA,qBAFU,GAIhC,gBAAwB,EAAkC,CAGxD,OAFK,EACD,MAAM,QAAQ,EAAO,CAAS,EAC3B,OAAO,OAAO,EAAO,CAFR,EAAE,CAKxB,2BAAsC,EAAa,CACjD,GAAI,GAAU,KACZ,OAAO,EAGT,GAAI,OAAO,SAAS,EAAM,CACxB,OAAO,IAAI,WAAW,EAAM,CAG9B,GAAI,MAAM,QAAQ,EAAM,CACtB,OAAO,EAAM,IAAK,GAAS,KAAK,2BAA2B,EAAK,CAAC,CAGnE,GAAI,OAAO,GAAU,SAAU,CAC7B,IAAM,EAAkC,EAAE,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAQ,OAAO,QAAQ,EAAM,CAC5C,EAAO,GAAO,KAAK,2BAA2B,EAAI,CAEpD,OAAO,EAGT,OAAO,EAIT,MAAuC,EAAa,EAAqC,CACvF,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAS,KAAK,2BAA2B,EAAK,CACpD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,SAA0C,EAAa,EAAgD,CACrG,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAU,EAAK,SAAS,CAAC,IAAK,GAAQ,EAAI,KAAK,CAE/C,EAAS,CACb,KAAM,KAAK,2BAA2B,EAAK,CAC3C,UACA,aAAc,EACd,gBAAiB,EAClB,CACD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,IAAI,EAAa,EAA2C,CAC1D,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAE5B,EAAS,CACb,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CACD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,KAAK,EAA4B,CAE/B,OADA,KAAK,GAAG,KAAK,EAAI,CACV,QAAQ,SAAS,CAI1B,UAA2C,EAAa,EAA4B,CAClF,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAClC,OAAO,KAAK,2BAA2B,EAAK,CAG9C,QAAQ,EAAa,EAAkC,CACrD,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAElC,MAAO,CACL,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CAGH,SAAS,EAAmB,CAC1B,KAAK,GAAG,KAAK,EAAI,CAUnB,MAAM,OAAuB,IAKzB,EAAN,cAAqC,CAAuB,CAC1D,mBAAoC,CAClC,MAAO,UAGT,MAAM,YAAe,EAA4D,CAG/E,OAAO,MAAM,EAAG,KAAK,CAGvB,MAAM,iBACJ,EACA,EACY,CAEZ,MAAU,MACR,qHACD,GAIC,EAAN,cAAsC,CAAuB,CAC3D,mBAAoC,CAClC,MAAO,WAGT,MAAM,YAAe,EAA4D,CAG/E,OAAO,MAAM,EAAG,KAAK,CAGvB,MAAM,iBAAoB,EAA4C,EAA+C,CAGnH,OAAO,MAAM,EAAG,KAAK"} |
| /** | ||
| * Browser Adapter (Multi-Tab Shared SQLite) | ||
| * | ||
| * Implements UniStoreConnection for browser environments using: | ||
| * - Web Locks for Host election | ||
| * - BroadcastChannel for RPC communication | ||
| * - Visibility-aware Host management | ||
| * - Transaction Session Protocol for interactive transactions | ||
| */ | ||
| import { BaseAdapter } from "../base.js"; | ||
| import type { | ||
| UniStoreConnection, | ||
| UniStoreOptions, | ||
| SQLiteWasmConfig, | ||
| SQLiteParams, | ||
| SQLiteValue, | ||
| QueryResult, | ||
| RunResult, | ||
| ConnectionType, | ||
| } from "../../types.js"; | ||
| import { HostElection } from "./host-election.js"; | ||
| import { VisibilityManager } from "./visibility-manager.js"; | ||
| import { RpcChannel, type RequestHandler } from "./rpc-channel.js"; | ||
| import { DbWorkerHost } from "./db-worker-host.js"; | ||
| import { TransactionSessionManager } from "./transaction-session.js"; | ||
| import { RemoteTransactionAdapter } from "./remote-transaction-adapter.js"; | ||
| import { | ||
| generateTabId, | ||
| generateTxnId, | ||
| type TabId, | ||
| type RequestMessage, | ||
| type RpcRequest, | ||
| type TxnBeginRequest, | ||
| type TxnExecRequest, | ||
| type TxnCommitRequest, | ||
| type TxnRollbackRequest, | ||
| type TxnHeartbeat, | ||
| } from "./message-types.js"; | ||
| import { adapterLogger as logger } from "./logger.js"; | ||
| // ============================================================================= | ||
| // Types | ||
| // ============================================================================= | ||
| interface ExtendedUniStoreOptions extends UniStoreOptions { | ||
| sqlite?: SQLiteWasmConfig; | ||
| } | ||
| // ============================================================================= | ||
| // Local Transaction Adapter | ||
| // ============================================================================= | ||
| /** | ||
| * Transaction adapter for local (Host) transactions | ||
| */ | ||
| class LocalTransactionAdapter implements UniStoreConnection { | ||
| constructor( | ||
| private readonly dbHost: DbWorkerHost, | ||
| private readonly isAsync: boolean | ||
| ) {} | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| const result = await this.dbHost.executeSqlRaw<T>(sql, params); | ||
| return result.rows; | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| return await this.dbHost.executeSqlRaw<T>(sql, params); | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| const result = await this.dbHost.executeSqlRaw(sql, params); | ||
| return { | ||
| rowsAffected: result.rowsAffected ?? 0, | ||
| lastInsertRowId: result.lastInsertRowId ?? 0, | ||
| }; | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| await this.dbHost.execRaw(sql); | ||
| } | ||
| async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> { | ||
| // Nested transaction: just execute in the same context | ||
| const result = fn(this); | ||
| return result instanceof Promise ? await result : result; | ||
| } | ||
| async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>): Promise<T> { | ||
| return await fn(this); | ||
| } | ||
| getConnectionType(): ConnectionType { | ||
| return this.isAsync ? "asyncTxn" : "syncTxn"; | ||
| } | ||
| async close(): Promise<void> { | ||
| // No-op for transaction adapter | ||
| } | ||
| } | ||
| // ============================================================================= | ||
| // Browser Adapter | ||
| // ============================================================================= | ||
| export class BrowserAdapter extends BaseAdapter { | ||
| private readonly tabId: TabId; | ||
| private hostElection: HostElection; | ||
| private visibilityManager: VisibilityManager; | ||
| private rpcChannel: RpcChannel; | ||
| private dbHost: DbWorkerHost | null = null; | ||
| private txnSessionManager: TransactionSessionManager | null = null; | ||
| private sqliteConfig: SQLiteWasmConfig; | ||
| private initialized = false; | ||
| private initPromise?: Promise<void>; | ||
| private closed = false; | ||
| // Promise that resolves when becomeHost() completes | ||
| private becomeHostPromise?: Promise<void>; | ||
| private becomeHostResolver?: () => void; | ||
| // Visibility unsubscribe function | ||
| private visibilityUnsubscribe?: () => void; | ||
| constructor(options: ExtendedUniStoreOptions) { | ||
| const path = options.path ?? (options as { name?: string }).name ?? "default"; | ||
| super({ ...options, path }); | ||
| this.options = { ...options, path }; | ||
| this.tabId = generateTabId(); | ||
| this.sqliteConfig = { | ||
| loadStrategy: "global", | ||
| storageBackend: "auto", | ||
| opfsVfsType: "auto", | ||
| cdnBaseUrl: "https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm", | ||
| version: "3.50.1-build1", | ||
| bundlerFriendly: false, | ||
| ...options.sqlite, | ||
| }; | ||
| // Check Web Locks support - if not available, fall back to memory-only | ||
| if (!HostElection.isSupported()) { | ||
| logger.warn("Web Locks API not supported. Falling back to memory-only storage."); | ||
| this.sqliteConfig.storageBackend = "memory"; | ||
| } | ||
| // Initialize components | ||
| this.visibilityManager = new VisibilityManager(); | ||
| this.rpcChannel = new RpcChannel(path, this.tabId); | ||
| this.hostElection = new HostElection({ | ||
| dbName: path, | ||
| tabId: this.tabId, | ||
| onBecomeHost: () => this.becomeHost(), | ||
| onLoseHost: () => this.loseHost(), | ||
| checkVisibility: () => this.visibilityManager.isVisible(), | ||
| }); | ||
| // Setup visibility-based Host management | ||
| this.visibilityUnsubscribe = this.visibilityManager.onVisibilityChange((visible) => { | ||
| this.handleVisibilityChange(visible); | ||
| }); | ||
| } | ||
| /** | ||
| * Create and initialize a BrowserAdapter | ||
| */ | ||
| static async create(options: ExtendedUniStoreOptions): Promise<BrowserAdapter> { | ||
| const adapter = new BrowserAdapter(options); | ||
| await adapter.initialize(); | ||
| return adapter; | ||
| } | ||
| /** | ||
| * Get the tab ID | ||
| */ | ||
| getTabId(): TabId { | ||
| return this.tabId; | ||
| } | ||
| /** | ||
| * Check if this tab is the Host | ||
| */ | ||
| get isHost(): boolean { | ||
| return this.hostElection.isHost; | ||
| } | ||
| // =========================================================================== | ||
| // Initialization | ||
| // =========================================================================== | ||
| private async initialize(): Promise<void> { | ||
| if (this.initialized) return; | ||
| if (this.initPromise) return this.initPromise; | ||
| this.initPromise = this.doInitialize(); | ||
| await this.initPromise; | ||
| this.initialized = true; | ||
| } | ||
| private async doInitialize(): Promise<void> { | ||
| // Create a promise that will be resolved when becomeHost() completes | ||
| this.becomeHostPromise = new Promise<void>((resolve) => { | ||
| this.becomeHostResolver = resolve; | ||
| }); | ||
| // Start Host election | ||
| await this.hostElection.start(); | ||
| // Wait for either: | ||
| // 1. We become Host and becomeHost() completes (dbHost is ready) | ||
| // 2. A timeout indicating we're likely a Follower | ||
| const followerTimeoutMs = 500; // If we don't become Host in 500ms, we're a Follower | ||
| const timeoutPromise = new Promise<"timeout">((resolve) => { | ||
| setTimeout(() => resolve("timeout"), followerTimeoutMs); | ||
| }); | ||
| const result = await Promise.race([ | ||
| this.becomeHostPromise.then(() => "host" as const), | ||
| timeoutPromise, | ||
| ]); | ||
| if (result === "host") { | ||
| // We're Host and database is ready | ||
| } else { | ||
| // Timeout - we're either a Follower, or becomeHost is taking longer | ||
| if (this.hostElection.isHost) { | ||
| // We are Host but becomeHost() is still running - wait for it | ||
| await this.becomeHostPromise; | ||
| } | ||
| // else: we're a Follower, which is fine - we can still operate via RPC | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Host Lifecycle | ||
| // =========================================================================== | ||
| private async becomeHost(): Promise<void> { | ||
| logger.log(`Tab ${this.tabId} becoming Host for ${this.options.path}`); | ||
| // Create the database host | ||
| this.dbHost = await DbWorkerHost.create({ | ||
| dbName: this.options.path, | ||
| config: this.sqliteConfig, | ||
| }); | ||
| // Create transaction session manager | ||
| this.txnSessionManager = new TransactionSessionManager(this.dbHost, this.hostElection); | ||
| this.txnSessionManager.startWatchdog(); | ||
| // Setup RPC handler | ||
| this.rpcChannel.setRequestHandler(this.handleRequest.bind(this)); | ||
| // Signal that we're ready as Host | ||
| if (this.becomeHostResolver) { | ||
| this.becomeHostResolver(); | ||
| this.becomeHostResolver = undefined; | ||
| } | ||
| } | ||
| private loseHost(): void { | ||
| logger.log(`Tab ${this.tabId} losing Host for ${this.options.path}`); | ||
| // Clear RPC handler | ||
| this.rpcChannel.setRequestHandler(undefined); | ||
| // Cleanup transaction session manager | ||
| if (this.txnSessionManager) { | ||
| void this.txnSessionManager.close(); | ||
| this.txnSessionManager = null; | ||
| } | ||
| // Cleanup database host | ||
| if (this.dbHost) { | ||
| void this.dbHost.close(); | ||
| this.dbHost = null; | ||
| } | ||
| } | ||
| private handleVisibilityChange(visible: boolean): void { | ||
| // Note: We don't automatically release Host when tab becomes hidden. | ||
| // This is intentional because: | ||
| // 1. It causes issues in multi-tab testing environments (Playwright) | ||
| // 2. The Host should remain stable to serve RPC requests from Followers | ||
| // 3. If the Host tab closes, the Web Lock is automatically released | ||
| // | ||
| // If visibility-based Host release is needed in the future, it can be | ||
| // added as an optional configuration. | ||
| if (visible && !this.hostElection.isHost && !this.closed) { | ||
| // Tab became visible and we're not Host - try to become Host | ||
| void this.hostElection.start(); | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Request Handler (Host only) | ||
| // =========================================================================== | ||
| private async handleRequest(req: RequestMessage): Promise<unknown> { | ||
| if (!this.dbHost) { | ||
| throw new Error("Not Host"); | ||
| } | ||
| switch (req.t) { | ||
| case "req": | ||
| return await this.handleRpcRequest(req); | ||
| case "txn_begin": | ||
| return await this.handleTxnBegin(req); | ||
| case "txn_exec": | ||
| return await this.handleTxnExec(req); | ||
| case "txn_commit": | ||
| return await this.handleTxnCommit(req); | ||
| case "txn_rollback": | ||
| return await this.handleTxnRollback(req); | ||
| case "txn_heartbeat": | ||
| this.handleTxnHeartbeat(req); | ||
| return undefined; | ||
| default: | ||
| throw new Error(`Unknown request type: ${(req as { t: string }).t}`); | ||
| } | ||
| } | ||
| private async handleRpcRequest(req: RpcRequest): Promise<unknown> { | ||
| const { op, payload } = req; | ||
| const { sql, params } = payload; | ||
| // Wait for any active transaction to complete before executing | ||
| // This prevents RPC requests from mixing into active transactions | ||
| if (this.txnSessionManager) { | ||
| await this.txnSessionManager.waitForTransactionSlot(); | ||
| } | ||
| switch (op) { | ||
| case "query": | ||
| return { rows: await this.dbHost!.query(sql, params), columns: [] }; | ||
| case "queryRaw": | ||
| return await this.dbHost!.queryRaw(sql, params); | ||
| case "run": | ||
| return await this.dbHost!.run(sql, params); | ||
| case "exec": | ||
| await this.dbHost!.exec(sql); | ||
| return null; | ||
| default: | ||
| throw new Error(`Unknown RPC operation: ${String(op)}`); | ||
| } | ||
| } | ||
| private async handleTxnBegin(req: TxnBeginRequest): Promise<void> { | ||
| await this.txnSessionManager!.beginTransaction(req.txnId, req.from, req.mode); | ||
| } | ||
| private async handleTxnExec(req: TxnExecRequest): Promise<unknown> { | ||
| const { txnId, op, payload } = req; | ||
| const { sql, params } = payload; | ||
| return await this.txnSessionManager!.execInTransaction(txnId, async () => { | ||
| switch (op) { | ||
| case "query": | ||
| return { rows: await this.dbHost!.query(sql, params), columns: [] }; | ||
| case "queryRaw": | ||
| return await this.dbHost!.queryRaw(sql, params); | ||
| case "run": | ||
| return await this.dbHost!.run(sql, params); | ||
| case "exec": | ||
| await this.dbHost!.exec(sql); | ||
| return null; | ||
| default: | ||
| throw new Error(`Unknown transaction operation: ${String(op)}`); | ||
| } | ||
| }); | ||
| } | ||
| private async handleTxnCommit(req: TxnCommitRequest): Promise<void> { | ||
| await this.txnSessionManager!.commitTransaction(req.txnId); | ||
| } | ||
| private async handleTxnRollback(req: TxnRollbackRequest): Promise<void> { | ||
| await this.txnSessionManager!.rollbackTransaction(req.txnId); | ||
| } | ||
| private handleTxnHeartbeat(req: TxnHeartbeat): void { | ||
| this.txnSessionManager?.recordHeartbeat(req.txnId); | ||
| } | ||
| // =========================================================================== | ||
| // UniStoreConnection Implementation | ||
| // =========================================================================== | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| return await this.dbHost.query<T>(sql, params); | ||
| } | ||
| const result = await this.rpcChannel.request<QueryResult<T>>("query", { sql, params }); | ||
| return result.rows; | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| return await this.dbHost.queryRaw<T>(sql, params); | ||
| } | ||
| return await this.rpcChannel.request<QueryResult<T>>("queryRaw", { sql, params }); | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| return await this.dbHost.run(sql, params); | ||
| } | ||
| return await this.rpcChannel.request<RunResult>("run", { sql, params }); | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| await this.dbHost.exec(sql); | ||
| return; | ||
| } | ||
| await this.rpcChannel.request<void>("exec", { sql }); | ||
| } | ||
| async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| return await this.executeLocalTransaction(fn, false); | ||
| } | ||
| return await this.executeRemoteTransaction(fn); | ||
| } | ||
| async asyncTransaction<T>( | ||
| fn: (tx: UniStoreConnection) => Promise<T>, | ||
| options?: { timeoutMs?: number } | ||
| ): Promise<T> { | ||
| await this.initialize(); | ||
| if (this.isHost && this.dbHost) { | ||
| return await this.executeLocalAsyncTransaction(fn, options); | ||
| } | ||
| return await this.executeRemoteTransaction(fn, options); | ||
| } | ||
| getConnectionType(): ConnectionType { | ||
| return "direct"; | ||
| } | ||
| async close(): Promise<void> { | ||
| if (this.closed) return; | ||
| this.closed = true; | ||
| // Unsubscribe from visibility changes | ||
| this.visibilityUnsubscribe?.(); | ||
| // Close Host election | ||
| await this.hostElection.close(); | ||
| // Close RPC channel | ||
| this.rpcChannel.close(); | ||
| // Cleanup visibility manager | ||
| this.visibilityManager.destroy(); | ||
| // Cleanup transaction session manager | ||
| if (this.txnSessionManager) { | ||
| await this.txnSessionManager.close(); | ||
| this.txnSessionManager = null; | ||
| } | ||
| // Cleanup database host | ||
| if (this.dbHost) { | ||
| await this.dbHost.close(); | ||
| this.dbHost = null; | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Transaction Implementation | ||
| // =========================================================================== | ||
| private async executeLocalTransaction<T>( | ||
| fn: (tx: UniStoreConnection) => Promise<T> | T, | ||
| isAsync: boolean | ||
| ): Promise<T> { | ||
| await this.dbHost!.execRaw("BEGIN"); | ||
| const txAdapter = new LocalTransactionAdapter(this.dbHost!, isAsync); | ||
| try { | ||
| const result = fn(txAdapter); | ||
| const value = result instanceof Promise ? await result : result; | ||
| await this.dbHost!.execRaw("COMMIT"); | ||
| return value; | ||
| } catch (e) { | ||
| try { | ||
| await this.dbHost!.execRaw("ROLLBACK"); | ||
| } catch (rollbackError) { | ||
| console.error("Rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| private async executeLocalAsyncTransaction<T>( | ||
| fn: (tx: UniStoreConnection) => Promise<T>, | ||
| options?: { timeoutMs?: number } | ||
| ): Promise<T> { | ||
| const timeoutMs = options?.timeoutMs ?? 30000; | ||
| await this.dbHost!.execRaw("BEGIN IMMEDIATE"); | ||
| const txAdapter = new LocalTransactionAdapter(this.dbHost!, true); | ||
| let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | ||
| let transactionCompleted = false; | ||
| try { | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => { | ||
| if (!transactionCompleted) { | ||
| reject(new Error(`Async transaction timeout after ${timeoutMs}ms`)); | ||
| } | ||
| }, timeoutMs); | ||
| }); | ||
| const result = await Promise.race([fn(txAdapter), timeoutPromise]); | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| await this.dbHost!.execRaw("COMMIT"); | ||
| return result; | ||
| } catch (e) { | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| try { | ||
| await this.dbHost!.execRaw("ROLLBACK"); | ||
| } catch (rollbackError) { | ||
| console.error("Rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| private async executeRemoteTransaction<T>( | ||
| fn: (tx: UniStoreConnection) => Promise<T> | T, | ||
| options?: { timeoutMs?: number } | ||
| ): Promise<T> { | ||
| const txnId = generateTxnId(); | ||
| const timeoutMs = options?.timeoutMs ?? 30000; | ||
| // Begin transaction | ||
| await this.rpcChannel.txnBegin(txnId, "immediate", timeoutMs); | ||
| // Create remote transaction adapter | ||
| const txAdapter = new RemoteTransactionAdapter(this.rpcChannel, txnId); | ||
| let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | ||
| let transactionCompleted = false; | ||
| try { | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => { | ||
| if (!transactionCompleted) { | ||
| reject(new Error(`Remote transaction timeout after ${timeoutMs}ms`)); | ||
| } | ||
| }, timeoutMs); | ||
| }); | ||
| const fnResult = fn(txAdapter); | ||
| const resultPromise = fnResult instanceof Promise ? fnResult : Promise.resolve(fnResult); | ||
| const result = await Promise.race([resultPromise, timeoutPromise]); | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| // Commit transaction | ||
| await txAdapter._commit(); | ||
| return result; | ||
| } catch (e) { | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| // Rollback transaction | ||
| try { | ||
| await txAdapter._rollback(); | ||
| } catch (rollbackError) { | ||
| console.error("Remote rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Info Methods | ||
| // =========================================================================== | ||
| /** | ||
| * Get information about the SQLite configuration and state | ||
| */ | ||
| getSQLiteInfo() { | ||
| // isReady means we can execute SQL: | ||
| // - If we're Host, we need dbHost to be ready | ||
| // - If we're Follower, we're ready as long as RPC channel is open | ||
| const isReady = this.isHost ? this.dbHost !== null : this.initialized && !this.closed; | ||
| return { | ||
| config: this.sqliteConfig, | ||
| isInitialized: this.initialized, | ||
| isHost: this.isHost, | ||
| isReady, | ||
| tabId: this.tabId, | ||
| hasDatabase: this.dbHost !== null, | ||
| usesWorker: this.dbHost?.isWorkerDbActive() ?? false, | ||
| activeStorageBackend: this.dbHost?.getActiveStorageBackend() ?? "unknown", | ||
| activeOpfsVfs: this.dbHost?.getActiveOpfsVfs(), | ||
| }; | ||
| } | ||
| /** | ||
| * Get the active storage backend | ||
| */ | ||
| getActiveStorageBackend(): "opfs" | "localStorage" | "memory" | "unknown" { | ||
| return this.dbHost?.getActiveStorageBackend() ?? "unknown"; | ||
| } | ||
| } |
| /** | ||
| * SQLite Worker Host | ||
| * | ||
| * Manages the SQLite database connection through a Worker. | ||
| * Only the Host tab creates and uses this - Followers send RPC requests. | ||
| * | ||
| * Reuses SQLite WASM loading and Worker management from the original browser.ts. | ||
| */ | ||
| import type { | ||
| SQLiteWasmConfig, | ||
| SQLiteParams, | ||
| SQLiteValue, | ||
| QueryResult, | ||
| RunResult, | ||
| } from "../../types.js"; | ||
| // ============================================================================= | ||
| // Type definitions for SQLite WASM | ||
| // ============================================================================= | ||
| interface SQLiteStatement { | ||
| bind: (params: unknown) => unknown; | ||
| step(): boolean; | ||
| get(index: number): SQLiteValue; | ||
| getColumnName(index: number): string; | ||
| columnCount: number; | ||
| finalize(): void; | ||
| } | ||
| interface SQLiteDatabase { | ||
| prepare(sql: string): SQLiteStatement; | ||
| exec(sql: string): void; | ||
| changes(): number; | ||
| close(): void; | ||
| pointer: unknown; | ||
| } | ||
| interface SQLiteAPI { | ||
| oo1: { | ||
| DB: new (filename?: string, flags?: string, vfs?: string) => SQLiteDatabase; | ||
| JsStorageDb?: new (filename: string) => SQLiteDatabase; | ||
| OpfsDb?: new (filename: string) => SQLiteDatabase; | ||
| }; | ||
| capi: { | ||
| sqlite3_last_insert_rowid(db: unknown): unknown; | ||
| sqlite3_vfs_find?: (name: string) => unknown; | ||
| }; | ||
| installOpfsSAHPoolVfs?: (options?: { name?: string; directory?: string }) => Promise<unknown>; | ||
| } | ||
| interface SQLiteInitOptions { | ||
| print?: (message: string) => void; | ||
| printErr?: (message: string) => void; | ||
| locateFile?: (filename: string) => string; | ||
| } | ||
| type SQLiteInitModule = (options?: SQLiteInitOptions) => Promise<SQLiteAPI>; | ||
| type SQLiteWorkerPromiser = (type: string, args?: unknown) => Promise<unknown>; | ||
| type SQLiteWorker1PromiserFactory = (config: Record<string, unknown>) => SQLiteWorkerPromiser; | ||
| interface WindowWithSQLite extends Window { | ||
| sqlite3InitModule?: SQLiteInitModule; | ||
| sqlite3InitModuleState?: { | ||
| locateFile: (filename: string) => string; | ||
| }; | ||
| } | ||
| // ============================================================================= | ||
| // Module-level caches (shared across instances) | ||
| // ============================================================================= | ||
| const sqliteModuleCache = new Map<string, Promise<SQLiteInitModule>>(); | ||
| const sqliteWorkerPromiserCache = new Map<string, Promise<SQLiteWorker1PromiserFactory>>(); | ||
| // ============================================================================= | ||
| // DbWorkerHost Class | ||
| // ============================================================================= | ||
| export interface DbWorkerHostOptions { | ||
| dbName: string; | ||
| config: SQLiteWasmConfig; | ||
| } | ||
| export class DbWorkerHost { | ||
| private sqlite3: SQLiteAPI | undefined; | ||
| private db: SQLiteDatabase | undefined; | ||
| private workerPromiser: SQLiteWorkerPromiser | undefined; | ||
| private workerPromiserInitPromise: Promise<SQLiteWorkerPromiser> | undefined; | ||
| private workerDbId: string | number | undefined; | ||
| private workerQueue: Promise<void> = Promise.resolve(); | ||
| private activeOpfsVfs: "opfs" | "opfs-sahpool" | undefined; | ||
| private activeStorageBackend: "opfs" | "localStorage" | "memory" = "memory"; | ||
| private closed = false; | ||
| constructor( | ||
| private readonly dbName: string, | ||
| private readonly config: SQLiteWasmConfig | ||
| ) {} | ||
| /** | ||
| * Create and initialize a DbWorkerHost | ||
| */ | ||
| static async create(options: DbWorkerHostOptions): Promise<DbWorkerHost> { | ||
| const host = new DbWorkerHost(options.dbName, options.config); | ||
| await host.initialize(); | ||
| return host; | ||
| } | ||
| /** | ||
| * Initialize the database | ||
| */ | ||
| async initialize(): Promise<void> { | ||
| console.log(`Loading SQLite WASM using strategy: ${this.config.loadStrategy}`); | ||
| const sqlite3InitModule = await this.loadSQLiteWasm(); | ||
| const initOptions: SQLiteInitOptions = { | ||
| print: console.log, | ||
| printErr: console.error, | ||
| }; | ||
| if (this.config.locateFile) { | ||
| initOptions.locateFile = this.config.locateFile; | ||
| } else if (typeof window !== "undefined") { | ||
| const windowWithSQLite = window as WindowWithSQLite; | ||
| if (windowWithSQLite.sqlite3InitModuleState?.locateFile) { | ||
| initOptions.locateFile = windowWithSQLite.sqlite3InitModuleState.locateFile; | ||
| } | ||
| } | ||
| console.log("Initializing SQLite WASM module..."); | ||
| this.sqlite3 = await sqlite3InitModule(initOptions); | ||
| if (!this.sqlite3) { | ||
| throw new Error("Failed to initialize SQLite WASM module"); | ||
| } | ||
| const sqlite3 = this.sqlite3; | ||
| console.log("SQLite WASM module initialized successfully"); | ||
| const storageBackend = this.config.storageBackend ?? "auto"; | ||
| const dbFileName = this.dbName === ":memory:" ? ":memory:" : `/unisqlite/${this.dbName}`; | ||
| if (storageBackend === "memory" || this.dbName === ":memory:") { | ||
| console.log("Using in-memory database"); | ||
| this.db = new sqlite3.oo1.DB(":memory:"); | ||
| this.activeStorageBackend = "memory"; | ||
| } else { | ||
| const dbCreated = await this.tryCreatePersistentDatabase(sqlite3, dbFileName, storageBackend); | ||
| if (!dbCreated) { | ||
| console.log("All persistent storage backends failed, using in-memory database"); | ||
| this.db = new sqlite3.oo1.DB(":memory:"); | ||
| this.activeStorageBackend = "memory"; | ||
| } | ||
| } | ||
| // Enable WAL mode for better performance | ||
| try { | ||
| await this.execInternal("PRAGMA journal_mode=WAL"); | ||
| console.log("Enabled WAL mode"); | ||
| } catch (e) { | ||
| console.warn("Could not enable WAL mode:", e); | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Public API | ||
| // =========================================================================== | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| const result = await this.executeSql<T>(sql, params); | ||
| return result.rows; | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| return await this.executeSql<T>(sql, params); | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| const result = await this.executeSql(sql, params); | ||
| return { | ||
| rowsAffected: result.rowsAffected ?? 0, | ||
| lastInsertRowId: result.lastInsertRowId ?? 0, | ||
| }; | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| await this.execInternal(sql); | ||
| } | ||
| /** | ||
| * Execute raw SQL bypassing queue (for transaction control) | ||
| */ | ||
| async execRaw(sql: string): Promise<void> { | ||
| await this.execInternal(sql, { bypassQueue: true }); | ||
| } | ||
| /** | ||
| * Execute SQL with result bypassing queue (for transaction statements) | ||
| */ | ||
| async executeSqlRaw<T>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| return await this.executeSql<T>(sql, params, { bypassQueue: true }); | ||
| } | ||
| getActiveStorageBackend(): "opfs" | "localStorage" | "memory" { | ||
| return this.activeStorageBackend; | ||
| } | ||
| getActiveOpfsVfs(): "opfs" | "opfs-sahpool" | undefined { | ||
| return this.activeOpfsVfs; | ||
| } | ||
| isWorkerDbActive(): boolean { | ||
| return !!this.workerPromiser && this.workerDbId !== undefined; | ||
| } | ||
| async close(): Promise<void> { | ||
| if (this.closed) return; | ||
| this.closed = true; | ||
| if (this.workerPromiser) { | ||
| const promiser = this.workerPromiser; | ||
| const dbId = this.workerDbId; | ||
| try { | ||
| if (dbId !== undefined) { | ||
| await this.enqueueWorker(async () => { | ||
| await promiser("close", { dbId }); | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| console.warn("Failed to close SQLite worker database:", e); | ||
| } finally { | ||
| this.workerDbId = undefined; | ||
| const worker = (promiser as unknown as { worker?: { terminate?: () => void } }).worker; | ||
| if (worker?.terminate) { | ||
| worker.terminate(); | ||
| } | ||
| this.workerPromiser = undefined; | ||
| } | ||
| } | ||
| if (this.db) { | ||
| this.db.close(); | ||
| this.db = undefined; | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // Internal implementation | ||
| // =========================================================================== | ||
| private enqueueWorker<T>(fn: () => Promise<T>): Promise<T> { | ||
| const run = async () => fn(); | ||
| const next = this.workerQueue.then(run, run); | ||
| this.workerQueue = next.then( | ||
| () => undefined, | ||
| () => undefined | ||
| ); | ||
| return next; | ||
| } | ||
| private normalizeParams(params?: SQLiteParams): SQLiteValue[] | undefined { | ||
| if (!params) return undefined; | ||
| if (Array.isArray(params)) { | ||
| return params; | ||
| } | ||
| const keys = Object.keys(params); | ||
| const paramRecord: Record<string, SQLiteValue> = params; | ||
| return keys.map((key) => paramRecord[key]); | ||
| } | ||
| private async execInternal(sql: string, options?: { bypassQueue?: boolean }): Promise<void> { | ||
| if (this.isWorkerDbActive()) { | ||
| const run = async () => { | ||
| if (!this.workerPromiser || this.workerDbId === undefined) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| await this.workerPromiser("exec", { dbId: this.workerDbId, sql }); | ||
| }; | ||
| if (options?.bypassQueue) { | ||
| await run(); | ||
| } else { | ||
| await this.enqueueWorker(run); | ||
| } | ||
| return; | ||
| } | ||
| if (!this.db) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| this.db.exec(sql); | ||
| } | ||
| private async executeSql<T>( | ||
| sql: string, | ||
| params?: SQLiteParams, | ||
| options?: { bypassQueue?: boolean } | ||
| ): Promise<QueryResult<T>> { | ||
| try { | ||
| if (this.isWorkerDbActive()) { | ||
| const run = async (): Promise<QueryResult<T>> => { | ||
| if (!this.workerPromiser || this.workerDbId === undefined) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| const normalizedParams = this.normalizeParams(params); | ||
| const execArgs: Record<string, unknown> = { | ||
| dbId: this.workerDbId, | ||
| sql, | ||
| rowMode: "object", | ||
| resultRows: [], | ||
| columnNames: [], | ||
| countChanges: true, | ||
| }; | ||
| if (normalizedParams) { | ||
| execArgs.bind = normalizedParams; | ||
| } | ||
| const execMsg = await this.workerPromiser("exec", execArgs); | ||
| const execOut = (execMsg as { result?: unknown }).result ?? execMsg; | ||
| const execOutRecord = execOut as Record<string, unknown>; | ||
| const rows = Array.isArray(execOutRecord.resultRows) ? (execOutRecord.resultRows as T[]) : []; | ||
| const columns = Array.isArray(execOutRecord.columnNames) ? (execOutRecord.columnNames as string[]) : []; | ||
| const rowsAffected = typeof execOutRecord.changeCount === "number" ? execOutRecord.changeCount : 0; | ||
| // Fetch last_insert_rowid via follow-up query | ||
| const metaArgs: Record<string, unknown> = { | ||
| dbId: this.workerDbId, | ||
| sql: "SELECT last_insert_rowid() AS lastInsertRowId", | ||
| rowMode: "object", | ||
| resultRows: [], | ||
| columnNames: [], | ||
| }; | ||
| const metaMsg = await this.workerPromiser("exec", metaArgs); | ||
| const metaOut = (metaMsg as { result?: unknown }).result ?? metaMsg; | ||
| const metaOutRecord = metaOut as Record<string, unknown>; | ||
| const metaRows = Array.isArray(metaOutRecord.resultRows) | ||
| ? (metaOutRecord.resultRows as Array<Record<string, unknown>>) | ||
| : []; | ||
| const lastInsertRowId = Number(metaRows[0]?.lastInsertRowId ?? 0); | ||
| return { | ||
| rows, | ||
| columns, | ||
| rowsAffected, | ||
| lastInsertRowId, | ||
| }; | ||
| }; | ||
| if (options?.bypassQueue) { | ||
| return await run(); | ||
| } | ||
| return await this.enqueueWorker(run); | ||
| } | ||
| if (!this.db || !this.sqlite3) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| const normalizedParams = this.normalizeParams(params); | ||
| const rows: T[] = []; | ||
| let columns: string[] = []; | ||
| const stmt = this.db.prepare(sql); | ||
| try { | ||
| const stmtWithMeta = stmt as unknown as { | ||
| getColumnCount?: () => number; | ||
| columnCount?: number; | ||
| getColumnNames?: () => string[]; | ||
| }; | ||
| if (normalizedParams) { | ||
| stmt.bind(normalizedParams); | ||
| } | ||
| const columnCount = | ||
| typeof stmtWithMeta.getColumnCount === "function" | ||
| ? stmtWithMeta.getColumnCount() | ||
| : typeof stmtWithMeta.columnCount === "number" | ||
| ? stmtWithMeta.columnCount | ||
| : 0; | ||
| if (columnCount > 0) { | ||
| if (typeof stmtWithMeta.getColumnNames === "function") { | ||
| columns = stmtWithMeta.getColumnNames(); | ||
| } else { | ||
| for (let i = 0; i < columnCount; i++) { | ||
| columns.push((stmt as { getColumnName: (index: number) => string }).getColumnName(i)); | ||
| } | ||
| } | ||
| } | ||
| while (stmt.step()) { | ||
| const row: Record<string, SQLiteValue> = {}; | ||
| for (let i = 0; i < columnCount; i++) { | ||
| row[columns[i]] = (stmt as { get: (index: number) => SQLiteValue }).get(i); | ||
| } | ||
| rows.push(row as T); | ||
| } | ||
| return { | ||
| rows, | ||
| columns, | ||
| rowsAffected: this.db.changes(), | ||
| lastInsertRowId: Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer) || 0), | ||
| }; | ||
| } finally { | ||
| stmt.finalize(); | ||
| } | ||
| } catch (error: unknown) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| const sqlError = new Error(`SQLite error: ${errorMessage}`); | ||
| if (error instanceof Error) { | ||
| sqlError.cause = error; | ||
| } | ||
| throw sqlError; | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // SQLite WASM Loading (copied from browser.ts) | ||
| // =========================================================================== | ||
| private async loadSQLiteWasm(): Promise<SQLiteInitModule> { | ||
| const cacheKey = `${this.config.loadStrategy}-${this.config.wasmUrl || this.config.cdnBaseUrl}-${this.config.version}`; | ||
| if (sqliteModuleCache.has(cacheKey)) { | ||
| return sqliteModuleCache.get(cacheKey)!; | ||
| } | ||
| const loadPromise = this.loadSQLiteWasmInternal(); | ||
| sqliteModuleCache.set(cacheKey, loadPromise); | ||
| return loadPromise; | ||
| } | ||
| private async loadSQLiteWasmInternal(): Promise<SQLiteInitModule> { | ||
| const { loadStrategy, wasmUrl, cdnBaseUrl, version, bundlerFriendly } = this.config; | ||
| switch (loadStrategy) { | ||
| case "npm": | ||
| return await this.loadFromNpm(); | ||
| case "cdn": | ||
| return await this.loadFromCdn(cdnBaseUrl!, version!, bundlerFriendly); | ||
| case "url": | ||
| if (!wasmUrl) { | ||
| throw new Error("wasmUrl must be provided when using 'url' loading strategy"); | ||
| } | ||
| return await this.loadFromUrl(wasmUrl); | ||
| case "module": | ||
| return await this.loadAsModule(); | ||
| case "global": | ||
| default: | ||
| return this.loadFromGlobal(); | ||
| } | ||
| } | ||
| private async loadFromNpm(): Promise<SQLiteInitModule> { | ||
| try { | ||
| const module = await import("@sqlite.org/sqlite-wasm"); | ||
| return (module.default || (module as unknown as SQLiteInitModule)) as SQLiteInitModule; | ||
| } catch (error) { | ||
| console.warn("Failed to load SQLite WASM from npm, falling back to CDN:", error); | ||
| return this.loadFromCdn(this.config.cdnBaseUrl!, this.config.version!, this.config.bundlerFriendly); | ||
| } | ||
| } | ||
| private async loadFromCdn(baseUrl: string, version: string, bundlerFriendly?: boolean): Promise<SQLiteInitModule> { | ||
| const filename = bundlerFriendly ? "sqlite3-bundler-friendly.mjs" : "index.mjs"; | ||
| const urlsToTry = [ | ||
| `https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${version}/${filename}`, | ||
| `https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${version}/dist/${filename}`, | ||
| ]; | ||
| let lastError: unknown; | ||
| for (const url of urlsToTry) { | ||
| try { | ||
| console.log(`Trying to load SQLite WASM from: ${url}`); | ||
| const module = await import(url); | ||
| console.log(`Successfully loaded SQLite WASM from: ${url}`); | ||
| return (module.default || module) as SQLiteInitModule; | ||
| } catch (error) { | ||
| lastError = error; | ||
| console.warn(`Failed to load SQLite WASM from ${url}:`, error); | ||
| } | ||
| } | ||
| const errorMessage = lastError instanceof Error ? lastError.message : String(lastError); | ||
| throw new Error(`Failed to load SQLite WASM from any CDN source. Last error: ${errorMessage}`); | ||
| } | ||
| private async loadFromUrl(url: string): Promise<SQLiteInitModule> { | ||
| const module = await import(url); | ||
| return module.default || module.sqlite3InitModule; | ||
| } | ||
| private async loadAsModule(): Promise<SQLiteInitModule> { | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts !== "undefined") { | ||
| throw new Error("ES6 module loading not supported in Web Worker context"); | ||
| } | ||
| const possiblePaths = ["./sqlite3.mjs", "./sqlite3-bundler-friendly.mjs", "/sqlite3.mjs"]; | ||
| for (const path of possiblePaths) { | ||
| try { | ||
| const module = await import(path); | ||
| return module.default || module.sqlite3InitModule; | ||
| } catch (error) { | ||
| console.warn(`Failed to load SQLite WASM from ${path}:`, error); | ||
| } | ||
| } | ||
| throw new Error("Could not load SQLite WASM as ES6 module from any known path"); | ||
| } | ||
| private loadFromGlobal(): SQLiteInitModule { | ||
| if (typeof window !== "undefined") { | ||
| const windowWithSQLite = window as WindowWithSQLite; | ||
| if (windowWithSQLite.sqlite3InitModule) { | ||
| return windowWithSQLite.sqlite3InitModule; | ||
| } | ||
| } | ||
| throw new Error( | ||
| "SQLite WASM module not found globally. Please:\n" + | ||
| "1. Install via npm: npm install @sqlite.org/sqlite-wasm\n" + | ||
| "2. Or set loadStrategy to 'cdn' for automatic CDN loading\n" + | ||
| "3. Or include SQLite WASM script in your HTML before using UniSqlite" | ||
| ); | ||
| } | ||
| // =========================================================================== | ||
| // Worker Promiser Loading | ||
| // =========================================================================== | ||
| private async loadSQLiteWorkerPromiserFactory(): Promise<SQLiteWorker1PromiserFactory> { | ||
| const cacheKey = `worker-${this.config.loadStrategy}-${this.config.wasmUrl || this.config.cdnBaseUrl}-${this.config.version}`; | ||
| if (sqliteWorkerPromiserCache.has(cacheKey)) { | ||
| return sqliteWorkerPromiserCache.get(cacheKey)!; | ||
| } | ||
| const loadPromise = this.loadSQLiteWorkerPromiserFactoryInternal(); | ||
| sqliteWorkerPromiserCache.set(cacheKey, loadPromise); | ||
| return loadPromise; | ||
| } | ||
| private async loadSQLiteWorkerPromiserFactoryInternal(): Promise<SQLiteWorker1PromiserFactory> { | ||
| const { loadStrategy, wasmUrl, cdnBaseUrl, version, bundlerFriendly } = this.config; | ||
| switch (loadStrategy) { | ||
| case "npm": | ||
| return await this.loadWorkerPromiserFromNpm(); | ||
| case "cdn": | ||
| return await this.loadWorkerPromiserFromCdn(cdnBaseUrl!, version!, bundlerFriendly); | ||
| case "url": | ||
| if (!wasmUrl) { | ||
| throw new Error("wasmUrl must be provided when using 'url' loading strategy"); | ||
| } | ||
| return await this.loadWorkerPromiserFromUrl(wasmUrl); | ||
| case "module": | ||
| return await this.loadWorkerPromiserAsModule(); | ||
| case "global": | ||
| default: | ||
| return this.loadWorkerPromiserFromGlobal(); | ||
| } | ||
| } | ||
| private async loadWorkerPromiserFromNpm(): Promise<SQLiteWorker1PromiserFactory> { | ||
| try { | ||
| const module = await import("@sqlite.org/sqlite-wasm"); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }) | ||
| .sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| return promiser; | ||
| } | ||
| throw new Error("sqlite3Worker1Promiser export not found"); | ||
| } catch (error) { | ||
| console.warn("Failed to load SQLite WASM worker promiser from npm, falling back to CDN:", error); | ||
| return this.loadWorkerPromiserFromCdn(this.config.cdnBaseUrl!, this.config.version!, this.config.bundlerFriendly); | ||
| } | ||
| } | ||
| private async loadWorkerPromiserFromCdn( | ||
| baseUrl: string, | ||
| version: string, | ||
| bundlerFriendly?: boolean | ||
| ): Promise<SQLiteWorker1PromiserFactory> { | ||
| const filename = bundlerFriendly ? "sqlite3-bundler-friendly.mjs" : "index.mjs"; | ||
| const urlsToTry = [`${baseUrl}@${version}/${filename}`, `${baseUrl}@${version}/dist/${filename}`]; | ||
| let lastError: unknown; | ||
| for (const url of urlsToTry) { | ||
| try { | ||
| console.log(`Trying to load SQLite WASM worker promiser from: ${url}`); | ||
| const module = await import(url); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }) | ||
| .sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| console.log(`Successfully loaded SQLite WASM worker promiser from: ${url}`); | ||
| return promiser; | ||
| } | ||
| throw new Error("sqlite3Worker1Promiser export not found"); | ||
| } catch (error) { | ||
| lastError = error; | ||
| console.warn(`Failed to load SQLite WASM worker promiser from ${url}:`, error); | ||
| } | ||
| } | ||
| const errorMessage = lastError instanceof Error ? lastError.message : String(lastError); | ||
| throw new Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${errorMessage}`); | ||
| } | ||
| private async loadWorkerPromiserFromUrl(url: string): Promise<SQLiteWorker1PromiserFactory> { | ||
| const module = await import(url); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }) | ||
| .sqlite3Worker1Promiser; | ||
| if (typeof promiser !== "function") { | ||
| throw new Error(`sqlite3Worker1Promiser export not found in module loaded from ${url}`); | ||
| } | ||
| return promiser; | ||
| } | ||
| private async loadWorkerPromiserAsModule(): Promise<SQLiteWorker1PromiserFactory> { | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts !== "undefined") { | ||
| throw new Error("ES6 module loading not supported in Web Worker context"); | ||
| } | ||
| const possiblePaths = ["./sqlite3.mjs", "./sqlite3-bundler-friendly.mjs", "/sqlite3.mjs"]; | ||
| for (const path of possiblePaths) { | ||
| try { | ||
| const module = await import(path); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }) | ||
| .sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| return promiser; | ||
| } | ||
| } catch (error) { | ||
| console.warn(`Failed to load SQLite WASM worker promiser from ${path}:`, error); | ||
| } | ||
| } | ||
| throw new Error("Could not load sqlite3Worker1Promiser as ES6 module from any known path"); | ||
| } | ||
| private loadWorkerPromiserFromGlobal(): SQLiteWorker1PromiserFactory { | ||
| const globalWithPromiser = globalThis as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }; | ||
| if (typeof globalWithPromiser.sqlite3Worker1Promiser === "function") { | ||
| return globalWithPromiser.sqlite3Worker1Promiser; | ||
| } | ||
| throw new Error( | ||
| "SQLite WASM worker promiser not found globally. Please:\n" + | ||
| "1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm'\n" + | ||
| "2. Or set loadStrategy to 'cdn' for automatic CDN loading\n" + | ||
| "3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite" | ||
| ); | ||
| } | ||
| // =========================================================================== | ||
| // OPFS Database Creation | ||
| // =========================================================================== | ||
| private async tryCreatePersistentDatabase( | ||
| sqlite3: SQLiteAPI, | ||
| dbFileName: string, | ||
| storageBackend: "opfs" | "localStorage" | "auto" | ||
| ): Promise<boolean> { | ||
| if (storageBackend === "opfs" || storageBackend === "auto") { | ||
| const opfsVfsType = this.config.opfsVfsType ?? "auto"; | ||
| const vfsOrder: Array<"opfs-sahpool" | "opfs"> = | ||
| opfsVfsType === "auto" | ||
| ? sqlite3.oo1.OpfsDb | ||
| ? ["opfs"] | ||
| : ["opfs-sahpool", "opfs"] | ||
| : opfsVfsType === "sahpool" | ||
| ? ["opfs-sahpool"] | ||
| : ["opfs"]; | ||
| for (const vfsName of vfsOrder) { | ||
| const created = await this.tryCreateOpfsDatabase(sqlite3, dbFileName, vfsName); | ||
| if (created) { | ||
| return true; | ||
| } | ||
| } | ||
| if (storageBackend === "opfs") { | ||
| throw new Error(this.getOpfsUnavailableError()); | ||
| } | ||
| } | ||
| if (storageBackend === "localStorage" || storageBackend === "auto") { | ||
| if (sqlite3.oo1.JsStorageDb) { | ||
| const isValidLocalStoragePath = this.dbName === "local" || this.dbName === "session"; | ||
| if (isValidLocalStoragePath) { | ||
| try { | ||
| console.log("Creating localStorage-backed database:", this.dbName); | ||
| this.db = new sqlite3.oo1.JsStorageDb(this.dbName); | ||
| this.activeStorageBackend = "localStorage"; | ||
| console.log("Successfully created localStorage-backed database"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("localStorage database creation failed:", e); | ||
| if (storageBackend === "localStorage") { | ||
| throw new Error(`localStorage database creation failed: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| } else if (storageBackend === "localStorage") { | ||
| throw new Error( | ||
| `localStorage storage backend requires path to be 'local' or 'session', got '${this.dbName}'. ` + | ||
| "Use storageBackend: 'opfs' for custom database names with persistence." | ||
| ); | ||
| } else { | ||
| console.log(`Skipping localStorage: path '${this.dbName}' is not 'local' or 'session'`); | ||
| } | ||
| } else if (storageBackend === "localStorage") { | ||
| throw new Error("JsStorageDb is not available in this environment."); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| private getOpfsUnavailableError(): string { | ||
| return ( | ||
| "OPFS is not available.\n\n" + | ||
| "SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite " + | ||
| "will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood.\n\n" + | ||
| "Make sure:\n" + | ||
| "- You are in a secure context (HTTPS or localhost)\n" + | ||
| "- For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available\n" + | ||
| "- Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)" | ||
| ); | ||
| } | ||
| private async tryCreateOpfsDatabase( | ||
| sqlite3: SQLiteAPI, | ||
| dbFileName: string, | ||
| vfsName: "opfs" | "opfs-sahpool" | ||
| ): Promise<boolean> { | ||
| if (vfsName === "opfs") { | ||
| if (sqlite3.oo1.OpfsDb) { | ||
| try { | ||
| console.log("Creating OPFS-backed database (OpfsDb):", dbFileName); | ||
| this.db = new sqlite3.oo1.OpfsDb(dbFileName); | ||
| this.workerDbId = undefined; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = "opfs"; | ||
| console.log("Successfully created OPFS-backed database (OpfsDb)"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("OPFS database creation via OpfsDb failed:", e); | ||
| } | ||
| } | ||
| return await this.tryCreateWrappedWorkerOpfsDatabase(dbFileName, vfsName); | ||
| } | ||
| const sahpoolReady = await this.ensureSahpoolVfs(sqlite3); | ||
| if (sahpoolReady) { | ||
| try { | ||
| console.log("Creating SAHPool OPFS-backed database:", dbFileName); | ||
| this.db = new sqlite3.oo1.DB(dbFileName, "c", "opfs-sahpool"); | ||
| this.workerDbId = undefined; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = "opfs-sahpool"; | ||
| console.log("Successfully created SAHPool OPFS-backed database"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("SAHPool OPFS database creation failed:", e); | ||
| } | ||
| } | ||
| return await this.tryCreateWrappedWorkerOpfsDatabase(dbFileName, vfsName); | ||
| } | ||
| private async ensureSahpoolVfs(sqlite3: SQLiteAPI): Promise<boolean> { | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts === "undefined") { | ||
| return false; | ||
| } | ||
| const vfsName = "opfs-sahpool"; | ||
| try { | ||
| const existing = sqlite3.capi.sqlite3_vfs_find?.(vfsName); | ||
| if (existing) { | ||
| return true; | ||
| } | ||
| } catch (e) { | ||
| console.warn("SAHPool VFS lookup failed:", e); | ||
| } | ||
| if (typeof sqlite3.installOpfsSAHPoolVfs !== "function") { | ||
| return false; | ||
| } | ||
| try { | ||
| await sqlite3.installOpfsSAHPoolVfs({ | ||
| name: vfsName, | ||
| directory: "/unisqlite-sahpool", | ||
| }); | ||
| } catch (e) { | ||
| console.warn("SAHPool VFS installation failed:", e); | ||
| return false; | ||
| } | ||
| try { | ||
| const installed = sqlite3.capi.sqlite3_vfs_find?.(vfsName); | ||
| return !!installed; | ||
| } catch { | ||
| return true; | ||
| } | ||
| } | ||
| /** | ||
| * Create a Worker from the custom workerUrl config option. | ||
| * | ||
| * This is the preferred method for bundler integration (Vite, Webpack, etc.) | ||
| * as it allows the bundler to process and bundle the Worker code. | ||
| * | ||
| * @returns Worker instance if workerUrl is configured, undefined otherwise | ||
| */ | ||
| private createWorkerFromConfig(): Worker | undefined { | ||
| if (!this.config.workerUrl) { | ||
| return undefined; | ||
| } | ||
| if (typeof Worker === "undefined") { | ||
| return undefined; | ||
| } | ||
| const url = typeof this.config.workerUrl === 'string' | ||
| ? this.config.workerUrl | ||
| : this.config.workerUrl.href; | ||
| try { | ||
| return new Worker(url, { type: "module" }); | ||
| } catch (e) { | ||
| console.warn("Failed to create Worker from custom workerUrl:", e); | ||
| return undefined; | ||
| } | ||
| } | ||
| private createWorkerFromCdn(options: { installSahpool: boolean }): Worker | undefined { | ||
| if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") { | ||
| return undefined; | ||
| } | ||
| const baseUrl = this.config.cdnBaseUrl; | ||
| const version = this.config.version; | ||
| if (!baseUrl || !version) { | ||
| return undefined; | ||
| } | ||
| const jswasmDir = `${baseUrl}@${version}/sqlite-wasm/jswasm`; | ||
| const sqliteModuleUrl = `${jswasmDir}/sqlite3-bundler-friendly.mjs`; | ||
| const installSahpool = options.installSahpool | ||
| ? [ | ||
| ` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`, | ||
| ` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`, | ||
| " }", | ||
| ].join("\n") | ||
| : ""; | ||
| const source = [ | ||
| `import sqlite3InitModule from ${JSON.stringify(sqliteModuleUrl)};`, | ||
| "sqlite3InitModule().then(async (sqlite3) => {", | ||
| " try {", | ||
| installSahpool || " // no-op", | ||
| " } catch (e) {", | ||
| ' console.warn("SAHPool VFS installation failed:", e);', | ||
| " }", | ||
| " sqlite3.initWorker1API();", | ||
| "});", | ||
| "", | ||
| ].join("\n"); | ||
| const blob = new Blob([source], { type: "text/javascript" }); | ||
| const workerUrl = URL.createObjectURL(blob); | ||
| const worker = new Worker(workerUrl, { type: "module" }); | ||
| worker.addEventListener( | ||
| "message", | ||
| () => { | ||
| URL.revokeObjectURL(workerUrl); | ||
| }, | ||
| { once: true } | ||
| ); | ||
| worker.addEventListener( | ||
| "error", | ||
| () => { | ||
| URL.revokeObjectURL(workerUrl); | ||
| }, | ||
| { once: true } | ||
| ); | ||
| return worker; | ||
| } | ||
| private async getOrCreateWorkerPromiser(options?: { worker?: Worker | (() => Worker) }): Promise<SQLiteWorkerPromiser> { | ||
| if (this.workerPromiser) { | ||
| return this.workerPromiser; | ||
| } | ||
| if (this.workerPromiserInitPromise) { | ||
| return await this.workerPromiserInitPromise; | ||
| } | ||
| const factory = await this.loadSQLiteWorkerPromiserFactory(); | ||
| const wantsOpfs = | ||
| this.config.storageBackend === "opfs" || | ||
| this.config.opfsVfsType === "opfs" || | ||
| this.config.opfsVfsType === "sahpool"; | ||
| const timeoutMs = this.config.loadStrategy === "global" && !wantsOpfs ? 5000 : 20000; | ||
| this.workerPromiserInitPromise = new Promise<SQLiteWorkerPromiser>((resolve, reject) => { | ||
| let settled = false; | ||
| const timeoutId = setTimeout(() => { | ||
| if (settled) return; | ||
| settled = true; | ||
| reject(new Error(`SQLite worker initialization timed out after ${timeoutMs}ms`)); | ||
| }, timeoutMs); | ||
| const settle = <T>(fn: (value: T) => void, value: T) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| clearTimeout(timeoutId); | ||
| fn(value); | ||
| }; | ||
| const settleError = (error: unknown) => { | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| settle(reject, err); | ||
| }; | ||
| let factoryResult: unknown; | ||
| try { | ||
| factoryResult = factory({ | ||
| ...(options?.worker ? { worker: options.worker } : {}), | ||
| onready: (readyPromiser?: unknown) => { | ||
| if (typeof readyPromiser === "function") { | ||
| settle(resolve, readyPromiser as SQLiteWorkerPromiser); | ||
| return; | ||
| } | ||
| if (typeof factoryResult === "function") { | ||
| settle(resolve, factoryResult as SQLiteWorkerPromiser); | ||
| return; | ||
| } | ||
| if (factoryResult && typeof (factoryResult as { then?: unknown }).then === "function") { | ||
| (factoryResult as Promise<SQLiteWorkerPromiser>).then( | ||
| (p) => settle(resolve, p), | ||
| (e) => settleError(e) | ||
| ); | ||
| return; | ||
| } | ||
| settleError(new Error("sqlite3Worker1Promiser did not provide a usable promiser")); | ||
| }, | ||
| onerror: (event: unknown) => { | ||
| const message = event instanceof Error ? event.message : String(event); | ||
| settleError(new Error(`SQLite worker error: ${message}`)); | ||
| }, | ||
| }); | ||
| if (factoryResult && typeof (factoryResult as { then?: unknown }).then === "function") { | ||
| (factoryResult as Promise<SQLiteWorkerPromiser>).then( | ||
| (p) => settle(resolve, p), | ||
| (e) => settleError(e) | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| settleError(error); | ||
| } | ||
| }); | ||
| const promiser = await this.workerPromiserInitPromise; | ||
| this.workerPromiser = promiser; | ||
| return promiser; | ||
| } | ||
| private async tryCreateWrappedWorkerOpfsDatabase( | ||
| dbFileName: string, | ||
| vfsName: "opfs" | "opfs-sahpool" | ||
| ): Promise<boolean> { | ||
| try { | ||
| const workerOptions = | ||
| !this.workerPromiser && !this.workerPromiserInitPromise | ||
| ? (() => { | ||
| // Prefer custom workerUrl if provided (for bundler integration) | ||
| const worker = this.createWorkerFromConfig() | ||
| ?? (vfsName === "opfs-sahpool" | ||
| ? this.createWorkerFromCdn({ installSahpool: true }) | ||
| : this.createWorkerFromCdn({ installSahpool: false })); | ||
| return worker ? { worker } : undefined; | ||
| })() | ||
| : undefined; | ||
| const promiser = await this.getOrCreateWorkerPromiser(workerOptions); | ||
| const openMsg = (await promiser("open", { | ||
| filename: `file:${dbFileName}`, | ||
| vfs: vfsName, | ||
| })) as { dbId?: unknown; result?: { dbId?: unknown; vfs?: string } }; | ||
| const rawDbId = openMsg.dbId ?? openMsg.result?.dbId; | ||
| if (rawDbId === undefined) { | ||
| throw new Error("SQLite worker did not return a dbId"); | ||
| } | ||
| if (typeof rawDbId !== "number" && typeof rawDbId !== "string") { | ||
| throw new Error(`SQLite worker returned an invalid dbId (type=${typeof rawDbId})`); | ||
| } | ||
| this.db = undefined; | ||
| this.workerDbId = rawDbId; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = vfsName; | ||
| console.log(`Successfully created OPFS-backed database via wrapped worker (${vfsName}):`, dbFileName); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn(`Wrapped-worker OPFS database creation failed (vfs=${vfsName}):`, e); | ||
| return false; | ||
| } | ||
| } | ||
| } |
| /** | ||
| * Host Election using Web Locks API | ||
| * | ||
| * Uses navigator.locks to ensure only one tab holds the "Host" role at a time. | ||
| * The Host tab is responsible for managing the SQLite database connection. | ||
| * | ||
| * Key features: | ||
| * - Uses Web Locks for cross-tab mutual exclusion | ||
| * - Visibility-aware: only visible tabs participate in Host election | ||
| * - Supports blocking Host release during active transactions | ||
| */ | ||
| import type { TabId } from "./message-types.js"; | ||
| import { hostLogger as logger } from "./logger.js"; | ||
| export interface HostElectionOptions { | ||
| /** Database name (used to create unique lock name) */ | ||
| dbName: string; | ||
| /** This tab's unique ID */ | ||
| tabId: TabId; | ||
| /** Callback when this tab becomes Host */ | ||
| onBecomeHost: () => Promise<void>; | ||
| /** Callback when this tab loses Host role */ | ||
| onLoseHost: () => void; | ||
| /** Check if this tab is visible (for visibility-aware election) */ | ||
| checkVisibility?: () => boolean; | ||
| } | ||
| export class HostElection { | ||
| private readonly lockName: string; | ||
| private readonly txnLockName: string; | ||
| private abortController: AbortController | null = null; | ||
| private _isHost = false; | ||
| private _isStarted = false; | ||
| private releaseResolver: (() => void) | null = null; | ||
| private hostReadyResolver: (() => void) | null = null; | ||
| private hostReadyPromise: Promise<void> | null = null; | ||
| constructor(private readonly options: HostElectionOptions) { | ||
| this.lockName = `sqlite:host:${options.dbName}`; | ||
| this.txnLockName = `sqlite:txn:${options.dbName}`; | ||
| } | ||
| /** | ||
| * Check if Web Locks API is available | ||
| */ | ||
| static isSupported(): boolean { | ||
| return typeof navigator !== "undefined" && typeof navigator.locks !== "undefined"; | ||
| } | ||
| /** | ||
| * Get whether this tab is currently the Host | ||
| */ | ||
| get isHost(): boolean { | ||
| return this._isHost; | ||
| } | ||
| /** | ||
| * Start participating in Host election. | ||
| * Returns immediately after starting the election process. | ||
| * Use `waitForHost()` if you need to wait until this tab becomes Host. | ||
| */ | ||
| async start(): Promise<void> { | ||
| // If already started, return immediately | ||
| if (this._isStarted) { | ||
| return; | ||
| } | ||
| // Check if Web Locks is supported | ||
| if (!HostElection.isSupported()) { | ||
| logger.warn("Web Locks API not supported. Running in local-only mode."); | ||
| this._isStarted = true; | ||
| this._isHost = true; | ||
| // Immediately become Host since we can't coordinate with other tabs | ||
| this.options.onBecomeHost().catch((e) => { | ||
| logger.error("Error in onBecomeHost callback:", e); | ||
| this._isHost = false; | ||
| }); | ||
| return; | ||
| } | ||
| // Check visibility if configured | ||
| if (this.options.checkVisibility && !this.options.checkVisibility()) { | ||
| // Not visible, wait for visibility change | ||
| return; | ||
| } | ||
| this._isStarted = true; | ||
| // Create a promise that resolves when we become Host | ||
| this.hostReadyPromise = new Promise<void>((resolve) => { | ||
| this.hostReadyResolver = resolve; | ||
| }); | ||
| // Start lock acquisition in background (don't await - returns immediately) | ||
| this.tryAcquireHost().catch((e) => { | ||
| logger.error("Error in host election:", e); | ||
| }); | ||
| // Return immediately - caller can use waitForHost() if needed | ||
| } | ||
| /** | ||
| * Wait until this tab becomes Host. | ||
| * Resolves immediately if already Host. | ||
| */ | ||
| async waitForHost(): Promise<void> { | ||
| if (this._isHost) { | ||
| return; | ||
| } | ||
| if (this.hostReadyPromise) { | ||
| return this.hostReadyPromise; | ||
| } | ||
| // Not started yet, start and wait | ||
| await this.start(); | ||
| if (this.hostReadyPromise) { | ||
| return this.hostReadyPromise; | ||
| } | ||
| } | ||
| /** | ||
| * Try to acquire the Host lock | ||
| */ | ||
| private async tryAcquireHost(): Promise<void> { | ||
| this.abortController = new AbortController(); | ||
| try { | ||
| await navigator.locks.request( | ||
| this.lockName, | ||
| { mode: "exclusive", signal: this.abortController.signal }, | ||
| async (lock) => { | ||
| if (!lock) { | ||
| // Lock request was aborted | ||
| return; | ||
| } | ||
| // We are now the Host | ||
| this._isHost = true; | ||
| logger.log(`Tab ${this.options.tabId} became Host for ${this.options.dbName}`); | ||
| try { | ||
| await this.options.onBecomeHost(); | ||
| } catch (e) { | ||
| logger.error("Error in onBecomeHost callback:", e); | ||
| // Release host role on error | ||
| this._isHost = false; | ||
| throw e; | ||
| } | ||
| // Signal that we're ready as Host | ||
| if (this.hostReadyResolver) { | ||
| this.hostReadyResolver(); | ||
| this.hostReadyResolver = null; | ||
| this.hostReadyPromise = null; | ||
| } | ||
| // Hold the lock until releaseHost() is called or abort signal fires | ||
| await new Promise<void>((resolve) => { | ||
| this.releaseResolver = resolve; | ||
| // Also resolve if abort signal fires | ||
| this.abortController!.signal.addEventListener("abort", () => { | ||
| resolve(); | ||
| }); | ||
| }); | ||
| // We are no longer the Host | ||
| this._isHost = false; | ||
| this.releaseResolver = null; | ||
| logger.log(`Tab ${this.options.tabId} lost Host role for ${this.options.dbName}`); | ||
| try { | ||
| this.options.onLoseHost(); | ||
| } catch (e) { | ||
| logger.error("Error in onLoseHost callback:", e); | ||
| } | ||
| } | ||
| ); | ||
| } catch (e) { | ||
| if ((e as Error).name === "AbortError") { | ||
| // Lock request was aborted, this is expected | ||
| return; | ||
| } | ||
| logger.error("Error acquiring Host lock:", e); | ||
| throw e; | ||
| } finally { | ||
| this._isStarted = false; | ||
| this.abortController = null; | ||
| } | ||
| } | ||
| /** | ||
| * Release the Host role if currently held. | ||
| * This allows another tab to become Host. | ||
| */ | ||
| releaseHost(): void { | ||
| if (this.releaseResolver) { | ||
| this.releaseResolver(); | ||
| this.releaseResolver = null; | ||
| } | ||
| if (this.abortController) { | ||
| this.abortController.abort(); | ||
| this.abortController = null; | ||
| } | ||
| } | ||
| /** | ||
| * Acquire a transaction lock to prevent Host release during transaction. | ||
| * Returns a release function that must be called when transaction completes. | ||
| * | ||
| * This is used by TransactionSessionManager to hold the Host during transactions. | ||
| */ | ||
| async acquireTransactionLock(): Promise<() => void> { | ||
| if (!HostElection.isSupported()) { | ||
| // No-op if Web Locks not supported | ||
| return () => {}; | ||
| } | ||
| const controller = new AbortController(); | ||
| let releaseResolver: (() => void) | null = null; | ||
| let lockAcquiredResolver: (() => void) | null = null; | ||
| // Promise that resolves when the lock is actually acquired | ||
| const lockAcquiredPromise = new Promise<void>((resolve) => { | ||
| lockAcquiredResolver = resolve; | ||
| }); | ||
| const lockPromise = navigator.locks.request( | ||
| this.txnLockName, | ||
| { mode: "exclusive", signal: controller.signal }, | ||
| async (lock) => { | ||
| if (!lock) return; | ||
| // Signal that lock is acquired | ||
| lockAcquiredResolver?.(); | ||
| // Hold lock until released | ||
| await new Promise<void>((resolve) => { | ||
| releaseResolver = resolve; | ||
| }); | ||
| } | ||
| ).catch((e) => { | ||
| if ((e as Error).name !== "AbortError") { | ||
| logger.error("Error acquiring transaction lock:", e); | ||
| } | ||
| // Reject the acquired promise if lock fails | ||
| lockAcquiredResolver?.(); | ||
| }); | ||
| // Wait for the lock to actually be acquired | ||
| await lockAcquiredPromise; | ||
| return () => { | ||
| if (releaseResolver) { | ||
| releaseResolver(); | ||
| } else { | ||
| controller.abort(); | ||
| } | ||
| }; | ||
| } | ||
| /** | ||
| * Check if there's a pending transaction lock | ||
| */ | ||
| async hasTransactionLock(): Promise<boolean> { | ||
| if (!HostElection.isSupported()) { | ||
| return false; | ||
| } | ||
| try { | ||
| const state = await navigator.locks.query(); | ||
| return state.held?.some((lock) => lock.name === this.txnLockName) ?? false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| /** | ||
| * Close and cleanup | ||
| */ | ||
| async close(): Promise<void> { | ||
| this.releaseHost(); | ||
| } | ||
| } |
| /** | ||
| * Browser Adapter Module | ||
| * | ||
| * Multi-tab shared SQLite implementation using: | ||
| * - Web Locks for Host election | ||
| * - BroadcastChannel for RPC communication | ||
| * - Transaction Session Protocol for interactive transactions | ||
| */ | ||
| export { BrowserAdapter } from "./browser-adapter.js"; | ||
| export { HostElection } from "./host-election.js"; | ||
| export { VisibilityManager } from "./visibility-manager.js"; | ||
| export { RpcChannel } from "./rpc-channel.js"; | ||
| export { DbWorkerHost } from "./db-worker-host.js"; | ||
| export { TransactionSessionManager } from "./transaction-session.js"; | ||
| export { RemoteTransactionAdapter } from "./remote-transaction-adapter.js"; | ||
| // Re-export message types | ||
| export * from "./message-types.js"; | ||
| // Re-export logger utilities | ||
| export { enableDebug, disableDebug, createDebugLogger } from "./logger.js"; | ||
| export type { Logger } from "./logger.js"; |
| /** | ||
| * Debug Logger for Multi-Tab SQLite | ||
| * | ||
| * Uses the `debug` package for conditional logging. | ||
| * Enable logs by setting localStorage.debug or DEBUG env var: | ||
| * | ||
| * - `unisqlite:*` - Enable all unisqlite logs | ||
| * - `unisqlite:host` - Host election logs only | ||
| * - `unisqlite:txn` - Transaction session logs only | ||
| * - `unisqlite:adapter` - Browser adapter logs only | ||
| * | ||
| * In browser console: | ||
| * localStorage.debug = 'unisqlite:*' | ||
| * | ||
| * In Node.js: | ||
| * DEBUG=unisqlite:* node app.js | ||
| */ | ||
| import debug from "debug"; | ||
| const NAMESPACE = "unisqlite"; | ||
| /** | ||
| * Logger interface with log, warn, and error methods | ||
| */ | ||
| export interface Logger { | ||
| log: (...args: unknown[]) => void; | ||
| warn: (...args: unknown[]) => void; | ||
| error: (...args: unknown[]) => void; | ||
| } | ||
| /** | ||
| * Create a namespaced debug logger with log/warn/error methods | ||
| */ | ||
| function createModuleLogger(name: string): Logger { | ||
| const logDebug = debug(`${NAMESPACE}:${name}`); | ||
| const warnDebug = debug(`${NAMESPACE}:${name}:warn`); | ||
| const errorDebug = debug(`${NAMESPACE}:${name}:error`); | ||
| // Enable error logs by default (they go to stderr) | ||
| errorDebug.enabled = true; | ||
| return { | ||
| log: logDebug as (...args: unknown[]) => void, | ||
| warn: warnDebug as (...args: unknown[]) => void, | ||
| error: errorDebug as (...args: unknown[]) => void, | ||
| }; | ||
| } | ||
| // Pre-created loggers for each module | ||
| export const hostLogger = createModuleLogger("host"); | ||
| export const txnLogger = createModuleLogger("txn"); | ||
| export const adapterLogger = createModuleLogger("adapter"); | ||
| /** | ||
| * Create a custom debug logger | ||
| */ | ||
| export function createDebugLogger(name: string): Logger { | ||
| return createModuleLogger(name); | ||
| } | ||
| /** | ||
| * Enable debug logging programmatically | ||
| * This is useful for enabling logs without setting localStorage/env | ||
| * | ||
| * @param namespaces - Debug namespaces to enable (default: "unisqlite:*") | ||
| * | ||
| * @example | ||
| * // Enable all unisqlite logs | ||
| * enableDebug("unisqlite:*"); | ||
| * | ||
| * // Enable only host election logs | ||
| * enableDebug("unisqlite:host"); | ||
| * | ||
| * // Enable multiple namespaces | ||
| * enableDebug("unisqlite:host,unisqlite:txn"); | ||
| */ | ||
| export function enableDebug(namespaces: string = "unisqlite:*"): void { | ||
| debug.enable(namespaces); | ||
| } | ||
| /** | ||
| * Disable all debug logging | ||
| */ | ||
| export function disableDebug(): void { | ||
| debug.disable(); | ||
| } |
| /** | ||
| * Message types for multi-tab SQLite communication | ||
| * | ||
| * Uses BroadcastChannel for RPC and transaction messages between tabs. | ||
| * Only one "Host" tab holds the actual SQLite connection; other tabs | ||
| * ("Followers") send requests via RPC. | ||
| */ | ||
| import type { SQLiteParams, SQLiteValue, QueryResult, RunResult } from "../../types.js"; | ||
| /** Unique identifier for each browser tab */ | ||
| export type TabId = string; | ||
| // ============================================================================= | ||
| // RPC Messages (sqlite:rpc:${dbName}) | ||
| // ============================================================================= | ||
| /** Payload for SQL operations */ | ||
| export interface SqlPayload { | ||
| sql: string; | ||
| params?: SQLiteParams; | ||
| } | ||
| /** RPC request from Follower to Host */ | ||
| export interface RpcRequest { | ||
| t: "req"; | ||
| from: TabId; | ||
| id: string; | ||
| op: "query" | "queryRaw" | "run" | "exec"; | ||
| payload: SqlPayload; | ||
| } | ||
| /** RPC response from Host to Follower */ | ||
| export interface RpcResponse { | ||
| t: "res"; | ||
| to: TabId; | ||
| id: string; | ||
| ok: boolean; | ||
| result?: unknown; | ||
| error?: { name: string; message: string; stack?: string }; | ||
| } | ||
| // ============================================================================= | ||
| // Transaction Messages (also on sqlite:rpc:${dbName}) | ||
| // ============================================================================= | ||
| /** Begin a new transaction session */ | ||
| export interface TxnBeginRequest { | ||
| t: "txn_begin"; | ||
| from: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| mode?: "deferred" | "immediate" | "exclusive"; | ||
| } | ||
| /** Execute SQL within a transaction */ | ||
| export interface TxnExecRequest { | ||
| t: "txn_exec"; | ||
| from: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| op: "query" | "queryRaw" | "run" | "exec"; | ||
| payload: SqlPayload; | ||
| } | ||
| /** Commit a transaction */ | ||
| export interface TxnCommitRequest { | ||
| t: "txn_commit"; | ||
| from: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| } | ||
| /** Rollback a transaction */ | ||
| export interface TxnRollbackRequest { | ||
| t: "txn_rollback"; | ||
| from: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| } | ||
| /** Heartbeat to keep transaction alive */ | ||
| export interface TxnHeartbeat { | ||
| t: "txn_heartbeat"; | ||
| from: TabId; | ||
| txnId: string; | ||
| } | ||
| /** Transaction acknowledgment from Host */ | ||
| export interface TxnAck { | ||
| t: "txn_ack"; | ||
| to: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| } | ||
| /** Transaction result from Host */ | ||
| export interface TxnResult { | ||
| t: "txn_result"; | ||
| to: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| ok: boolean; | ||
| result?: unknown; | ||
| error?: { name: string; message: string; stack?: string }; | ||
| } | ||
| /** Transaction completed (commit/rollback done) */ | ||
| export interface TxnDone { | ||
| t: "txn_done"; | ||
| to: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| } | ||
| /** Transaction error */ | ||
| export interface TxnError { | ||
| t: "txn_error"; | ||
| to: TabId; | ||
| id: string; | ||
| txnId: string; | ||
| error: { name: string; message: string; stack?: string }; | ||
| } | ||
| // ============================================================================= | ||
| // Union Types | ||
| // ============================================================================= | ||
| /** All request message types (from Follower) */ | ||
| export type RequestMessage = | ||
| | RpcRequest | ||
| | TxnBeginRequest | ||
| | TxnExecRequest | ||
| | TxnCommitRequest | ||
| | TxnRollbackRequest | ||
| | TxnHeartbeat; | ||
| /** All response message types (from Host) */ | ||
| export type ResponseMessage = RpcResponse | TxnAck | TxnResult | TxnDone | TxnError; | ||
| /** All message types on the RPC channel */ | ||
| export type RpcChannelMessage = RequestMessage | ResponseMessage; | ||
| // ============================================================================= | ||
| // Helper functions | ||
| // ============================================================================= | ||
| /** Convert an error to a serializable format */ | ||
| export function serializeError(e: unknown): { name: string; message: string; stack?: string } { | ||
| if (e instanceof Error) { | ||
| return { | ||
| name: e.name, | ||
| message: e.message, | ||
| stack: e.stack, | ||
| }; | ||
| } | ||
| return { | ||
| name: "Error", | ||
| message: String(e), | ||
| }; | ||
| } | ||
| /** Deserialize an error from a message */ | ||
| export function deserializeError(e: { name: string; message: string; stack?: string }): Error { | ||
| const error = new Error(e.message); | ||
| error.name = e.name; | ||
| if (e.stack) { | ||
| error.stack = e.stack; | ||
| } | ||
| return error; | ||
| } | ||
| /** Type guard for request messages */ | ||
| export function isRequestMessage(msg: RpcChannelMessage): msg is RequestMessage { | ||
| return ( | ||
| msg.t === "req" || | ||
| msg.t === "txn_begin" || | ||
| msg.t === "txn_exec" || | ||
| msg.t === "txn_commit" || | ||
| msg.t === "txn_rollback" || | ||
| msg.t === "txn_heartbeat" | ||
| ); | ||
| } | ||
| /** Type guard for response messages */ | ||
| export function isResponseMessage(msg: RpcChannelMessage): msg is ResponseMessage { | ||
| return ( | ||
| msg.t === "res" || | ||
| msg.t === "txn_ack" || | ||
| msg.t === "txn_result" || | ||
| msg.t === "txn_done" || | ||
| msg.t === "txn_error" | ||
| ); | ||
| } | ||
| /** Generate a unique request ID */ | ||
| export function generateRequestId(): string { | ||
| return crypto.randomUUID(); | ||
| } | ||
| /** Generate a unique tab ID */ | ||
| export function generateTabId(): TabId { | ||
| return crypto.randomUUID(); | ||
| } | ||
| /** Generate a unique transaction ID */ | ||
| export function generateTxnId(): string { | ||
| return crypto.randomUUID(); | ||
| } |
| /** | ||
| * Remote Transaction Adapter | ||
| * | ||
| * Implements UniStoreConnection for Follower tabs executing transactions. | ||
| * Each SQL operation is sent to the Host via RPC. | ||
| */ | ||
| import type { | ||
| UniStoreConnection, | ||
| SQLiteParams, | ||
| SQLiteValue, | ||
| QueryResult, | ||
| RunResult, | ||
| ConnectionType, | ||
| } from "../../types.js"; | ||
| import type { RpcChannel } from "./rpc-channel.js"; | ||
| export class RemoteTransactionAdapter implements UniStoreConnection { | ||
| private committed = false; | ||
| private rolledBack = false; | ||
| private heartbeatInterval?: ReturnType<typeof setInterval>; | ||
| constructor( | ||
| private readonly rpcChannel: RpcChannel, | ||
| private readonly txnId: string, | ||
| private readonly heartbeatIntervalMs = 5000 | ||
| ) { | ||
| // Start sending heartbeats | ||
| this.startHeartbeat(); | ||
| } | ||
| /** | ||
| * Start sending periodic heartbeats to keep transaction alive | ||
| */ | ||
| private startHeartbeat(): void { | ||
| this.heartbeatInterval = setInterval(() => { | ||
| if (!this.committed && !this.rolledBack) { | ||
| this.rpcChannel.txnHeartbeat(this.txnId); | ||
| } | ||
| }, this.heartbeatIntervalMs); | ||
| } | ||
| /** | ||
| * Stop sending heartbeats | ||
| */ | ||
| private stopHeartbeat(): void { | ||
| if (this.heartbeatInterval) { | ||
| clearInterval(this.heartbeatInterval); | ||
| this.heartbeatInterval = undefined; | ||
| } | ||
| } | ||
| /** | ||
| * Ensure the transaction is still active | ||
| */ | ||
| private ensureActive(): void { | ||
| if (this.committed) { | ||
| throw new Error("Transaction already committed"); | ||
| } | ||
| if (this.rolledBack) { | ||
| throw new Error("Transaction already rolled back"); | ||
| } | ||
| } | ||
| // =========================================================================== | ||
| // UniStoreConnection implementation | ||
| // =========================================================================== | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| this.ensureActive(); | ||
| const result = await this.rpcChannel.txnExec<QueryResult<T>>( | ||
| this.txnId, | ||
| "query", | ||
| { sql, params } | ||
| ); | ||
| return result.rows; | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| this.ensureActive(); | ||
| return await this.rpcChannel.txnExec<QueryResult<T>>( | ||
| this.txnId, | ||
| "queryRaw", | ||
| { sql, params } | ||
| ); | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| this.ensureActive(); | ||
| const result = await this.rpcChannel.txnExec<QueryResult<unknown>>( | ||
| this.txnId, | ||
| "run", | ||
| { sql, params } | ||
| ); | ||
| return { | ||
| rowsAffected: result.rowsAffected ?? 0, | ||
| lastInsertRowId: result.lastInsertRowId ?? 0, | ||
| }; | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| this.ensureActive(); | ||
| await this.rpcChannel.txnExec(this.txnId, "exec", { sql }); | ||
| } | ||
| /** | ||
| * Nested transaction: execute within the same transaction | ||
| */ | ||
| async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> { | ||
| this.ensureActive(); | ||
| const result = fn(this); | ||
| return result instanceof Promise ? await result : result; | ||
| } | ||
| /** | ||
| * Async transaction: execute within the same transaction | ||
| */ | ||
| async asyncTransaction<T>( | ||
| fn: (tx: UniStoreConnection) => Promise<T>, | ||
| _options?: { timeoutMs?: number } | ||
| ): Promise<T> { | ||
| this.ensureActive(); | ||
| return await fn(this); | ||
| } | ||
| getConnectionType(): ConnectionType { | ||
| return "asyncTxn"; | ||
| } | ||
| async close(): Promise<void> { | ||
| // No-op for transaction adapter | ||
| } | ||
| // =========================================================================== | ||
| // Internal methods for commit/rollback | ||
| // =========================================================================== | ||
| /** | ||
| * Commit the transaction (called by BrowserAdapter) | ||
| * @internal | ||
| */ | ||
| async _commit(): Promise<void> { | ||
| this.ensureActive(); | ||
| this.stopHeartbeat(); | ||
| try { | ||
| await this.rpcChannel.txnCommit(this.txnId); | ||
| this.committed = true; | ||
| } catch (e) { | ||
| // If commit fails, mark as rolled back | ||
| this.rolledBack = true; | ||
| throw e; | ||
| } | ||
| } | ||
| /** | ||
| * Rollback the transaction (called by BrowserAdapter) | ||
| * @internal | ||
| */ | ||
| async _rollback(): Promise<void> { | ||
| if (this.committed || this.rolledBack) { | ||
| return; | ||
| } | ||
| this.stopHeartbeat(); | ||
| try { | ||
| await this.rpcChannel.txnRollback(this.txnId); | ||
| } finally { | ||
| this.rolledBack = true; | ||
| } | ||
| } | ||
| /** | ||
| * Check if the transaction is committed | ||
| */ | ||
| isCommitted(): boolean { | ||
| return this.committed; | ||
| } | ||
| /** | ||
| * Check if the transaction is rolled back | ||
| */ | ||
| isRolledBack(): boolean { | ||
| return this.rolledBack; | ||
| } | ||
| } |
| /** | ||
| * RPC Channel using BroadcastChannel | ||
| * | ||
| * Provides request/response semantics over BroadcastChannel for cross-tab | ||
| * communication. Handles message routing, timeouts, and error propagation. | ||
| */ | ||
| import type { | ||
| TabId, | ||
| RpcChannelMessage, | ||
| RpcRequest, | ||
| RpcResponse, | ||
| RequestMessage, | ||
| ResponseMessage, | ||
| TxnBeginRequest, | ||
| TxnExecRequest, | ||
| TxnCommitRequest, | ||
| TxnRollbackRequest, | ||
| TxnHeartbeat, | ||
| TxnAck, | ||
| TxnResult, | ||
| TxnDone, | ||
| TxnError, | ||
| SqlPayload, | ||
| } from "./message-types.js"; | ||
| import { | ||
| generateRequestId, | ||
| serializeError, | ||
| deserializeError, | ||
| isRequestMessage, | ||
| isResponseMessage, | ||
| } from "./message-types.js"; | ||
| export interface PendingRequest { | ||
| resolve: (value: unknown) => void; | ||
| reject: (error: Error) => void; | ||
| timer: ReturnType<typeof setTimeout>; | ||
| } | ||
| export type RequestHandler = (req: RequestMessage) => Promise<unknown>; | ||
| export class RpcChannel { | ||
| private channel: BroadcastChannel; | ||
| private pending = new Map<string, PendingRequest>(); | ||
| private requestHandler?: RequestHandler; | ||
| private closed = false; | ||
| constructor( | ||
| private readonly dbName: string, | ||
| private readonly tabId: TabId, | ||
| private readonly defaultTimeoutMs = 30000 | ||
| ) { | ||
| this.channel = new BroadcastChannel(`sqlite:rpc:${dbName}`); | ||
| this.channel.onmessage = this.handleMessage.bind(this); | ||
| } | ||
| /** | ||
| * Handle incoming messages | ||
| */ | ||
| private handleMessage(event: MessageEvent<RpcChannelMessage>): void { | ||
| if (this.closed) return; | ||
| const msg = event.data; | ||
| if (!msg || typeof msg.t !== "string") return; | ||
| if (isResponseMessage(msg)) { | ||
| this.handleResponse(msg); | ||
| } else if (isRequestMessage(msg) && this.requestHandler) { | ||
| // Only handle requests if we have a handler (i.e., we're the Host) | ||
| // Don't handle our own requests | ||
| if ("from" in msg && msg.from !== this.tabId) { | ||
| void this.handleRequest(msg); | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Handle response messages | ||
| */ | ||
| private handleResponse(msg: ResponseMessage): void { | ||
| // Check if this response is for us | ||
| if ("to" in msg && msg.to !== this.tabId) return; | ||
| if (!("id" in msg)) return; | ||
| const pending = this.pending.get(msg.id); | ||
| if (!pending) return; | ||
| clearTimeout(pending.timer); | ||
| this.pending.delete(msg.id); | ||
| switch (msg.t) { | ||
| case "res": | ||
| if (msg.ok) { | ||
| pending.resolve(msg.result); | ||
| } else { | ||
| pending.reject(deserializeError(msg.error!)); | ||
| } | ||
| break; | ||
| case "txn_ack": | ||
| case "txn_done": | ||
| pending.resolve(undefined); | ||
| break; | ||
| case "txn_result": | ||
| if (msg.ok) { | ||
| pending.resolve(msg.result); | ||
| } else { | ||
| pending.reject(deserializeError(msg.error!)); | ||
| } | ||
| break; | ||
| case "txn_error": | ||
| pending.reject(deserializeError(msg.error)); | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Handle request messages (when we're the Host) | ||
| */ | ||
| private async handleRequest(req: RequestMessage): Promise<void> { | ||
| if (!this.requestHandler) return; | ||
| try { | ||
| const result = await this.requestHandler(req); | ||
| this.sendResponse(req, result); | ||
| } catch (e) { | ||
| this.sendErrorResponse(req, e); | ||
| } | ||
| } | ||
| /** | ||
| * Send a success response | ||
| */ | ||
| private sendResponse(req: RequestMessage, result: unknown): void { | ||
| if (this.closed) return; | ||
| const from = "from" in req ? req.from : undefined; | ||
| const id = "id" in req ? req.id : undefined; | ||
| if (!from || !id) return; | ||
| switch (req.t) { | ||
| case "req": | ||
| this.channel.postMessage({ | ||
| t: "res", | ||
| to: from, | ||
| id, | ||
| ok: true, | ||
| result, | ||
| } satisfies RpcResponse); | ||
| break; | ||
| case "txn_begin": | ||
| this.channel.postMessage({ | ||
| t: "txn_ack", | ||
| to: from, | ||
| id, | ||
| txnId: req.txnId, | ||
| } satisfies TxnAck); | ||
| break; | ||
| case "txn_exec": | ||
| this.channel.postMessage({ | ||
| t: "txn_result", | ||
| to: from, | ||
| id, | ||
| txnId: req.txnId, | ||
| ok: true, | ||
| result, | ||
| } satisfies TxnResult); | ||
| break; | ||
| case "txn_commit": | ||
| case "txn_rollback": | ||
| this.channel.postMessage({ | ||
| t: "txn_done", | ||
| to: from, | ||
| id, | ||
| txnId: req.txnId, | ||
| } satisfies TxnDone); | ||
| break; | ||
| case "txn_heartbeat": | ||
| // No response needed for heartbeats | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Send an error response | ||
| */ | ||
| private sendErrorResponse(req: RequestMessage, error: unknown): void { | ||
| if (this.closed) return; | ||
| const from = "from" in req ? req.from : undefined; | ||
| const id = "id" in req ? req.id : undefined; | ||
| if (!from || !id) return; | ||
| const serializedError = serializeError(error); | ||
| switch (req.t) { | ||
| case "req": | ||
| this.channel.postMessage({ | ||
| t: "res", | ||
| to: from, | ||
| id, | ||
| ok: false, | ||
| error: serializedError, | ||
| } satisfies RpcResponse); | ||
| break; | ||
| case "txn_begin": | ||
| case "txn_exec": | ||
| case "txn_commit": | ||
| case "txn_rollback": | ||
| this.channel.postMessage({ | ||
| t: "txn_error", | ||
| to: from, | ||
| id, | ||
| txnId: (req as { txnId: string }).txnId, | ||
| error: serializedError, | ||
| } satisfies TxnError); | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Set the request handler (makes this channel act as Host) | ||
| */ | ||
| setRequestHandler(handler: RequestHandler | undefined): void { | ||
| this.requestHandler = handler; | ||
| } | ||
| /** | ||
| * Send an RPC request and wait for response | ||
| */ | ||
| async request<T>( | ||
| op: "query" | "queryRaw" | "run" | "exec", | ||
| payload: SqlPayload, | ||
| timeoutMs?: number | ||
| ): Promise<T> { | ||
| const id = generateRequestId(); | ||
| const timeout = timeoutMs ?? this.defaultTimeoutMs; | ||
| return new Promise<T>((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`RPC timeout after ${timeout}ms for ${op}`)); | ||
| }, timeout); | ||
| this.pending.set(id, { | ||
| resolve: resolve as (value: unknown) => void, | ||
| reject, | ||
| timer, | ||
| }); | ||
| this.channel.postMessage({ | ||
| t: "req", | ||
| from: this.tabId, | ||
| id, | ||
| op, | ||
| payload, | ||
| } satisfies RpcRequest); | ||
| }); | ||
| } | ||
| /** | ||
| * Begin a transaction | ||
| */ | ||
| async txnBegin( | ||
| txnId: string, | ||
| mode: "deferred" | "immediate" | "exclusive" = "immediate", | ||
| timeoutMs?: number | ||
| ): Promise<void> { | ||
| const id = generateRequestId(); | ||
| const timeout = timeoutMs ?? this.defaultTimeoutMs; | ||
| return new Promise<void>((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`Transaction begin timeout after ${timeout}ms`)); | ||
| }, timeout); | ||
| this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer }); | ||
| this.channel.postMessage({ | ||
| t: "txn_begin", | ||
| from: this.tabId, | ||
| id, | ||
| txnId, | ||
| mode, | ||
| } satisfies TxnBeginRequest); | ||
| }); | ||
| } | ||
| /** | ||
| * Execute SQL within a transaction | ||
| */ | ||
| async txnExec<T>( | ||
| txnId: string, | ||
| op: "query" | "queryRaw" | "run" | "exec", | ||
| payload: SqlPayload, | ||
| timeoutMs?: number | ||
| ): Promise<T> { | ||
| const id = generateRequestId(); | ||
| const timeout = timeoutMs ?? 10000; // Shorter timeout for individual SQL | ||
| return new Promise<T>((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`Transaction SQL timeout after ${timeout}ms`)); | ||
| }, timeout); | ||
| this.pending.set(id, { | ||
| resolve: resolve as (value: unknown) => void, | ||
| reject, | ||
| timer, | ||
| }); | ||
| this.channel.postMessage({ | ||
| t: "txn_exec", | ||
| from: this.tabId, | ||
| id, | ||
| txnId, | ||
| op, | ||
| payload, | ||
| } satisfies TxnExecRequest); | ||
| }); | ||
| } | ||
| /** | ||
| * Commit a transaction | ||
| */ | ||
| async txnCommit(txnId: string, timeoutMs?: number): Promise<void> { | ||
| const id = generateRequestId(); | ||
| const timeout = timeoutMs ?? 10000; | ||
| return new Promise<void>((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`Transaction commit timeout after ${timeout}ms`)); | ||
| }, timeout); | ||
| this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer }); | ||
| this.channel.postMessage({ | ||
| t: "txn_commit", | ||
| from: this.tabId, | ||
| id, | ||
| txnId, | ||
| } satisfies TxnCommitRequest); | ||
| }); | ||
| } | ||
| /** | ||
| * Rollback a transaction | ||
| */ | ||
| async txnRollback(txnId: string, timeoutMs?: number): Promise<void> { | ||
| const id = generateRequestId(); | ||
| const timeout = timeoutMs ?? 10000; | ||
| return new Promise<void>((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| this.pending.delete(id); | ||
| reject(new Error(`Transaction rollback timeout after ${timeout}ms`)); | ||
| }, timeout); | ||
| this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer }); | ||
| this.channel.postMessage({ | ||
| t: "txn_rollback", | ||
| from: this.tabId, | ||
| id, | ||
| txnId, | ||
| } satisfies TxnRollbackRequest); | ||
| }); | ||
| } | ||
| /** | ||
| * Send transaction heartbeat (no response expected) | ||
| */ | ||
| txnHeartbeat(txnId: string): void { | ||
| if (this.closed) return; | ||
| this.channel.postMessage({ | ||
| t: "txn_heartbeat", | ||
| from: this.tabId, | ||
| txnId, | ||
| } satisfies TxnHeartbeat); | ||
| } | ||
| /** | ||
| * Close the channel and cleanup | ||
| */ | ||
| close(): void { | ||
| this.closed = true; | ||
| this.channel.close(); | ||
| // Reject all pending requests | ||
| for (const pending of this.pending.values()) { | ||
| clearTimeout(pending.timer); | ||
| pending.reject(new Error("RPC channel closed")); | ||
| } | ||
| this.pending.clear(); | ||
| } | ||
| } |
| /** | ||
| * Transaction Session Manager | ||
| * | ||
| * Manages transaction sessions on the Host tab. Handles: | ||
| * - Active transaction tracking | ||
| * - Transaction queuing (one active at a time) | ||
| * - Heartbeat timeout detection | ||
| * - Automatic rollback on timeout or error | ||
| */ | ||
| import type { TabId } from "./message-types.js"; | ||
| import type { DbWorkerHost } from "./db-worker-host.js"; | ||
| import type { HostElection } from "./host-election.js"; | ||
| import { txnLogger as logger } from "./logger.js"; | ||
| export interface ActiveTransaction { | ||
| txnId: string; | ||
| originTabId: TabId; | ||
| startedAt: number; | ||
| lastActivityAt: number; | ||
| mode: "deferred" | "immediate" | "exclusive"; | ||
| } | ||
| export interface TransactionSessionManagerOptions { | ||
| /** Timeout for transaction inactivity (ms) */ | ||
| txnTimeoutMs?: number; | ||
| /** Timeout for individual operations (ms) */ | ||
| opTimeoutMs?: number; | ||
| /** Maximum queue size */ | ||
| maxQueueSize?: number; | ||
| /** Heartbeat timeout (ms) */ | ||
| heartbeatTimeoutMs?: number; | ||
| } | ||
| interface QueuedTransaction { | ||
| txnId: string; | ||
| originTabId: TabId; | ||
| mode: "deferred" | "immediate" | "exclusive"; | ||
| resolve: () => void; | ||
| reject: (error: Error) => void; | ||
| enqueuedAt: number; | ||
| } | ||
| export class TransactionSessionManager { | ||
| private activeTxn: ActiveTransaction | null = null; | ||
| private txnQueue: QueuedTransaction[] = []; | ||
| private watchdogTimer?: ReturnType<typeof setInterval>; | ||
| private heartbeatTimers = new Map<string, ReturnType<typeof setTimeout>>(); | ||
| private txnLockRelease?: () => void; | ||
| // Pending state: true when a transaction is being acquired (between queue wait and BEGIN completion) | ||
| // Non-transaction requests must wait when this is true OR when activeTxn is set | ||
| private txnPending = false; | ||
| // Queue for non-transaction requests waiting for transaction to complete | ||
| private rpcQueue: Array<{ resolve: () => void; reject: (err: Error) => void }> = []; | ||
| private readonly txnTimeoutMs: number; | ||
| private readonly opTimeoutMs: number; | ||
| private readonly maxQueueSize: number; | ||
| private readonly heartbeatTimeoutMs: number; | ||
| constructor( | ||
| private readonly dbHost: DbWorkerHost, | ||
| private readonly hostElection: HostElection, | ||
| options?: TransactionSessionManagerOptions | ||
| ) { | ||
| this.txnTimeoutMs = options?.txnTimeoutMs ?? 30000; | ||
| this.opTimeoutMs = options?.opTimeoutMs ?? 10000; | ||
| this.maxQueueSize = options?.maxQueueSize ?? 10; | ||
| this.heartbeatTimeoutMs = options?.heartbeatTimeoutMs ?? 15000; | ||
| } | ||
| /** | ||
| * Start the watchdog timer for transaction timeout detection | ||
| */ | ||
| startWatchdog(): void { | ||
| if (this.watchdogTimer) return; | ||
| this.watchdogTimer = setInterval(() => { | ||
| if (this.activeTxn) { | ||
| const elapsed = Date.now() - this.activeTxn.lastActivityAt; | ||
| if (elapsed > this.txnTimeoutMs) { | ||
| logger.warn(`Transaction ${this.activeTxn.txnId} timed out after ${elapsed}ms, rolling back`); | ||
| this.forceRollback(); | ||
| } | ||
| } | ||
| // Clean up expired queue entries | ||
| const now = Date.now(); | ||
| const expired = this.txnQueue.filter((t) => now - t.enqueuedAt > this.txnTimeoutMs); | ||
| for (const t of expired) { | ||
| const idx = this.txnQueue.indexOf(t); | ||
| if (idx !== -1) { | ||
| this.txnQueue.splice(idx, 1); | ||
| t.reject(new Error("Transaction queue wait timeout")); | ||
| } | ||
| } | ||
| }, 5000); | ||
| } | ||
| /** | ||
| * Stop the watchdog timer | ||
| */ | ||
| stopWatchdog(): void { | ||
| if (this.watchdogTimer) { | ||
| clearInterval(this.watchdogTimer); | ||
| this.watchdogTimer = undefined; | ||
| } | ||
| } | ||
| /** | ||
| * Get the active transaction | ||
| */ | ||
| getActiveTransaction(): ActiveTransaction | null { | ||
| return this.activeTxn; | ||
| } | ||
| /** | ||
| * Check if there's an active transaction | ||
| */ | ||
| hasActiveTransaction(): boolean { | ||
| return this.activeTxn !== null; | ||
| } | ||
| /** | ||
| * Check if a transaction is active or pending (being acquired) | ||
| * Non-transaction requests should wait when this returns true | ||
| */ | ||
| isTransactionBlocking(): boolean { | ||
| return this.activeTxn !== null || this.txnPending; | ||
| } | ||
| /** | ||
| * Wait for any active/pending transaction to complete before executing | ||
| * Returns a promise that resolves when it's safe to execute non-transaction SQL | ||
| */ | ||
| async waitForTransactionSlot(): Promise<void> { | ||
| if (!this.isTransactionBlocking()) { | ||
| return; | ||
| } | ||
| // Queue and wait for transaction to complete | ||
| return new Promise<void>((resolve, reject) => { | ||
| this.rpcQueue.push({ resolve, reject }); | ||
| }); | ||
| } | ||
| /** | ||
| * Begin a new transaction session | ||
| */ | ||
| async beginTransaction( | ||
| txnId: string, | ||
| originTabId: TabId, | ||
| mode: "deferred" | "immediate" | "exclusive" = "immediate" | ||
| ): Promise<void> { | ||
| // If there's already an active or pending transaction, queue this one | ||
| if (this.activeTxn || this.txnPending) { | ||
| if (this.txnQueue.length >= this.maxQueueSize) { | ||
| throw new Error("Transaction queue full"); | ||
| } | ||
| await new Promise<void>((resolve, reject) => { | ||
| this.txnQueue.push({ | ||
| txnId, | ||
| originTabId, | ||
| mode, | ||
| resolve, | ||
| reject, | ||
| enqueuedAt: Date.now(), | ||
| }); | ||
| }); | ||
| } | ||
| // Mark as pending BEFORE acquiring lock - this blocks new RPC requests | ||
| this.txnPending = true; | ||
| try { | ||
| // Acquire transaction lock to prevent Host release | ||
| this.txnLockRelease = await this.hostElection.acquireTransactionLock(); | ||
| // Execute BEGIN | ||
| const beginSql = | ||
| mode === "deferred" ? "BEGIN" : mode === "exclusive" ? "BEGIN EXCLUSIVE" : "BEGIN IMMEDIATE"; | ||
| await this.dbHost.execRaw(beginSql); | ||
| this.activeTxn = { | ||
| txnId, | ||
| originTabId, | ||
| startedAt: Date.now(), | ||
| lastActivityAt: Date.now(), | ||
| mode, | ||
| }; | ||
| // Start heartbeat timer for this transaction | ||
| this.resetHeartbeatTimer(txnId); | ||
| } catch (e) { | ||
| // Release lock on error | ||
| this.txnLockRelease?.(); | ||
| this.txnLockRelease = undefined; | ||
| // Clear pending state | ||
| this.txnPending = false; | ||
| // Process next queued transaction (don't leave queue stuck) | ||
| this.processQueue(); | ||
| // Also release any waiting RPC requests | ||
| this.processRpcQueue(); | ||
| throw e; | ||
| } finally { | ||
| // Clear pending state (activeTxn is now set if successful) | ||
| this.txnPending = false; | ||
| } | ||
| } | ||
| /** | ||
| * Execute SQL within a transaction | ||
| */ | ||
| async execInTransaction<T>( | ||
| txnId: string, | ||
| executor: () => Promise<T> | ||
| ): Promise<T> { | ||
| this.validateActiveTxn(txnId); | ||
| this.activeTxn!.lastActivityAt = Date.now(); | ||
| this.resetHeartbeatTimer(txnId); | ||
| return await executor(); | ||
| } | ||
| /** | ||
| * Record a heartbeat for a transaction | ||
| */ | ||
| recordHeartbeat(txnId: string): void { | ||
| if (this.activeTxn?.txnId === txnId) { | ||
| this.activeTxn.lastActivityAt = Date.now(); | ||
| this.resetHeartbeatTimer(txnId); | ||
| } | ||
| } | ||
| /** | ||
| * Commit a transaction | ||
| */ | ||
| async commitTransaction(txnId: string): Promise<void> { | ||
| this.validateActiveTxn(txnId); | ||
| try { | ||
| await this.dbHost.execRaw("COMMIT"); | ||
| } finally { | ||
| this.endTransaction(); | ||
| } | ||
| } | ||
| /** | ||
| * Rollback a transaction | ||
| */ | ||
| async rollbackTransaction(txnId: string): Promise<void> { | ||
| this.validateActiveTxn(txnId); | ||
| try { | ||
| await this.dbHost.execRaw("ROLLBACK"); | ||
| } finally { | ||
| this.endTransaction(); | ||
| } | ||
| } | ||
| /** | ||
| * Force rollback the current transaction (for timeout/error cases) | ||
| */ | ||
| private forceRollback(): void { | ||
| if (!this.activeTxn) return; | ||
| try { | ||
| // Use sync exec if possible, otherwise queue | ||
| this.dbHost.execRaw("ROLLBACK").catch((e) => { | ||
| logger.error("Force rollback failed:", e); | ||
| }); | ||
| } catch (e) { | ||
| logger.error("Force rollback failed:", e); | ||
| } | ||
| this.endTransaction(); | ||
| } | ||
| /** | ||
| * End the current transaction and process queue | ||
| */ | ||
| private endTransaction(): void { | ||
| if (this.activeTxn) { | ||
| // Clear heartbeat timer | ||
| const timer = this.heartbeatTimers.get(this.activeTxn.txnId); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| this.heartbeatTimers.delete(this.activeTxn.txnId); | ||
| } | ||
| this.activeTxn = null; | ||
| } | ||
| // Release transaction lock | ||
| if (this.txnLockRelease) { | ||
| this.txnLockRelease(); | ||
| this.txnLockRelease = undefined; | ||
| } | ||
| // Process next queued transaction first (transactions have priority) | ||
| this.processQueue(); | ||
| // Then release waiting RPC requests (if no new transaction started) | ||
| this.processRpcQueue(); | ||
| } | ||
| /** | ||
| * Process the next queued transaction | ||
| */ | ||
| private processQueue(): void { | ||
| if (this.txnQueue.length > 0 && !this.activeTxn && !this.txnPending) { | ||
| const next = this.txnQueue.shift()!; | ||
| next.resolve(); | ||
| } | ||
| } | ||
| /** | ||
| * Process queued RPC requests (release them when no transaction is active) | ||
| */ | ||
| private processRpcQueue(): void { | ||
| if (this.activeTxn || this.txnPending) { | ||
| // Still blocked, don't release | ||
| return; | ||
| } | ||
| // Release all waiting RPC requests | ||
| const queue = this.rpcQueue; | ||
| this.rpcQueue = []; | ||
| for (const { resolve } of queue) { | ||
| resolve(); | ||
| } | ||
| } | ||
| /** | ||
| * Reset the heartbeat timer for a transaction | ||
| */ | ||
| private resetHeartbeatTimer(txnId: string): void { | ||
| // Clear existing timer | ||
| const existingTimer = this.heartbeatTimers.get(txnId); | ||
| if (existingTimer) { | ||
| clearTimeout(existingTimer); | ||
| } | ||
| // Set new timer | ||
| const timer = setTimeout(() => { | ||
| if (this.activeTxn?.txnId === txnId) { | ||
| logger.warn(`No heartbeat for transaction ${txnId} in ${this.heartbeatTimeoutMs}ms, rolling back`); | ||
| this.forceRollback(); | ||
| } | ||
| }, this.heartbeatTimeoutMs); | ||
| this.heartbeatTimers.set(txnId, timer); | ||
| } | ||
| /** | ||
| * Validate that the given txnId matches the active transaction | ||
| */ | ||
| private validateActiveTxn(txnId: string): void { | ||
| if (!this.activeTxn) { | ||
| throw new Error(`No active transaction (expected ${txnId})`); | ||
| } | ||
| if (this.activeTxn.txnId !== txnId) { | ||
| throw new Error(`Transaction ID mismatch: expected ${this.activeTxn.txnId}, got ${txnId}`); | ||
| } | ||
| // Check timeout | ||
| const elapsed = Date.now() - this.activeTxn.lastActivityAt; | ||
| if (elapsed > this.txnTimeoutMs) { | ||
| this.forceRollback(); | ||
| throw new Error(`Transaction ${txnId} timed out after ${elapsed}ms`); | ||
| } | ||
| } | ||
| /** | ||
| * Cleanup and release resources | ||
| */ | ||
| async close(): Promise<void> { | ||
| this.stopWatchdog(); | ||
| // Force rollback any active transaction | ||
| if (this.activeTxn) { | ||
| this.forceRollback(); | ||
| } | ||
| // Reject all queued transactions | ||
| for (const t of this.txnQueue) { | ||
| t.reject(new Error("Transaction session manager closed")); | ||
| } | ||
| this.txnQueue = []; | ||
| // Reject all queued RPC requests | ||
| for (const r of this.rpcQueue) { | ||
| r.reject(new Error("Transaction session manager closed")); | ||
| } | ||
| this.rpcQueue = []; | ||
| // Clear all heartbeat timers | ||
| for (const timer of this.heartbeatTimers.values()) { | ||
| clearTimeout(timer); | ||
| } | ||
| this.heartbeatTimers.clear(); | ||
| } | ||
| } |
| /** | ||
| * Visibility Manager | ||
| * | ||
| * Manages document visibility state and provides callbacks for visibility changes. | ||
| * Used to implement visibility-aware Host election where only visible tabs | ||
| * participate in Host competition. | ||
| */ | ||
| export type VisibilityChangeListener = (visible: boolean) => void; | ||
| export class VisibilityManager { | ||
| private listeners = new Set<VisibilityChangeListener>(); | ||
| private boundHandler: () => void; | ||
| constructor() { | ||
| this.boundHandler = this.handleVisibilityChange.bind(this); | ||
| if (typeof document !== "undefined") { | ||
| document.addEventListener("visibilitychange", this.boundHandler); | ||
| } | ||
| } | ||
| /** | ||
| * Handle document visibility change event | ||
| */ | ||
| private handleVisibilityChange(): void { | ||
| const visible = this.isVisible(); | ||
| this.listeners.forEach((fn) => { | ||
| try { | ||
| fn(visible); | ||
| } catch (e) { | ||
| console.error("Error in visibility change listener:", e); | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Check if the document is currently visible | ||
| */ | ||
| isVisible(): boolean { | ||
| if (typeof document === "undefined") { | ||
| // Not in a browser environment, assume visible | ||
| return true; | ||
| } | ||
| return document.visibilityState === "visible"; | ||
| } | ||
| /** | ||
| * Register a listener for visibility changes | ||
| * @param fn Callback function that receives visibility state | ||
| * @returns Unsubscribe function | ||
| */ | ||
| onVisibilityChange(fn: VisibilityChangeListener): () => void { | ||
| this.listeners.add(fn); | ||
| return () => { | ||
| this.listeners.delete(fn); | ||
| }; | ||
| } | ||
| /** | ||
| * Cleanup and remove all listeners | ||
| */ | ||
| destroy(): void { | ||
| if (typeof document !== "undefined") { | ||
| document.removeEventListener("visibilitychange", this.boundHandler); | ||
| } | ||
| this.listeners.clear(); | ||
| } | ||
| } |
+1
-1
@@ -1,2 +0,2 @@ | ||
| const e=require(`./browser3.cjs`);async function t(t){return new e.t(t)}exports.openStore=t; | ||
| const e=require(`./browser2.cjs`);async function t(t){return await e.t.create(t)}exports.openStore=t; | ||
| //# sourceMappingURL=browser.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"browser.cjs","names":["BrowserAdapter"],"sources":["../src/browser.ts"],"sourcesContent":["export type {\n UniStoreConnection,\n UniStoreOptions,\n ChangeEvent,\n SQLiteValue,\n SQLiteParams,\n QueryResult,\n RunResult,\n SQLiteWasmConfig,\n} from \"./types.js\";\n\nexport type { BrowserSQLiteDatabase, BrowserSQLiteStatement } from \"./platform-types.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\nimport { BrowserAdapter } from \"./adapters/browser.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n return new BrowserAdapter(options);\n}\n"],"mappings":"kCAgBA,eAAsB,EAAU,EAAuD,CACrF,OAAO,IAAIA,EAAAA,EAAe,EAAQ"} | ||
| {"version":3,"file":"browser.cjs","names":["BrowserAdapter"],"sources":["../src/browser.ts"],"sourcesContent":["export type {\n UniStoreConnection,\n UniStoreOptions,\n ChangeEvent,\n SQLiteValue,\n SQLiteParams,\n QueryResult,\n RunResult,\n SQLiteWasmConfig,\n} from \"./types.js\";\n\nexport type { BrowserSQLiteDatabase, BrowserSQLiteStatement } from \"./platform-types.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\nimport { BrowserAdapter } from \"./adapters/browser/index.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n return await BrowserAdapter.create(options);\n}\n"],"mappings":"kCAgBA,eAAsB,EAAU,EAAuD,CACrF,OAAO,MAAMA,EAAAA,EAAe,OAAO,EAAQ"} |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import{t as e}from"./browser3.mjs";async function t(t){return new e(t)}export{t as openStore}; | ||
| import{t as e}from"./browser2.mjs";async function t(t){return await e.create(t)}export{t as openStore}; | ||
| //# sourceMappingURL=browser.mjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["export type {\n UniStoreConnection,\n UniStoreOptions,\n ChangeEvent,\n SQLiteValue,\n SQLiteParams,\n QueryResult,\n RunResult,\n SQLiteWasmConfig,\n} from \"./types.js\";\n\nexport type { BrowserSQLiteDatabase, BrowserSQLiteStatement } from \"./platform-types.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\nimport { BrowserAdapter } from \"./adapters/browser.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n return new BrowserAdapter(options);\n}\n"],"mappings":"mCAgBA,eAAsB,EAAU,EAAuD,CACrF,OAAO,IAAI,EAAe,EAAQ"} | ||
| {"version":3,"file":"browser.mjs","names":[],"sources":["../src/browser.ts"],"sourcesContent":["export type {\n UniStoreConnection,\n UniStoreOptions,\n ChangeEvent,\n SQLiteValue,\n SQLiteParams,\n QueryResult,\n RunResult,\n SQLiteWasmConfig,\n} from \"./types.js\";\n\nexport type { BrowserSQLiteDatabase, BrowserSQLiteStatement } from \"./platform-types.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\nimport { BrowserAdapter } from \"./adapters/browser/index.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n return await BrowserAdapter.create(options);\n}\n"],"mappings":"mCAgBA,eAAsB,EAAU,EAAuD,CACrF,OAAO,MAAM,EAAe,OAAO,EAAQ"} |
+17
-1
@@ -1,1 +0,17 @@ | ||
| const e=require(`./browser3.cjs`);exports.BrowserAdapter=e.t; | ||
| const e=require(`./chunk.cjs`),t=require(`./base.cjs`);let n=require(`debug`);n=e.t(n);const r=`unisqlite`;function i(e){let t=(0,n.default)(`${r}:${e}`),i=(0,n.default)(`${r}:${e}:warn`),a=(0,n.default)(`${r}:${e}:error`);return a.enabled=!0,{log:t,warn:i,error:a}}const a=i(`host`),o=i(`txn`),s=i(`adapter`);var c=class e{constructor(e){this.options=e,this.abortController=null,this._isHost=!1,this._isStarted=!1,this.releaseResolver=null,this.hostReadyResolver=null,this.hostReadyPromise=null,this.lockName=`sqlite:host:${e.dbName}`,this.txnLockName=`sqlite:txn:${e.dbName}`}static isSupported(){return typeof navigator<`u`&&navigator.locks!==void 0}get isHost(){return this._isHost}async start(){if(!this._isStarted){if(!e.isSupported()){a.warn(`Web Locks API not supported. Running in local-only mode.`),this._isStarted=!0,this._isHost=!0,this.options.onBecomeHost().catch(e=>{a.error(`Error in onBecomeHost callback:`,e),this._isHost=!1});return}this.options.checkVisibility&&!this.options.checkVisibility()||(this._isStarted=!0,this.hostReadyPromise=new Promise(e=>{this.hostReadyResolver=e}),this.tryAcquireHost().catch(e=>{a.error(`Error in host election:`,e)}))}}async waitForHost(){if(!this._isHost&&(this.hostReadyPromise||(await this.start(),this.hostReadyPromise)))return this.hostReadyPromise}async tryAcquireHost(){this.abortController=new AbortController;try{await navigator.locks.request(this.lockName,{mode:`exclusive`,signal:this.abortController.signal},async e=>{if(e){this._isHost=!0,a.log(`Tab ${this.options.tabId} became Host for ${this.options.dbName}`);try{await this.options.onBecomeHost()}catch(e){throw a.error(`Error in onBecomeHost callback:`,e),this._isHost=!1,e}this.hostReadyResolver&&(this.hostReadyResolver(),this.hostReadyResolver=null,this.hostReadyPromise=null),await new Promise(e=>{this.releaseResolver=e,this.abortController.signal.addEventListener(`abort`,()=>{e()})}),this._isHost=!1,this.releaseResolver=null,a.log(`Tab ${this.options.tabId} lost Host role for ${this.options.dbName}`);try{this.options.onLoseHost()}catch(e){a.error(`Error in onLoseHost callback:`,e)}}})}catch(e){if(e.name===`AbortError`)return;throw a.error(`Error acquiring Host lock:`,e),e}finally{this._isStarted=!1,this.abortController=null}}releaseHost(){this.releaseResolver&&=(this.releaseResolver(),null),this.abortController&&=(this.abortController.abort(),null)}async acquireTransactionLock(){if(!e.isSupported())return()=>{};let t=new AbortController,n=null,r=null,i=new Promise(e=>{r=e});return navigator.locks.request(this.txnLockName,{mode:`exclusive`,signal:t.signal},async e=>{e&&(r?.(),await new Promise(e=>{n=e}))}).catch(e=>{e.name!==`AbortError`&&a.error(`Error acquiring transaction lock:`,e),r?.()}),await i,()=>{n?n():t.abort()}}async hasTransactionLock(){if(!e.isSupported())return!1;try{return(await navigator.locks.query()).held?.some(e=>e.name===this.txnLockName)??!1}catch{return!1}}async close(){this.releaseHost()}},l=class{constructor(){this.listeners=new Set,this.boundHandler=this.handleVisibilityChange.bind(this),typeof document<`u`&&document.addEventListener(`visibilitychange`,this.boundHandler)}handleVisibilityChange(){let e=this.isVisible();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error(`Error in visibility change listener:`,e)}})}isVisible(){return typeof document>`u`?!0:document.visibilityState===`visible`}onVisibilityChange(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}destroy(){typeof document<`u`&&document.removeEventListener(`visibilitychange`,this.boundHandler),this.listeners.clear()}};function u(e){return e instanceof Error?{name:e.name,message:e.message,stack:e.stack}:{name:`Error`,message:String(e)}}function d(e){let t=Error(e.message);return t.name=e.name,e.stack&&(t.stack=e.stack),t}function f(e){return e.t===`req`||e.t===`txn_begin`||e.t===`txn_exec`||e.t===`txn_commit`||e.t===`txn_rollback`||e.t===`txn_heartbeat`}function p(e){return e.t===`res`||e.t===`txn_ack`||e.t===`txn_result`||e.t===`txn_done`||e.t===`txn_error`}function m(){return crypto.randomUUID()}function h(){return crypto.randomUUID()}function g(){return crypto.randomUUID()}var _=class{constructor(e,t,n=3e4){this.dbName=e,this.tabId=t,this.defaultTimeoutMs=n,this.pending=new Map,this.closed=!1,this.channel=new BroadcastChannel(`sqlite:rpc:${e}`),this.channel.onmessage=this.handleMessage.bind(this)}handleMessage(e){if(this.closed)return;let t=e.data;!t||typeof t.t!=`string`||(p(t)?this.handleResponse(t):f(t)&&this.requestHandler&&`from`in t&&t.from!==this.tabId&&this.handleRequest(t))}handleResponse(e){if(`to`in e&&e.to!==this.tabId||!(`id`in e))return;let t=this.pending.get(e.id);if(t)switch(clearTimeout(t.timer),this.pending.delete(e.id),e.t){case`res`:e.ok?t.resolve(e.result):t.reject(d(e.error));break;case`txn_ack`:case`txn_done`:t.resolve(void 0);break;case`txn_result`:e.ok?t.resolve(e.result):t.reject(d(e.error));break;case`txn_error`:t.reject(d(e.error));break}}async handleRequest(e){if(this.requestHandler)try{let t=await this.requestHandler(e);this.sendResponse(e,t)}catch(t){this.sendErrorResponse(e,t)}}sendResponse(e,t){if(this.closed)return;let n=`from`in e?e.from:void 0,r=`id`in e?e.id:void 0;if(!(!n||!r))switch(e.t){case`req`:this.channel.postMessage({t:`res`,to:n,id:r,ok:!0,result:t});break;case`txn_begin`:this.channel.postMessage({t:`txn_ack`,to:n,id:r,txnId:e.txnId});break;case`txn_exec`:this.channel.postMessage({t:`txn_result`,to:n,id:r,txnId:e.txnId,ok:!0,result:t});break;case`txn_commit`:case`txn_rollback`:this.channel.postMessage({t:`txn_done`,to:n,id:r,txnId:e.txnId});break;case`txn_heartbeat`:break}}sendErrorResponse(e,t){if(this.closed)return;let n=`from`in e?e.from:void 0,r=`id`in e?e.id:void 0;if(!n||!r)return;let i=u(t);switch(e.t){case`req`:this.channel.postMessage({t:`res`,to:n,id:r,ok:!1,error:i});break;case`txn_begin`:case`txn_exec`:case`txn_commit`:case`txn_rollback`:this.channel.postMessage({t:`txn_error`,to:n,id:r,txnId:e.txnId,error:i});break}}setRequestHandler(e){this.requestHandler=e}async request(e,t,n){let r=m(),i=n??this.defaultTimeoutMs;return new Promise((n,a)=>{let o=setTimeout(()=>{this.pending.delete(r),a(Error(`RPC timeout after ${i}ms for ${e}`))},i);this.pending.set(r,{resolve:n,reject:a,timer:o}),this.channel.postMessage({t:`req`,from:this.tabId,id:r,op:e,payload:t})})}async txnBegin(e,t=`immediate`,n){let r=m(),i=n??this.defaultTimeoutMs;return new Promise((n,a)=>{let o=setTimeout(()=>{this.pending.delete(r),a(Error(`Transaction begin timeout after ${i}ms`))},i);this.pending.set(r,{resolve:n,reject:a,timer:o}),this.channel.postMessage({t:`txn_begin`,from:this.tabId,id:r,txnId:e,mode:t})})}async txnExec(e,t,n,r){let i=m(),a=r??1e4;return new Promise((r,o)=>{let s=setTimeout(()=>{this.pending.delete(i),o(Error(`Transaction SQL timeout after ${a}ms`))},a);this.pending.set(i,{resolve:r,reject:o,timer:s}),this.channel.postMessage({t:`txn_exec`,from:this.tabId,id:i,txnId:e,op:t,payload:n})})}async txnCommit(e,t){let n=m(),r=t??1e4;return new Promise((t,i)=>{let a=setTimeout(()=>{this.pending.delete(n),i(Error(`Transaction commit timeout after ${r}ms`))},r);this.pending.set(n,{resolve:t,reject:i,timer:a}),this.channel.postMessage({t:`txn_commit`,from:this.tabId,id:n,txnId:e})})}async txnRollback(e,t){let n=m(),r=t??1e4;return new Promise((t,i)=>{let a=setTimeout(()=>{this.pending.delete(n),i(Error(`Transaction rollback timeout after ${r}ms`))},r);this.pending.set(n,{resolve:t,reject:i,timer:a}),this.channel.postMessage({t:`txn_rollback`,from:this.tabId,id:n,txnId:e})})}txnHeartbeat(e){this.closed||this.channel.postMessage({t:`txn_heartbeat`,from:this.tabId,txnId:e})}close(){this.closed=!0,this.channel.close();for(let e of this.pending.values())clearTimeout(e.timer),e.reject(Error(`RPC channel closed`));this.pending.clear()}};const v=new Map,y=new Map;var b=class e{constructor(e,t){this.dbName=e,this.config=t,this.workerQueue=Promise.resolve(),this.activeStorageBackend=`memory`,this.closed=!1}static async create(t){let n=new e(t.dbName,t.config);return await n.initialize(),n}async initialize(){console.log(`Loading SQLite WASM using strategy: ${this.config.loadStrategy}`);let e=await this.loadSQLiteWasm(),t={print:console.log,printErr:console.error};if(this.config.locateFile)t.locateFile=this.config.locateFile;else if(typeof window<`u`){let e=window;e.sqlite3InitModuleState?.locateFile&&(t.locateFile=e.sqlite3InitModuleState.locateFile)}if(console.log(`Initializing SQLite WASM module...`),this.sqlite3=await e(t),!this.sqlite3)throw Error(`Failed to initialize SQLite WASM module`);let n=this.sqlite3;console.log(`SQLite WASM module initialized successfully`);let r=this.config.storageBackend??`auto`,i=this.dbName===`:memory:`?`:memory:`:`/unisqlite/${this.dbName}`;r===`memory`||this.dbName===`:memory:`?(console.log(`Using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`):await this.tryCreatePersistentDatabase(n,i,r)||(console.log(`All persistent storage backends failed, using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`);try{await this.execInternal(`PRAGMA journal_mode=WAL`),console.log(`Enabled WAL mode`)}catch(e){console.warn(`Could not enable WAL mode:`,e)}}async query(e,t){return(await this.executeSql(e,t)).rows}async queryRaw(e,t){return await this.executeSql(e,t)}async run(e,t){let n=await this.executeSql(e,t);return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){await this.execInternal(e)}async execRaw(e){await this.execInternal(e,{bypassQueue:!0})}async executeSqlRaw(e,t){return await this.executeSql(e,t,{bypassQueue:!0})}getActiveStorageBackend(){return this.activeStorageBackend}getActiveOpfsVfs(){return this.activeOpfsVfs}isWorkerDbActive(){return!!this.workerPromiser&&this.workerDbId!==void 0}async close(){if(!this.closed){if(this.closed=!0,this.workerPromiser){let e=this.workerPromiser,t=this.workerDbId;try{t!==void 0&&await this.enqueueWorker(async()=>{await e(`close`,{dbId:t})})}catch(e){console.warn(`Failed to close SQLite worker database:`,e)}finally{this.workerDbId=void 0;let t=e.worker;t?.terminate&&t.terminate(),this.workerPromiser=void 0}}this.db&&=(this.db.close(),void 0)}}enqueueWorker(e){let t=async()=>e(),n=this.workerQueue.then(t,t);return this.workerQueue=n.then(()=>void 0,()=>void 0),n}normalizeParams(e){if(!e)return;if(Array.isArray(e))return e;let t=Object.keys(e),n=e;return t.map(e=>n[e])}async execInternal(e,t){if(this.isWorkerDbActive()){let n=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);await this.workerPromiser(`exec`,{dbId:this.workerDbId,sql:e})};t?.bypassQueue?await n():await this.enqueueWorker(n);return}if(!this.db)throw Error(`Database not initialized`);this.db.exec(e)}async executeSql(e,t,n){try{if(this.isWorkerDbActive()){let r=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);let n=this.normalizeParams(t),r={dbId:this.workerDbId,sql:e,rowMode:`object`,resultRows:[],columnNames:[],countChanges:!0};n&&(r.bind=n);let i=await this.workerPromiser(`exec`,r),a=i.result??i,o=Array.isArray(a.resultRows)?a.resultRows:[],s=Array.isArray(a.columnNames)?a.columnNames:[],c=typeof a.changeCount==`number`?a.changeCount:0,l={dbId:this.workerDbId,sql:`SELECT last_insert_rowid() AS lastInsertRowId`,rowMode:`object`,resultRows:[],columnNames:[]},u=await this.workerPromiser(`exec`,l),d=u.result??u,f=Array.isArray(d.resultRows)?d.resultRows:[];return{rows:o,columns:s,rowsAffected:c,lastInsertRowId:Number(f[0]?.lastInsertRowId??0)}};return n?.bypassQueue?await r():await this.enqueueWorker(r)}if(!this.db||!this.sqlite3)throw Error(`Database not initialized`);let r=this.normalizeParams(t),i=[],a=[],o=this.db.prepare(e);try{let e=o;r&&o.bind(r);let t=typeof e.getColumnCount==`function`?e.getColumnCount():typeof e.columnCount==`number`?e.columnCount:0;if(t>0)if(typeof e.getColumnNames==`function`)a=e.getColumnNames();else for(let e=0;e<t;e++)a.push(o.getColumnName(e));for(;o.step();){let e={};for(let n=0;n<t;n++)e[a[n]]=o.get(n);i.push(e)}return{rows:i,columns:a,rowsAffected:this.db.changes(),lastInsertRowId:Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)||0)}}finally{o.finalize()}}catch(e){let t=e instanceof Error?e.message:String(e),n=Error(`SQLite error: ${t}`);throw e instanceof Error&&(n.cause=e),n}}async loadSQLiteWasm(){let e=`${this.config.loadStrategy}-${this.config.wasmUrl||this.config.cdnBaseUrl}-${this.config.version}`;if(v.has(e))return v.get(e);let t=this.loadSQLiteWasmInternal();return v.set(e,t),t}async loadSQLiteWasmInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.config;switch(e){case`npm`:return await this.loadFromNpm();case`cdn`:return await this.loadFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadFromUrl(t);case`module`:return await this.loadAsModule();case`global`:default:return this.loadFromGlobal()}}async loadFromNpm(){try{let e=await import(`@sqlite.org/sqlite-wasm`);return e.default||e}catch(e){return console.warn(`Failed to load SQLite WASM from npm, falling back to CDN:`,e),this.loadFromCdn(this.config.cdnBaseUrl,this.config.version,this.config.bundlerFriendly)}}async loadFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/${r}`,`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM from: ${e}`);let t=await import(e);return console.log(`Successfully loaded SQLite WASM from: ${e}`),t.default||t}catch(t){a=t,console.warn(`Failed to load SQLite WASM from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load SQLite WASM from any CDN source. Last error: ${o}`)}async loadFromUrl(e){let t=await import(e);return t.default||t.sqlite3InitModule}async loadAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=await import(e);return t.default||t.sqlite3InitModule}catch(t){console.warn(`Failed to load SQLite WASM from ${e}:`,t)}throw Error(`Could not load SQLite WASM as ES6 module from any known path`)}loadFromGlobal(){if(typeof window<`u`){let e=window;if(e.sqlite3InitModule)return e.sqlite3InitModule}throw Error(`SQLite WASM module not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM script in your HTML before using UniSqlite`)}async loadSQLiteWorkerPromiserFactory(){let e=`worker-${this.config.loadStrategy}-${this.config.wasmUrl||this.config.cdnBaseUrl}-${this.config.version}`;if(y.has(e))return y.get(e);let t=this.loadSQLiteWorkerPromiserFactoryInternal();return y.set(e,t),t}async loadSQLiteWorkerPromiserFactoryInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.config;switch(e){case`npm`:return await this.loadWorkerPromiserFromNpm();case`cdn`:return await this.loadWorkerPromiserFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadWorkerPromiserFromUrl(t);case`module`:return await this.loadWorkerPromiserAsModule();case`global`:default:return this.loadWorkerPromiserFromGlobal()}}async loadWorkerPromiserFromNpm(){try{let e=(await import(`@sqlite.org/sqlite-wasm`)).sqlite3Worker1Promiser;if(typeof e==`function`)return e;throw Error(`sqlite3Worker1Promiser export not found`)}catch(e){return console.warn(`Failed to load SQLite WASM worker promiser from npm, falling back to CDN:`,e),this.loadWorkerPromiserFromCdn(this.config.cdnBaseUrl,this.config.version,this.config.bundlerFriendly)}}async loadWorkerPromiserFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`${e}@${t}/${r}`,`${e}@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM worker promiser from: ${e}`);let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return console.log(`Successfully loaded SQLite WASM worker promiser from: ${e}`),t;throw Error(`sqlite3Worker1Promiser export not found`)}catch(t){a=t,console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${o}`)}async loadWorkerPromiserFromUrl(e){let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t!=`function`)throw Error(`sqlite3Worker1Promiser export not found in module loaded from ${e}`);return t}async loadWorkerPromiserAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return t}catch(t){console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}throw Error(`Could not load sqlite3Worker1Promiser as ES6 module from any known path`)}loadWorkerPromiserFromGlobal(){let e=globalThis;if(typeof e.sqlite3Worker1Promiser==`function`)return e.sqlite3Worker1Promiser;throw Error(`SQLite WASM worker promiser not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm' | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite`)}async tryCreatePersistentDatabase(e,t,n){if(n===`opfs`||n===`auto`){let r=this.config.opfsVfsType??`auto`,i=r===`auto`?e.oo1.OpfsDb?[`opfs`]:[`opfs-sahpool`,`opfs`]:r===`sahpool`?[`opfs-sahpool`]:[`opfs`];for(let n of i)if(await this.tryCreateOpfsDatabase(e,t,n))return!0;if(n===`opfs`)throw Error(this.getOpfsUnavailableError())}if(n===`localStorage`||n===`auto`){if(e.oo1.JsStorageDb)if(this.dbName===`local`||this.dbName===`session`)try{return console.log(`Creating localStorage-backed database:`,this.dbName),this.db=new e.oo1.JsStorageDb(this.dbName),this.activeStorageBackend=`localStorage`,console.log(`Successfully created localStorage-backed database`),!0}catch(e){if(console.warn(`localStorage database creation failed:`,e),n===`localStorage`)throw Error(`localStorage database creation failed: ${e instanceof Error?e.message:String(e)}`)}else if(n===`localStorage`)throw Error(`localStorage storage backend requires path to be 'local' or 'session', got '${this.dbName}'. Use storageBackend: 'opfs' for custom database names with persistence.`);else console.log(`Skipping localStorage: path '${this.dbName}' is not 'local' or 'session'`);else if(n===`localStorage`)throw Error(`JsStorageDb is not available in this environment.`)}return!1}getOpfsUnavailableError(){return`OPFS is not available. | ||
| SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood. | ||
| Make sure: | ||
| - You are in a secure context (HTTPS or localhost) | ||
| - For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available | ||
| - Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)`}async tryCreateOpfsDatabase(e,t,n){if(n===`opfs`){if(e.oo1.OpfsDb)try{return console.log(`Creating OPFS-backed database (OpfsDb):`,t),this.db=new e.oo1.OpfsDb(t),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs`,console.log(`Successfully created OPFS-backed database (OpfsDb)`),!0}catch(e){console.warn(`OPFS database creation via OpfsDb failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}if(await this.ensureSahpoolVfs(e))try{return console.log(`Creating SAHPool OPFS-backed database:`,t),this.db=new e.oo1.DB(t,`c`,`opfs-sahpool`),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs-sahpool`,console.log(`Successfully created SAHPool OPFS-backed database`),!0}catch(e){console.warn(`SAHPool OPFS database creation failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}async ensureSahpoolVfs(e){if(globalThis.importScripts===void 0)return!1;let t=`opfs-sahpool`;try{if(e.capi.sqlite3_vfs_find?.(t))return!0}catch(e){console.warn(`SAHPool VFS lookup failed:`,e)}if(typeof e.installOpfsSAHPoolVfs!=`function`)return!1;try{await e.installOpfsSAHPoolVfs({name:t,directory:`/unisqlite-sahpool`})}catch(e){return console.warn(`SAHPool VFS installation failed:`,e),!1}try{return!!e.capi.sqlite3_vfs_find?.(t)}catch{return!0}}createWorkerFromConfig(){if(!this.config.workerUrl||typeof Worker>`u`)return;let e=typeof this.config.workerUrl==`string`?this.config.workerUrl:this.config.workerUrl.href;try{return new Worker(e,{type:`module`})}catch(e){console.warn(`Failed to create Worker from custom workerUrl:`,e);return}}createWorkerFromCdn(e){if(typeof Worker>`u`||typeof Blob>`u`||typeof URL>`u`)return;let t=this.config.cdnBaseUrl,n=this.config.version;if(!t||!n)return;let r=`${`${t}@${n}/sqlite-wasm/jswasm`}/sqlite3-bundler-friendly.mjs`,i=e.installSahpool?[` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`,` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`,` }`].join(` | ||
| `):``,a=[`import sqlite3InitModule from ${JSON.stringify(r)};`,`sqlite3InitModule().then(async (sqlite3) => {`,` try {`,i||` // no-op`,` } catch (e) {`,` console.warn("SAHPool VFS installation failed:", e);`,` }`,` sqlite3.initWorker1API();`,`});`,``].join(` | ||
| `),o=new Blob([a],{type:`text/javascript`}),s=URL.createObjectURL(o),c=new Worker(s,{type:`module`});return c.addEventListener(`message`,()=>{URL.revokeObjectURL(s)},{once:!0}),c.addEventListener(`error`,()=>{URL.revokeObjectURL(s)},{once:!0}),c}async getOrCreateWorkerPromiser(e){if(this.workerPromiser)return this.workerPromiser;if(this.workerPromiserInitPromise)return await this.workerPromiserInitPromise;let t=await this.loadSQLiteWorkerPromiserFactory(),n=this.config.storageBackend===`opfs`||this.config.opfsVfsType===`opfs`||this.config.opfsVfsType===`sahpool`,r=this.config.loadStrategy===`global`&&!n?5e3:2e4;this.workerPromiserInitPromise=new Promise((n,i)=>{let a=!1,o=setTimeout(()=>{a||(a=!0,i(Error(`SQLite worker initialization timed out after ${r}ms`)))},r),s=(e,t)=>{a||(a=!0,clearTimeout(o),e(t))},c=e=>{s(i,e instanceof Error?e:Error(String(e)))},l;try{l=t({...e?.worker?{worker:e.worker}:{},onready:e=>{if(typeof e==`function`){s(n,e);return}if(typeof l==`function`){s(n,l);return}if(l&&typeof l.then==`function`){l.then(e=>s(n,e),e=>c(e));return}c(Error(`sqlite3Worker1Promiser did not provide a usable promiser`))},onerror:e=>{let t=e instanceof Error?e.message:String(e);c(Error(`SQLite worker error: ${t}`))}}),l&&typeof l.then==`function`&&l.then(e=>s(n,e),e=>c(e))}catch(e){c(e)}});let i=await this.workerPromiserInitPromise;return this.workerPromiser=i,i}async tryCreateWrappedWorkerOpfsDatabase(e,t){try{let n=!this.workerPromiser&&!this.workerPromiserInitPromise?(()=>{let e=this.createWorkerFromConfig()??(t===`opfs-sahpool`?this.createWorkerFromCdn({installSahpool:!0}):this.createWorkerFromCdn({installSahpool:!1}));return e?{worker:e}:void 0})():void 0,r=await(await this.getOrCreateWorkerPromiser(n))(`open`,{filename:`file:${e}`,vfs:t}),i=r.dbId??r.result?.dbId;if(i===void 0)throw Error(`SQLite worker did not return a dbId`);if(typeof i!=`number`&&typeof i!=`string`)throw Error(`SQLite worker returned an invalid dbId (type=${typeof i})`);return this.db=void 0,this.workerDbId=i,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=t,console.log(`Successfully created OPFS-backed database via wrapped worker (${t}):`,e),!0}catch(e){return console.warn(`Wrapped-worker OPFS database creation failed (vfs=${t}):`,e),!1}}},x=class{constructor(e,t,n){this.dbHost=e,this.hostElection=t,this.activeTxn=null,this.txnQueue=[],this.heartbeatTimers=new Map,this.txnPending=!1,this.rpcQueue=[],this.txnTimeoutMs=n?.txnTimeoutMs??3e4,this.opTimeoutMs=n?.opTimeoutMs??1e4,this.maxQueueSize=n?.maxQueueSize??10,this.heartbeatTimeoutMs=n?.heartbeatTimeoutMs??15e3}startWatchdog(){this.watchdogTimer||=setInterval(()=>{if(this.activeTxn){let e=Date.now()-this.activeTxn.lastActivityAt;e>this.txnTimeoutMs&&(o.warn(`Transaction ${this.activeTxn.txnId} timed out after ${e}ms, rolling back`),this.forceRollback())}let e=Date.now(),t=this.txnQueue.filter(t=>e-t.enqueuedAt>this.txnTimeoutMs);for(let e of t){let t=this.txnQueue.indexOf(e);t!==-1&&(this.txnQueue.splice(t,1),e.reject(Error(`Transaction queue wait timeout`)))}},5e3)}stopWatchdog(){this.watchdogTimer&&=(clearInterval(this.watchdogTimer),void 0)}getActiveTransaction(){return this.activeTxn}hasActiveTransaction(){return this.activeTxn!==null}isTransactionBlocking(){return this.activeTxn!==null||this.txnPending}async waitForTransactionSlot(){if(this.isTransactionBlocking())return new Promise((e,t)=>{this.rpcQueue.push({resolve:e,reject:t})})}async beginTransaction(e,t,n=`immediate`){if(this.activeTxn||this.txnPending){if(this.txnQueue.length>=this.maxQueueSize)throw Error(`Transaction queue full`);await new Promise((r,i)=>{this.txnQueue.push({txnId:e,originTabId:t,mode:n,resolve:r,reject:i,enqueuedAt:Date.now()})})}this.txnPending=!0;try{this.txnLockRelease=await this.hostElection.acquireTransactionLock();let r=n===`deferred`?`BEGIN`:n===`exclusive`?`BEGIN EXCLUSIVE`:`BEGIN IMMEDIATE`;await this.dbHost.execRaw(r),this.activeTxn={txnId:e,originTabId:t,startedAt:Date.now(),lastActivityAt:Date.now(),mode:n},this.resetHeartbeatTimer(e)}catch(e){throw this.txnLockRelease?.(),this.txnLockRelease=void 0,this.txnPending=!1,this.processQueue(),this.processRpcQueue(),e}finally{this.txnPending=!1}}async execInTransaction(e,t){return this.validateActiveTxn(e),this.activeTxn.lastActivityAt=Date.now(),this.resetHeartbeatTimer(e),await t()}recordHeartbeat(e){this.activeTxn?.txnId===e&&(this.activeTxn.lastActivityAt=Date.now(),this.resetHeartbeatTimer(e))}async commitTransaction(e){this.validateActiveTxn(e);try{await this.dbHost.execRaw(`COMMIT`)}finally{this.endTransaction()}}async rollbackTransaction(e){this.validateActiveTxn(e);try{await this.dbHost.execRaw(`ROLLBACK`)}finally{this.endTransaction()}}forceRollback(){if(this.activeTxn){try{this.dbHost.execRaw(`ROLLBACK`).catch(e=>{o.error(`Force rollback failed:`,e)})}catch(e){o.error(`Force rollback failed:`,e)}this.endTransaction()}}endTransaction(){if(this.activeTxn){let e=this.heartbeatTimers.get(this.activeTxn.txnId);e&&(clearTimeout(e),this.heartbeatTimers.delete(this.activeTxn.txnId)),this.activeTxn=null}this.txnLockRelease&&=(this.txnLockRelease(),void 0),this.processQueue(),this.processRpcQueue()}processQueue(){this.txnQueue.length>0&&!this.activeTxn&&!this.txnPending&&this.txnQueue.shift().resolve()}processRpcQueue(){if(this.activeTxn||this.txnPending)return;let e=this.rpcQueue;this.rpcQueue=[];for(let{resolve:t}of e)t()}resetHeartbeatTimer(e){let t=this.heartbeatTimers.get(e);t&&clearTimeout(t);let n=setTimeout(()=>{this.activeTxn?.txnId===e&&(o.warn(`No heartbeat for transaction ${e} in ${this.heartbeatTimeoutMs}ms, rolling back`),this.forceRollback())},this.heartbeatTimeoutMs);this.heartbeatTimers.set(e,n)}validateActiveTxn(e){if(!this.activeTxn)throw Error(`No active transaction (expected ${e})`);if(this.activeTxn.txnId!==e)throw Error(`Transaction ID mismatch: expected ${this.activeTxn.txnId}, got ${e}`);let t=Date.now()-this.activeTxn.lastActivityAt;if(t>this.txnTimeoutMs)throw this.forceRollback(),Error(`Transaction ${e} timed out after ${t}ms`)}async close(){this.stopWatchdog(),this.activeTxn&&this.forceRollback();for(let e of this.txnQueue)e.reject(Error(`Transaction session manager closed`));this.txnQueue=[];for(let e of this.rpcQueue)e.reject(Error(`Transaction session manager closed`));this.rpcQueue=[];for(let e of this.heartbeatTimers.values())clearTimeout(e);this.heartbeatTimers.clear()}},S=class{constructor(e,t,n=5e3){this.rpcChannel=e,this.txnId=t,this.heartbeatIntervalMs=n,this.committed=!1,this.rolledBack=!1,this.startHeartbeat()}startHeartbeat(){this.heartbeatInterval=setInterval(()=>{!this.committed&&!this.rolledBack&&this.rpcChannel.txnHeartbeat(this.txnId)},this.heartbeatIntervalMs)}stopHeartbeat(){this.heartbeatInterval&&=(clearInterval(this.heartbeatInterval),void 0)}ensureActive(){if(this.committed)throw Error(`Transaction already committed`);if(this.rolledBack)throw Error(`Transaction already rolled back`)}async query(e,t){return this.ensureActive(),(await this.rpcChannel.txnExec(this.txnId,`query`,{sql:e,params:t})).rows}async queryRaw(e,t){return this.ensureActive(),await this.rpcChannel.txnExec(this.txnId,`queryRaw`,{sql:e,params:t})}async run(e,t){this.ensureActive();let n=await this.rpcChannel.txnExec(this.txnId,`run`,{sql:e,params:t});return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){this.ensureActive(),await this.rpcChannel.txnExec(this.txnId,`exec`,{sql:e})}async transaction(e){this.ensureActive();let t=e(this);return t instanceof Promise?await t:t}async asyncTransaction(e,t){return this.ensureActive(),await e(this)}getConnectionType(){return`asyncTxn`}async close(){}async _commit(){this.ensureActive(),this.stopHeartbeat();try{await this.rpcChannel.txnCommit(this.txnId),this.committed=!0}catch(e){throw this.rolledBack=!0,e}}async _rollback(){if(!(this.committed||this.rolledBack)){this.stopHeartbeat();try{await this.rpcChannel.txnRollback(this.txnId)}finally{this.rolledBack=!0}}}isCommitted(){return this.committed}isRolledBack(){return this.rolledBack}},C=class{constructor(e,t){this.dbHost=e,this.isAsync=t}async query(e,t){return(await this.dbHost.executeSqlRaw(e,t)).rows}async queryRaw(e,t){return await this.dbHost.executeSqlRaw(e,t)}async run(e,t){let n=await this.dbHost.executeSqlRaw(e,t);return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){await this.dbHost.execRaw(e)}async transaction(e){let t=e(this);return t instanceof Promise?await t:t}async asyncTransaction(e){return await e(this)}getConnectionType(){return this.isAsync?`asyncTxn`:`syncTxn`}async close(){}},w=class e extends t.t{constructor(e){let t=e.path??e.name??`default`;super({...e,path:t}),this.dbHost=null,this.txnSessionManager=null,this.initialized=!1,this.closed=!1,this.options={...e,path:t},this.tabId=h(),this.sqliteConfig={loadStrategy:`global`,storageBackend:`auto`,opfsVfsType:`auto`,cdnBaseUrl:`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm`,version:`3.50.1-build1`,bundlerFriendly:!1,...e.sqlite},c.isSupported()||(s.warn(`Web Locks API not supported. Falling back to memory-only storage.`),this.sqliteConfig.storageBackend=`memory`),this.visibilityManager=new l,this.rpcChannel=new _(t,this.tabId),this.hostElection=new c({dbName:t,tabId:this.tabId,onBecomeHost:()=>this.becomeHost(),onLoseHost:()=>this.loseHost(),checkVisibility:()=>this.visibilityManager.isVisible()}),this.visibilityUnsubscribe=this.visibilityManager.onVisibilityChange(e=>{this.handleVisibilityChange(e)})}static async create(t){let n=new e(t);return await n.initialize(),n}getTabId(){return this.tabId}get isHost(){return this.hostElection.isHost}async initialize(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.doInitialize(),await this.initPromise,this.initialized=!0}}async doInitialize(){this.becomeHostPromise=new Promise(e=>{this.becomeHostResolver=e}),await this.hostElection.start();let e=new Promise(e=>{setTimeout(()=>e(`timeout`),500)});await Promise.race([this.becomeHostPromise.then(()=>`host`),e])===`host`||this.hostElection.isHost&&await this.becomeHostPromise}async becomeHost(){s.log(`Tab ${this.tabId} becoming Host for ${this.options.path}`),this.dbHost=await b.create({dbName:this.options.path,config:this.sqliteConfig}),this.txnSessionManager=new x(this.dbHost,this.hostElection),this.txnSessionManager.startWatchdog(),this.rpcChannel.setRequestHandler(this.handleRequest.bind(this)),this.becomeHostResolver&&=(this.becomeHostResolver(),void 0)}loseHost(){s.log(`Tab ${this.tabId} losing Host for ${this.options.path}`),this.rpcChannel.setRequestHandler(void 0),this.txnSessionManager&&=(this.txnSessionManager.close(),null),this.dbHost&&=(this.dbHost.close(),null)}handleVisibilityChange(e){e&&!this.hostElection.isHost&&!this.closed&&this.hostElection.start()}async handleRequest(e){if(!this.dbHost)throw Error(`Not Host`);switch(e.t){case`req`:return await this.handleRpcRequest(e);case`txn_begin`:return await this.handleTxnBegin(e);case`txn_exec`:return await this.handleTxnExec(e);case`txn_commit`:return await this.handleTxnCommit(e);case`txn_rollback`:return await this.handleTxnRollback(e);case`txn_heartbeat`:this.handleTxnHeartbeat(e);return;default:throw Error(`Unknown request type: ${e.t}`)}}async handleRpcRequest(e){let{op:t,payload:n}=e,{sql:r,params:i}=n;switch(this.txnSessionManager&&await this.txnSessionManager.waitForTransactionSlot(),t){case`query`:return{rows:await this.dbHost.query(r,i),columns:[]};case`queryRaw`:return await this.dbHost.queryRaw(r,i);case`run`:return await this.dbHost.run(r,i);case`exec`:return await this.dbHost.exec(r),null;default:throw Error(`Unknown RPC operation: ${String(t)}`)}}async handleTxnBegin(e){await this.txnSessionManager.beginTransaction(e.txnId,e.from,e.mode)}async handleTxnExec(e){let{txnId:t,op:n,payload:r}=e,{sql:i,params:a}=r;return await this.txnSessionManager.execInTransaction(t,async()=>{switch(n){case`query`:return{rows:await this.dbHost.query(i,a),columns:[]};case`queryRaw`:return await this.dbHost.queryRaw(i,a);case`run`:return await this.dbHost.run(i,a);case`exec`:return await this.dbHost.exec(i),null;default:throw Error(`Unknown transaction operation: ${String(n)}`)}})}async handleTxnCommit(e){await this.txnSessionManager.commitTransaction(e.txnId)}async handleTxnRollback(e){await this.txnSessionManager.rollbackTransaction(e.txnId)}handleTxnHeartbeat(e){this.txnSessionManager?.recordHeartbeat(e.txnId)}async query(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.query(e,t):(await this.rpcChannel.request(`query`,{sql:e,params:t})).rows}async queryRaw(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.queryRaw(e,t):await this.rpcChannel.request(`queryRaw`,{sql:e,params:t})}async run(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.run(e,t):await this.rpcChannel.request(`run`,{sql:e,params:t})}async exec(e){if(await this.initialize(),this.isHost&&this.dbHost){await this.dbHost.exec(e);return}await this.rpcChannel.request(`exec`,{sql:e})}async transaction(e){return await this.initialize(),this.isHost&&this.dbHost?await this.executeLocalTransaction(e,!1):await this.executeRemoteTransaction(e)}async asyncTransaction(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.executeLocalAsyncTransaction(e,t):await this.executeRemoteTransaction(e,t)}getConnectionType(){return`direct`}async close(){this.closed||(this.closed=!0,this.visibilityUnsubscribe?.(),await this.hostElection.close(),this.rpcChannel.close(),this.visibilityManager.destroy(),this.txnSessionManager&&=(await this.txnSessionManager.close(),null),this.dbHost&&=(await this.dbHost.close(),null))}async executeLocalTransaction(e,t){await this.dbHost.execRaw(`BEGIN`);let n=new C(this.dbHost,t);try{let t=e(n),r=t instanceof Promise?await t:t;return await this.dbHost.execRaw(`COMMIT`),r}catch(e){try{await this.dbHost.execRaw(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}async executeLocalAsyncTransaction(e,t){let n=t?.timeoutMs??3e4;await this.dbHost.execRaw(`BEGIN IMMEDIATE`);let r=new C(this.dbHost,!0),i,a=!1;try{let t=new Promise((e,t)=>{i=setTimeout(()=>{a||t(Error(`Async transaction timeout after ${n}ms`))},n)}),o=await Promise.race([e(r),t]);return a=!0,i&&clearTimeout(i),await this.dbHost.execRaw(`COMMIT`),o}catch(e){a=!0,i&&clearTimeout(i);try{await this.dbHost.execRaw(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}async executeRemoteTransaction(e,t){let n=g(),r=t?.timeoutMs??3e4;await this.rpcChannel.txnBegin(n,`immediate`,r);let i=new S(this.rpcChannel,n),a,o=!1;try{let t=new Promise((e,t)=>{a=setTimeout(()=>{o||t(Error(`Remote transaction timeout after ${r}ms`))},r)}),n=e(i),s=n instanceof Promise?n:Promise.resolve(n),c=await Promise.race([s,t]);return o=!0,a&&clearTimeout(a),await i._commit(),c}catch(e){o=!0,a&&clearTimeout(a);try{await i._rollback()}catch(e){console.error(`Remote rollback failed:`,e)}throw e}}getSQLiteInfo(){let e=this.isHost?this.dbHost!==null:this.initialized&&!this.closed;return{config:this.sqliteConfig,isInitialized:this.initialized,isHost:this.isHost,isReady:e,tabId:this.tabId,hasDatabase:this.dbHost!==null,usesWorker:this.dbHost?.isWorkerDbActive()??!1,activeStorageBackend:this.dbHost?.getActiveStorageBackend()??`unknown`,activeOpfsVfs:this.dbHost?.getActiveOpfsVfs()}}getActiveStorageBackend(){return this.dbHost?.getActiveStorageBackend()??`unknown`}};Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return _}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return f}}); | ||
| //# sourceMappingURL=browser2.cjs.map |
+17
-1
@@ -1,1 +0,17 @@ | ||
| import{t as e}from"./browser3.mjs";export{e as BrowserAdapter}; | ||
| import{t as e}from"./base.mjs";import t from"debug";const n=`unisqlite`;function r(e){let r=t(`${n}:${e}`),i=t(`${n}:${e}:warn`),a=t(`${n}:${e}:error`);return a.enabled=!0,{log:r,warn:i,error:a}}const i=r(`host`),a=r(`txn`),o=r(`adapter`);var s=class e{constructor(e){this.options=e,this.abortController=null,this._isHost=!1,this._isStarted=!1,this.releaseResolver=null,this.hostReadyResolver=null,this.hostReadyPromise=null,this.lockName=`sqlite:host:${e.dbName}`,this.txnLockName=`sqlite:txn:${e.dbName}`}static isSupported(){return typeof navigator<`u`&&navigator.locks!==void 0}get isHost(){return this._isHost}async start(){if(!this._isStarted){if(!e.isSupported()){i.warn(`Web Locks API not supported. Running in local-only mode.`),this._isStarted=!0,this._isHost=!0,this.options.onBecomeHost().catch(e=>{i.error(`Error in onBecomeHost callback:`,e),this._isHost=!1});return}this.options.checkVisibility&&!this.options.checkVisibility()||(this._isStarted=!0,this.hostReadyPromise=new Promise(e=>{this.hostReadyResolver=e}),this.tryAcquireHost().catch(e=>{i.error(`Error in host election:`,e)}))}}async waitForHost(){if(!this._isHost&&(this.hostReadyPromise||(await this.start(),this.hostReadyPromise)))return this.hostReadyPromise}async tryAcquireHost(){this.abortController=new AbortController;try{await navigator.locks.request(this.lockName,{mode:`exclusive`,signal:this.abortController.signal},async e=>{if(e){this._isHost=!0,i.log(`Tab ${this.options.tabId} became Host for ${this.options.dbName}`);try{await this.options.onBecomeHost()}catch(e){throw i.error(`Error in onBecomeHost callback:`,e),this._isHost=!1,e}this.hostReadyResolver&&(this.hostReadyResolver(),this.hostReadyResolver=null,this.hostReadyPromise=null),await new Promise(e=>{this.releaseResolver=e,this.abortController.signal.addEventListener(`abort`,()=>{e()})}),this._isHost=!1,this.releaseResolver=null,i.log(`Tab ${this.options.tabId} lost Host role for ${this.options.dbName}`);try{this.options.onLoseHost()}catch(e){i.error(`Error in onLoseHost callback:`,e)}}})}catch(e){if(e.name===`AbortError`)return;throw i.error(`Error acquiring Host lock:`,e),e}finally{this._isStarted=!1,this.abortController=null}}releaseHost(){this.releaseResolver&&=(this.releaseResolver(),null),this.abortController&&=(this.abortController.abort(),null)}async acquireTransactionLock(){if(!e.isSupported())return()=>{};let t=new AbortController,n=null,r=null,a=new Promise(e=>{r=e});return navigator.locks.request(this.txnLockName,{mode:`exclusive`,signal:t.signal},async e=>{e&&(r?.(),await new Promise(e=>{n=e}))}).catch(e=>{e.name!==`AbortError`&&i.error(`Error acquiring transaction lock:`,e),r?.()}),await a,()=>{n?n():t.abort()}}async hasTransactionLock(){if(!e.isSupported())return!1;try{return(await navigator.locks.query()).held?.some(e=>e.name===this.txnLockName)??!1}catch{return!1}}async close(){this.releaseHost()}},c=class{constructor(){this.listeners=new Set,this.boundHandler=this.handleVisibilityChange.bind(this),typeof document<`u`&&document.addEventListener(`visibilitychange`,this.boundHandler)}handleVisibilityChange(){let e=this.isVisible();this.listeners.forEach(t=>{try{t(e)}catch(e){console.error(`Error in visibility change listener:`,e)}})}isVisible(){return typeof document>`u`?!0:document.visibilityState===`visible`}onVisibilityChange(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}destroy(){typeof document<`u`&&document.removeEventListener(`visibilitychange`,this.boundHandler),this.listeners.clear()}};function l(e){return e instanceof Error?{name:e.name,message:e.message,stack:e.stack}:{name:`Error`,message:String(e)}}function u(e){let t=Error(e.message);return t.name=e.name,e.stack&&(t.stack=e.stack),t}function d(e){return e.t===`req`||e.t===`txn_begin`||e.t===`txn_exec`||e.t===`txn_commit`||e.t===`txn_rollback`||e.t===`txn_heartbeat`}function f(e){return e.t===`res`||e.t===`txn_ack`||e.t===`txn_result`||e.t===`txn_done`||e.t===`txn_error`}function p(){return crypto.randomUUID()}function m(){return crypto.randomUUID()}function h(){return crypto.randomUUID()}var g=class{constructor(e,t,n=3e4){this.dbName=e,this.tabId=t,this.defaultTimeoutMs=n,this.pending=new Map,this.closed=!1,this.channel=new BroadcastChannel(`sqlite:rpc:${e}`),this.channel.onmessage=this.handleMessage.bind(this)}handleMessage(e){if(this.closed)return;let t=e.data;!t||typeof t.t!=`string`||(f(t)?this.handleResponse(t):d(t)&&this.requestHandler&&`from`in t&&t.from!==this.tabId&&this.handleRequest(t))}handleResponse(e){if(`to`in e&&e.to!==this.tabId||!(`id`in e))return;let t=this.pending.get(e.id);if(t)switch(clearTimeout(t.timer),this.pending.delete(e.id),e.t){case`res`:e.ok?t.resolve(e.result):t.reject(u(e.error));break;case`txn_ack`:case`txn_done`:t.resolve(void 0);break;case`txn_result`:e.ok?t.resolve(e.result):t.reject(u(e.error));break;case`txn_error`:t.reject(u(e.error));break}}async handleRequest(e){if(this.requestHandler)try{let t=await this.requestHandler(e);this.sendResponse(e,t)}catch(t){this.sendErrorResponse(e,t)}}sendResponse(e,t){if(this.closed)return;let n=`from`in e?e.from:void 0,r=`id`in e?e.id:void 0;if(!(!n||!r))switch(e.t){case`req`:this.channel.postMessage({t:`res`,to:n,id:r,ok:!0,result:t});break;case`txn_begin`:this.channel.postMessage({t:`txn_ack`,to:n,id:r,txnId:e.txnId});break;case`txn_exec`:this.channel.postMessage({t:`txn_result`,to:n,id:r,txnId:e.txnId,ok:!0,result:t});break;case`txn_commit`:case`txn_rollback`:this.channel.postMessage({t:`txn_done`,to:n,id:r,txnId:e.txnId});break;case`txn_heartbeat`:break}}sendErrorResponse(e,t){if(this.closed)return;let n=`from`in e?e.from:void 0,r=`id`in e?e.id:void 0;if(!n||!r)return;let i=l(t);switch(e.t){case`req`:this.channel.postMessage({t:`res`,to:n,id:r,ok:!1,error:i});break;case`txn_begin`:case`txn_exec`:case`txn_commit`:case`txn_rollback`:this.channel.postMessage({t:`txn_error`,to:n,id:r,txnId:e.txnId,error:i});break}}setRequestHandler(e){this.requestHandler=e}async request(e,t,n){let r=p(),i=n??this.defaultTimeoutMs;return new Promise((n,a)=>{let o=setTimeout(()=>{this.pending.delete(r),a(Error(`RPC timeout after ${i}ms for ${e}`))},i);this.pending.set(r,{resolve:n,reject:a,timer:o}),this.channel.postMessage({t:`req`,from:this.tabId,id:r,op:e,payload:t})})}async txnBegin(e,t=`immediate`,n){let r=p(),i=n??this.defaultTimeoutMs;return new Promise((n,a)=>{let o=setTimeout(()=>{this.pending.delete(r),a(Error(`Transaction begin timeout after ${i}ms`))},i);this.pending.set(r,{resolve:n,reject:a,timer:o}),this.channel.postMessage({t:`txn_begin`,from:this.tabId,id:r,txnId:e,mode:t})})}async txnExec(e,t,n,r){let i=p(),a=r??1e4;return new Promise((r,o)=>{let s=setTimeout(()=>{this.pending.delete(i),o(Error(`Transaction SQL timeout after ${a}ms`))},a);this.pending.set(i,{resolve:r,reject:o,timer:s}),this.channel.postMessage({t:`txn_exec`,from:this.tabId,id:i,txnId:e,op:t,payload:n})})}async txnCommit(e,t){let n=p(),r=t??1e4;return new Promise((t,i)=>{let a=setTimeout(()=>{this.pending.delete(n),i(Error(`Transaction commit timeout after ${r}ms`))},r);this.pending.set(n,{resolve:t,reject:i,timer:a}),this.channel.postMessage({t:`txn_commit`,from:this.tabId,id:n,txnId:e})})}async txnRollback(e,t){let n=p(),r=t??1e4;return new Promise((t,i)=>{let a=setTimeout(()=>{this.pending.delete(n),i(Error(`Transaction rollback timeout after ${r}ms`))},r);this.pending.set(n,{resolve:t,reject:i,timer:a}),this.channel.postMessage({t:`txn_rollback`,from:this.tabId,id:n,txnId:e})})}txnHeartbeat(e){this.closed||this.channel.postMessage({t:`txn_heartbeat`,from:this.tabId,txnId:e})}close(){this.closed=!0,this.channel.close();for(let e of this.pending.values())clearTimeout(e.timer),e.reject(Error(`RPC channel closed`));this.pending.clear()}};const _=new Map,v=new Map;var y=class e{constructor(e,t){this.dbName=e,this.config=t,this.workerQueue=Promise.resolve(),this.activeStorageBackend=`memory`,this.closed=!1}static async create(t){let n=new e(t.dbName,t.config);return await n.initialize(),n}async initialize(){console.log(`Loading SQLite WASM using strategy: ${this.config.loadStrategy}`);let e=await this.loadSQLiteWasm(),t={print:console.log,printErr:console.error};if(this.config.locateFile)t.locateFile=this.config.locateFile;else if(typeof window<`u`){let e=window;e.sqlite3InitModuleState?.locateFile&&(t.locateFile=e.sqlite3InitModuleState.locateFile)}if(console.log(`Initializing SQLite WASM module...`),this.sqlite3=await e(t),!this.sqlite3)throw Error(`Failed to initialize SQLite WASM module`);let n=this.sqlite3;console.log(`SQLite WASM module initialized successfully`);let r=this.config.storageBackend??`auto`,i=this.dbName===`:memory:`?`:memory:`:`/unisqlite/${this.dbName}`;r===`memory`||this.dbName===`:memory:`?(console.log(`Using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`):await this.tryCreatePersistentDatabase(n,i,r)||(console.log(`All persistent storage backends failed, using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`);try{await this.execInternal(`PRAGMA journal_mode=WAL`),console.log(`Enabled WAL mode`)}catch(e){console.warn(`Could not enable WAL mode:`,e)}}async query(e,t){return(await this.executeSql(e,t)).rows}async queryRaw(e,t){return await this.executeSql(e,t)}async run(e,t){let n=await this.executeSql(e,t);return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){await this.execInternal(e)}async execRaw(e){await this.execInternal(e,{bypassQueue:!0})}async executeSqlRaw(e,t){return await this.executeSql(e,t,{bypassQueue:!0})}getActiveStorageBackend(){return this.activeStorageBackend}getActiveOpfsVfs(){return this.activeOpfsVfs}isWorkerDbActive(){return!!this.workerPromiser&&this.workerDbId!==void 0}async close(){if(!this.closed){if(this.closed=!0,this.workerPromiser){let e=this.workerPromiser,t=this.workerDbId;try{t!==void 0&&await this.enqueueWorker(async()=>{await e(`close`,{dbId:t})})}catch(e){console.warn(`Failed to close SQLite worker database:`,e)}finally{this.workerDbId=void 0;let t=e.worker;t?.terminate&&t.terminate(),this.workerPromiser=void 0}}this.db&&=(this.db.close(),void 0)}}enqueueWorker(e){let t=async()=>e(),n=this.workerQueue.then(t,t);return this.workerQueue=n.then(()=>void 0,()=>void 0),n}normalizeParams(e){if(!e)return;if(Array.isArray(e))return e;let t=Object.keys(e),n=e;return t.map(e=>n[e])}async execInternal(e,t){if(this.isWorkerDbActive()){let n=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);await this.workerPromiser(`exec`,{dbId:this.workerDbId,sql:e})};t?.bypassQueue?await n():await this.enqueueWorker(n);return}if(!this.db)throw Error(`Database not initialized`);this.db.exec(e)}async executeSql(e,t,n){try{if(this.isWorkerDbActive()){let r=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);let n=this.normalizeParams(t),r={dbId:this.workerDbId,sql:e,rowMode:`object`,resultRows:[],columnNames:[],countChanges:!0};n&&(r.bind=n);let i=await this.workerPromiser(`exec`,r),a=i.result??i,o=Array.isArray(a.resultRows)?a.resultRows:[],s=Array.isArray(a.columnNames)?a.columnNames:[],c=typeof a.changeCount==`number`?a.changeCount:0,l={dbId:this.workerDbId,sql:`SELECT last_insert_rowid() AS lastInsertRowId`,rowMode:`object`,resultRows:[],columnNames:[]},u=await this.workerPromiser(`exec`,l),d=u.result??u,f=Array.isArray(d.resultRows)?d.resultRows:[];return{rows:o,columns:s,rowsAffected:c,lastInsertRowId:Number(f[0]?.lastInsertRowId??0)}};return n?.bypassQueue?await r():await this.enqueueWorker(r)}if(!this.db||!this.sqlite3)throw Error(`Database not initialized`);let r=this.normalizeParams(t),i=[],a=[],o=this.db.prepare(e);try{let e=o;r&&o.bind(r);let t=typeof e.getColumnCount==`function`?e.getColumnCount():typeof e.columnCount==`number`?e.columnCount:0;if(t>0)if(typeof e.getColumnNames==`function`)a=e.getColumnNames();else for(let e=0;e<t;e++)a.push(o.getColumnName(e));for(;o.step();){let e={};for(let n=0;n<t;n++)e[a[n]]=o.get(n);i.push(e)}return{rows:i,columns:a,rowsAffected:this.db.changes(),lastInsertRowId:Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)||0)}}finally{o.finalize()}}catch(e){let t=e instanceof Error?e.message:String(e),n=Error(`SQLite error: ${t}`);throw e instanceof Error&&(n.cause=e),n}}async loadSQLiteWasm(){let e=`${this.config.loadStrategy}-${this.config.wasmUrl||this.config.cdnBaseUrl}-${this.config.version}`;if(_.has(e))return _.get(e);let t=this.loadSQLiteWasmInternal();return _.set(e,t),t}async loadSQLiteWasmInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.config;switch(e){case`npm`:return await this.loadFromNpm();case`cdn`:return await this.loadFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadFromUrl(t);case`module`:return await this.loadAsModule();case`global`:default:return this.loadFromGlobal()}}async loadFromNpm(){try{let e=await import(`@sqlite.org/sqlite-wasm`);return e.default||e}catch(e){return console.warn(`Failed to load SQLite WASM from npm, falling back to CDN:`,e),this.loadFromCdn(this.config.cdnBaseUrl,this.config.version,this.config.bundlerFriendly)}}async loadFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/${r}`,`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM from: ${e}`);let t=await import(e);return console.log(`Successfully loaded SQLite WASM from: ${e}`),t.default||t}catch(t){a=t,console.warn(`Failed to load SQLite WASM from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load SQLite WASM from any CDN source. Last error: ${o}`)}async loadFromUrl(e){let t=await import(e);return t.default||t.sqlite3InitModule}async loadAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=await import(e);return t.default||t.sqlite3InitModule}catch(t){console.warn(`Failed to load SQLite WASM from ${e}:`,t)}throw Error(`Could not load SQLite WASM as ES6 module from any known path`)}loadFromGlobal(){if(typeof window<`u`){let e=window;if(e.sqlite3InitModule)return e.sqlite3InitModule}throw Error(`SQLite WASM module not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM script in your HTML before using UniSqlite`)}async loadSQLiteWorkerPromiserFactory(){let e=`worker-${this.config.loadStrategy}-${this.config.wasmUrl||this.config.cdnBaseUrl}-${this.config.version}`;if(v.has(e))return v.get(e);let t=this.loadSQLiteWorkerPromiserFactoryInternal();return v.set(e,t),t}async loadSQLiteWorkerPromiserFactoryInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.config;switch(e){case`npm`:return await this.loadWorkerPromiserFromNpm();case`cdn`:return await this.loadWorkerPromiserFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadWorkerPromiserFromUrl(t);case`module`:return await this.loadWorkerPromiserAsModule();case`global`:default:return this.loadWorkerPromiserFromGlobal()}}async loadWorkerPromiserFromNpm(){try{let e=(await import(`@sqlite.org/sqlite-wasm`)).sqlite3Worker1Promiser;if(typeof e==`function`)return e;throw Error(`sqlite3Worker1Promiser export not found`)}catch(e){return console.warn(`Failed to load SQLite WASM worker promiser from npm, falling back to CDN:`,e),this.loadWorkerPromiserFromCdn(this.config.cdnBaseUrl,this.config.version,this.config.bundlerFriendly)}}async loadWorkerPromiserFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`${e}@${t}/${r}`,`${e}@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM worker promiser from: ${e}`);let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return console.log(`Successfully loaded SQLite WASM worker promiser from: ${e}`),t;throw Error(`sqlite3Worker1Promiser export not found`)}catch(t){a=t,console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${o}`)}async loadWorkerPromiserFromUrl(e){let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t!=`function`)throw Error(`sqlite3Worker1Promiser export not found in module loaded from ${e}`);return t}async loadWorkerPromiserAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return t}catch(t){console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}throw Error(`Could not load sqlite3Worker1Promiser as ES6 module from any known path`)}loadWorkerPromiserFromGlobal(){let e=globalThis;if(typeof e.sqlite3Worker1Promiser==`function`)return e.sqlite3Worker1Promiser;throw Error(`SQLite WASM worker promiser not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm' | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite`)}async tryCreatePersistentDatabase(e,t,n){if(n===`opfs`||n===`auto`){let r=this.config.opfsVfsType??`auto`,i=r===`auto`?e.oo1.OpfsDb?[`opfs`]:[`opfs-sahpool`,`opfs`]:r===`sahpool`?[`opfs-sahpool`]:[`opfs`];for(let n of i)if(await this.tryCreateOpfsDatabase(e,t,n))return!0;if(n===`opfs`)throw Error(this.getOpfsUnavailableError())}if(n===`localStorage`||n===`auto`){if(e.oo1.JsStorageDb)if(this.dbName===`local`||this.dbName===`session`)try{return console.log(`Creating localStorage-backed database:`,this.dbName),this.db=new e.oo1.JsStorageDb(this.dbName),this.activeStorageBackend=`localStorage`,console.log(`Successfully created localStorage-backed database`),!0}catch(e){if(console.warn(`localStorage database creation failed:`,e),n===`localStorage`)throw Error(`localStorage database creation failed: ${e instanceof Error?e.message:String(e)}`)}else if(n===`localStorage`)throw Error(`localStorage storage backend requires path to be 'local' or 'session', got '${this.dbName}'. Use storageBackend: 'opfs' for custom database names with persistence.`);else console.log(`Skipping localStorage: path '${this.dbName}' is not 'local' or 'session'`);else if(n===`localStorage`)throw Error(`JsStorageDb is not available in this environment.`)}return!1}getOpfsUnavailableError(){return`OPFS is not available. | ||
| SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood. | ||
| Make sure: | ||
| - You are in a secure context (HTTPS or localhost) | ||
| - For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available | ||
| - Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)`}async tryCreateOpfsDatabase(e,t,n){if(n===`opfs`){if(e.oo1.OpfsDb)try{return console.log(`Creating OPFS-backed database (OpfsDb):`,t),this.db=new e.oo1.OpfsDb(t),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs`,console.log(`Successfully created OPFS-backed database (OpfsDb)`),!0}catch(e){console.warn(`OPFS database creation via OpfsDb failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}if(await this.ensureSahpoolVfs(e))try{return console.log(`Creating SAHPool OPFS-backed database:`,t),this.db=new e.oo1.DB(t,`c`,`opfs-sahpool`),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs-sahpool`,console.log(`Successfully created SAHPool OPFS-backed database`),!0}catch(e){console.warn(`SAHPool OPFS database creation failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}async ensureSahpoolVfs(e){if(globalThis.importScripts===void 0)return!1;let t=`opfs-sahpool`;try{if(e.capi.sqlite3_vfs_find?.(t))return!0}catch(e){console.warn(`SAHPool VFS lookup failed:`,e)}if(typeof e.installOpfsSAHPoolVfs!=`function`)return!1;try{await e.installOpfsSAHPoolVfs({name:t,directory:`/unisqlite-sahpool`})}catch(e){return console.warn(`SAHPool VFS installation failed:`,e),!1}try{return!!e.capi.sqlite3_vfs_find?.(t)}catch{return!0}}createWorkerFromConfig(){if(!this.config.workerUrl||typeof Worker>`u`)return;let e=typeof this.config.workerUrl==`string`?this.config.workerUrl:this.config.workerUrl.href;try{return new Worker(e,{type:`module`})}catch(e){console.warn(`Failed to create Worker from custom workerUrl:`,e);return}}createWorkerFromCdn(e){if(typeof Worker>`u`||typeof Blob>`u`||typeof URL>`u`)return;let t=this.config.cdnBaseUrl,n=this.config.version;if(!t||!n)return;let r=`${`${t}@${n}/sqlite-wasm/jswasm`}/sqlite3-bundler-friendly.mjs`,i=e.installSahpool?[` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`,` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`,` }`].join(` | ||
| `):``,a=[`import sqlite3InitModule from ${JSON.stringify(r)};`,`sqlite3InitModule().then(async (sqlite3) => {`,` try {`,i||` // no-op`,` } catch (e) {`,` console.warn("SAHPool VFS installation failed:", e);`,` }`,` sqlite3.initWorker1API();`,`});`,``].join(` | ||
| `),o=new Blob([a],{type:`text/javascript`}),s=URL.createObjectURL(o),c=new Worker(s,{type:`module`});return c.addEventListener(`message`,()=>{URL.revokeObjectURL(s)},{once:!0}),c.addEventListener(`error`,()=>{URL.revokeObjectURL(s)},{once:!0}),c}async getOrCreateWorkerPromiser(e){if(this.workerPromiser)return this.workerPromiser;if(this.workerPromiserInitPromise)return await this.workerPromiserInitPromise;let t=await this.loadSQLiteWorkerPromiserFactory(),n=this.config.storageBackend===`opfs`||this.config.opfsVfsType===`opfs`||this.config.opfsVfsType===`sahpool`,r=this.config.loadStrategy===`global`&&!n?5e3:2e4;this.workerPromiserInitPromise=new Promise((n,i)=>{let a=!1,o=setTimeout(()=>{a||(a=!0,i(Error(`SQLite worker initialization timed out after ${r}ms`)))},r),s=(e,t)=>{a||(a=!0,clearTimeout(o),e(t))},c=e=>{s(i,e instanceof Error?e:Error(String(e)))},l;try{l=t({...e?.worker?{worker:e.worker}:{},onready:e=>{if(typeof e==`function`){s(n,e);return}if(typeof l==`function`){s(n,l);return}if(l&&typeof l.then==`function`){l.then(e=>s(n,e),e=>c(e));return}c(Error(`sqlite3Worker1Promiser did not provide a usable promiser`))},onerror:e=>{let t=e instanceof Error?e.message:String(e);c(Error(`SQLite worker error: ${t}`))}}),l&&typeof l.then==`function`&&l.then(e=>s(n,e),e=>c(e))}catch(e){c(e)}});let i=await this.workerPromiserInitPromise;return this.workerPromiser=i,i}async tryCreateWrappedWorkerOpfsDatabase(e,t){try{let n=!this.workerPromiser&&!this.workerPromiserInitPromise?(()=>{let e=this.createWorkerFromConfig()??(t===`opfs-sahpool`?this.createWorkerFromCdn({installSahpool:!0}):this.createWorkerFromCdn({installSahpool:!1}));return e?{worker:e}:void 0})():void 0,r=await(await this.getOrCreateWorkerPromiser(n))(`open`,{filename:`file:${e}`,vfs:t}),i=r.dbId??r.result?.dbId;if(i===void 0)throw Error(`SQLite worker did not return a dbId`);if(typeof i!=`number`&&typeof i!=`string`)throw Error(`SQLite worker returned an invalid dbId (type=${typeof i})`);return this.db=void 0,this.workerDbId=i,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=t,console.log(`Successfully created OPFS-backed database via wrapped worker (${t}):`,e),!0}catch(e){return console.warn(`Wrapped-worker OPFS database creation failed (vfs=${t}):`,e),!1}}},b=class{constructor(e,t,n){this.dbHost=e,this.hostElection=t,this.activeTxn=null,this.txnQueue=[],this.heartbeatTimers=new Map,this.txnPending=!1,this.rpcQueue=[],this.txnTimeoutMs=n?.txnTimeoutMs??3e4,this.opTimeoutMs=n?.opTimeoutMs??1e4,this.maxQueueSize=n?.maxQueueSize??10,this.heartbeatTimeoutMs=n?.heartbeatTimeoutMs??15e3}startWatchdog(){this.watchdogTimer||=setInterval(()=>{if(this.activeTxn){let e=Date.now()-this.activeTxn.lastActivityAt;e>this.txnTimeoutMs&&(a.warn(`Transaction ${this.activeTxn.txnId} timed out after ${e}ms, rolling back`),this.forceRollback())}let e=Date.now(),t=this.txnQueue.filter(t=>e-t.enqueuedAt>this.txnTimeoutMs);for(let e of t){let t=this.txnQueue.indexOf(e);t!==-1&&(this.txnQueue.splice(t,1),e.reject(Error(`Transaction queue wait timeout`)))}},5e3)}stopWatchdog(){this.watchdogTimer&&=(clearInterval(this.watchdogTimer),void 0)}getActiveTransaction(){return this.activeTxn}hasActiveTransaction(){return this.activeTxn!==null}isTransactionBlocking(){return this.activeTxn!==null||this.txnPending}async waitForTransactionSlot(){if(this.isTransactionBlocking())return new Promise((e,t)=>{this.rpcQueue.push({resolve:e,reject:t})})}async beginTransaction(e,t,n=`immediate`){if(this.activeTxn||this.txnPending){if(this.txnQueue.length>=this.maxQueueSize)throw Error(`Transaction queue full`);await new Promise((r,i)=>{this.txnQueue.push({txnId:e,originTabId:t,mode:n,resolve:r,reject:i,enqueuedAt:Date.now()})})}this.txnPending=!0;try{this.txnLockRelease=await this.hostElection.acquireTransactionLock();let r=n===`deferred`?`BEGIN`:n===`exclusive`?`BEGIN EXCLUSIVE`:`BEGIN IMMEDIATE`;await this.dbHost.execRaw(r),this.activeTxn={txnId:e,originTabId:t,startedAt:Date.now(),lastActivityAt:Date.now(),mode:n},this.resetHeartbeatTimer(e)}catch(e){throw this.txnLockRelease?.(),this.txnLockRelease=void 0,this.txnPending=!1,this.processQueue(),this.processRpcQueue(),e}finally{this.txnPending=!1}}async execInTransaction(e,t){return this.validateActiveTxn(e),this.activeTxn.lastActivityAt=Date.now(),this.resetHeartbeatTimer(e),await t()}recordHeartbeat(e){this.activeTxn?.txnId===e&&(this.activeTxn.lastActivityAt=Date.now(),this.resetHeartbeatTimer(e))}async commitTransaction(e){this.validateActiveTxn(e);try{await this.dbHost.execRaw(`COMMIT`)}finally{this.endTransaction()}}async rollbackTransaction(e){this.validateActiveTxn(e);try{await this.dbHost.execRaw(`ROLLBACK`)}finally{this.endTransaction()}}forceRollback(){if(this.activeTxn){try{this.dbHost.execRaw(`ROLLBACK`).catch(e=>{a.error(`Force rollback failed:`,e)})}catch(e){a.error(`Force rollback failed:`,e)}this.endTransaction()}}endTransaction(){if(this.activeTxn){let e=this.heartbeatTimers.get(this.activeTxn.txnId);e&&(clearTimeout(e),this.heartbeatTimers.delete(this.activeTxn.txnId)),this.activeTxn=null}this.txnLockRelease&&=(this.txnLockRelease(),void 0),this.processQueue(),this.processRpcQueue()}processQueue(){this.txnQueue.length>0&&!this.activeTxn&&!this.txnPending&&this.txnQueue.shift().resolve()}processRpcQueue(){if(this.activeTxn||this.txnPending)return;let e=this.rpcQueue;this.rpcQueue=[];for(let{resolve:t}of e)t()}resetHeartbeatTimer(e){let t=this.heartbeatTimers.get(e);t&&clearTimeout(t);let n=setTimeout(()=>{this.activeTxn?.txnId===e&&(a.warn(`No heartbeat for transaction ${e} in ${this.heartbeatTimeoutMs}ms, rolling back`),this.forceRollback())},this.heartbeatTimeoutMs);this.heartbeatTimers.set(e,n)}validateActiveTxn(e){if(!this.activeTxn)throw Error(`No active transaction (expected ${e})`);if(this.activeTxn.txnId!==e)throw Error(`Transaction ID mismatch: expected ${this.activeTxn.txnId}, got ${e}`);let t=Date.now()-this.activeTxn.lastActivityAt;if(t>this.txnTimeoutMs)throw this.forceRollback(),Error(`Transaction ${e} timed out after ${t}ms`)}async close(){this.stopWatchdog(),this.activeTxn&&this.forceRollback();for(let e of this.txnQueue)e.reject(Error(`Transaction session manager closed`));this.txnQueue=[];for(let e of this.rpcQueue)e.reject(Error(`Transaction session manager closed`));this.rpcQueue=[];for(let e of this.heartbeatTimers.values())clearTimeout(e);this.heartbeatTimers.clear()}},x=class{constructor(e,t,n=5e3){this.rpcChannel=e,this.txnId=t,this.heartbeatIntervalMs=n,this.committed=!1,this.rolledBack=!1,this.startHeartbeat()}startHeartbeat(){this.heartbeatInterval=setInterval(()=>{!this.committed&&!this.rolledBack&&this.rpcChannel.txnHeartbeat(this.txnId)},this.heartbeatIntervalMs)}stopHeartbeat(){this.heartbeatInterval&&=(clearInterval(this.heartbeatInterval),void 0)}ensureActive(){if(this.committed)throw Error(`Transaction already committed`);if(this.rolledBack)throw Error(`Transaction already rolled back`)}async query(e,t){return this.ensureActive(),(await this.rpcChannel.txnExec(this.txnId,`query`,{sql:e,params:t})).rows}async queryRaw(e,t){return this.ensureActive(),await this.rpcChannel.txnExec(this.txnId,`queryRaw`,{sql:e,params:t})}async run(e,t){this.ensureActive();let n=await this.rpcChannel.txnExec(this.txnId,`run`,{sql:e,params:t});return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){this.ensureActive(),await this.rpcChannel.txnExec(this.txnId,`exec`,{sql:e})}async transaction(e){this.ensureActive();let t=e(this);return t instanceof Promise?await t:t}async asyncTransaction(e,t){return this.ensureActive(),await e(this)}getConnectionType(){return`asyncTxn`}async close(){}async _commit(){this.ensureActive(),this.stopHeartbeat();try{await this.rpcChannel.txnCommit(this.txnId),this.committed=!0}catch(e){throw this.rolledBack=!0,e}}async _rollback(){if(!(this.committed||this.rolledBack)){this.stopHeartbeat();try{await this.rpcChannel.txnRollback(this.txnId)}finally{this.rolledBack=!0}}}isCommitted(){return this.committed}isRolledBack(){return this.rolledBack}},S=class{constructor(e,t){this.dbHost=e,this.isAsync=t}async query(e,t){return(await this.dbHost.executeSqlRaw(e,t)).rows}async queryRaw(e,t){return await this.dbHost.executeSqlRaw(e,t)}async run(e,t){let n=await this.dbHost.executeSqlRaw(e,t);return{rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0}}async exec(e){await this.dbHost.execRaw(e)}async transaction(e){let t=e(this);return t instanceof Promise?await t:t}async asyncTransaction(e){return await e(this)}getConnectionType(){return this.isAsync?`asyncTxn`:`syncTxn`}async close(){}},C=class t extends e{constructor(e){let t=e.path??e.name??`default`;super({...e,path:t}),this.dbHost=null,this.txnSessionManager=null,this.initialized=!1,this.closed=!1,this.options={...e,path:t},this.tabId=m(),this.sqliteConfig={loadStrategy:`global`,storageBackend:`auto`,opfsVfsType:`auto`,cdnBaseUrl:`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm`,version:`3.50.1-build1`,bundlerFriendly:!1,...e.sqlite},s.isSupported()||(o.warn(`Web Locks API not supported. Falling back to memory-only storage.`),this.sqliteConfig.storageBackend=`memory`),this.visibilityManager=new c,this.rpcChannel=new g(t,this.tabId),this.hostElection=new s({dbName:t,tabId:this.tabId,onBecomeHost:()=>this.becomeHost(),onLoseHost:()=>this.loseHost(),checkVisibility:()=>this.visibilityManager.isVisible()}),this.visibilityUnsubscribe=this.visibilityManager.onVisibilityChange(e=>{this.handleVisibilityChange(e)})}static async create(e){let n=new t(e);return await n.initialize(),n}getTabId(){return this.tabId}get isHost(){return this.hostElection.isHost}async initialize(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.doInitialize(),await this.initPromise,this.initialized=!0}}async doInitialize(){this.becomeHostPromise=new Promise(e=>{this.becomeHostResolver=e}),await this.hostElection.start();let e=new Promise(e=>{setTimeout(()=>e(`timeout`),500)});await Promise.race([this.becomeHostPromise.then(()=>`host`),e])===`host`||this.hostElection.isHost&&await this.becomeHostPromise}async becomeHost(){o.log(`Tab ${this.tabId} becoming Host for ${this.options.path}`),this.dbHost=await y.create({dbName:this.options.path,config:this.sqliteConfig}),this.txnSessionManager=new b(this.dbHost,this.hostElection),this.txnSessionManager.startWatchdog(),this.rpcChannel.setRequestHandler(this.handleRequest.bind(this)),this.becomeHostResolver&&=(this.becomeHostResolver(),void 0)}loseHost(){o.log(`Tab ${this.tabId} losing Host for ${this.options.path}`),this.rpcChannel.setRequestHandler(void 0),this.txnSessionManager&&=(this.txnSessionManager.close(),null),this.dbHost&&=(this.dbHost.close(),null)}handleVisibilityChange(e){e&&!this.hostElection.isHost&&!this.closed&&this.hostElection.start()}async handleRequest(e){if(!this.dbHost)throw Error(`Not Host`);switch(e.t){case`req`:return await this.handleRpcRequest(e);case`txn_begin`:return await this.handleTxnBegin(e);case`txn_exec`:return await this.handleTxnExec(e);case`txn_commit`:return await this.handleTxnCommit(e);case`txn_rollback`:return await this.handleTxnRollback(e);case`txn_heartbeat`:this.handleTxnHeartbeat(e);return;default:throw Error(`Unknown request type: ${e.t}`)}}async handleRpcRequest(e){let{op:t,payload:n}=e,{sql:r,params:i}=n;switch(this.txnSessionManager&&await this.txnSessionManager.waitForTransactionSlot(),t){case`query`:return{rows:await this.dbHost.query(r,i),columns:[]};case`queryRaw`:return await this.dbHost.queryRaw(r,i);case`run`:return await this.dbHost.run(r,i);case`exec`:return await this.dbHost.exec(r),null;default:throw Error(`Unknown RPC operation: ${String(t)}`)}}async handleTxnBegin(e){await this.txnSessionManager.beginTransaction(e.txnId,e.from,e.mode)}async handleTxnExec(e){let{txnId:t,op:n,payload:r}=e,{sql:i,params:a}=r;return await this.txnSessionManager.execInTransaction(t,async()=>{switch(n){case`query`:return{rows:await this.dbHost.query(i,a),columns:[]};case`queryRaw`:return await this.dbHost.queryRaw(i,a);case`run`:return await this.dbHost.run(i,a);case`exec`:return await this.dbHost.exec(i),null;default:throw Error(`Unknown transaction operation: ${String(n)}`)}})}async handleTxnCommit(e){await this.txnSessionManager.commitTransaction(e.txnId)}async handleTxnRollback(e){await this.txnSessionManager.rollbackTransaction(e.txnId)}handleTxnHeartbeat(e){this.txnSessionManager?.recordHeartbeat(e.txnId)}async query(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.query(e,t):(await this.rpcChannel.request(`query`,{sql:e,params:t})).rows}async queryRaw(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.queryRaw(e,t):await this.rpcChannel.request(`queryRaw`,{sql:e,params:t})}async run(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.dbHost.run(e,t):await this.rpcChannel.request(`run`,{sql:e,params:t})}async exec(e){if(await this.initialize(),this.isHost&&this.dbHost){await this.dbHost.exec(e);return}await this.rpcChannel.request(`exec`,{sql:e})}async transaction(e){return await this.initialize(),this.isHost&&this.dbHost?await this.executeLocalTransaction(e,!1):await this.executeRemoteTransaction(e)}async asyncTransaction(e,t){return await this.initialize(),this.isHost&&this.dbHost?await this.executeLocalAsyncTransaction(e,t):await this.executeRemoteTransaction(e,t)}getConnectionType(){return`direct`}async close(){this.closed||(this.closed=!0,this.visibilityUnsubscribe?.(),await this.hostElection.close(),this.rpcChannel.close(),this.visibilityManager.destroy(),this.txnSessionManager&&=(await this.txnSessionManager.close(),null),this.dbHost&&=(await this.dbHost.close(),null))}async executeLocalTransaction(e,t){await this.dbHost.execRaw(`BEGIN`);let n=new S(this.dbHost,t);try{let t=e(n),r=t instanceof Promise?await t:t;return await this.dbHost.execRaw(`COMMIT`),r}catch(e){try{await this.dbHost.execRaw(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}async executeLocalAsyncTransaction(e,t){let n=t?.timeoutMs??3e4;await this.dbHost.execRaw(`BEGIN IMMEDIATE`);let r=new S(this.dbHost,!0),i,a=!1;try{let t=new Promise((e,t)=>{i=setTimeout(()=>{a||t(Error(`Async transaction timeout after ${n}ms`))},n)}),o=await Promise.race([e(r),t]);return a=!0,i&&clearTimeout(i),await this.dbHost.execRaw(`COMMIT`),o}catch(e){a=!0,i&&clearTimeout(i);try{await this.dbHost.execRaw(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}async executeRemoteTransaction(e,t){let n=h(),r=t?.timeoutMs??3e4;await this.rpcChannel.txnBegin(n,`immediate`,r);let i=new x(this.rpcChannel,n),a,o=!1;try{let t=new Promise((e,t)=>{a=setTimeout(()=>{o||t(Error(`Remote transaction timeout after ${r}ms`))},r)}),n=e(i),s=n instanceof Promise?n:Promise.resolve(n),c=await Promise.race([s,t]);return o=!0,a&&clearTimeout(a),await i._commit(),c}catch(e){o=!0,a&&clearTimeout(a);try{await i._rollback()}catch(e){console.error(`Remote rollback failed:`,e)}throw e}}getSQLiteInfo(){let e=this.isHost?this.dbHost!==null:this.initialized&&!this.closed;return{config:this.sqliteConfig,isInitialized:this.initialized,isHost:this.isHost,isReady:e,tabId:this.tabId,hasDatabase:this.dbHost!==null,usesWorker:this.dbHost?.isWorkerDbActive()??!1,activeStorageBackend:this.dbHost?.getActiveStorageBackend()??`unknown`,activeOpfsVfs:this.dbHost?.getActiveOpfsVfs()}}getActiveStorageBackend(){return this.dbHost?.getActiveStorageBackend()??`unknown`}};export{g as a,m as c,f as d,l as f,y as i,h as l,s as m,x as n,u as o,c as p,b as r,p as s,C as t,d as u}; | ||
| //# sourceMappingURL=browser2.mjs.map |
+1
-17
@@ -1,17 +0,1 @@ | ||
| const e=require(`./node2.cjs`),t=require(`./base.cjs`);let n=require(`broadcast-channel`);const r=new Map,i=new Map;function a(e){return typeof e==`object`&&!!e&&`data`in e}var o=class e extends t.t{constructor(e){let t=e.path??e.name??`default`;super({...e,path:t}),this.isLeader=!1,this.workerQueue=Promise.resolve(),this.workerTxnDepth=0,this.pendingRequests=new Map,this.initialized=!1,this.activeStorageBackend=`memory`,this.options={...e,path:t},this.channel=new n.BroadcastChannel(`unisqlite-${t}`),this.sqliteConfig={loadStrategy:`global`,storageBackend:`auto`,opfsVfsType:`auto`,cdnBaseUrl:`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm`,version:`3.50.1-build1`,bundlerFriendly:!1,...e.sqlite}}static async create(t){let n=new e(t);return await n.initialize(),n}async loadSQLiteWasm(){let e=`${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl||this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`;if(r.has(e))return r.get(e);let t=this.loadSQLiteWasmInternal();return r.set(e,t),t}async loadSQLiteWasmInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.sqliteConfig;switch(e){case`npm`:return await this.loadFromNpm();case`cdn`:return await this.loadFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadFromUrl(t);case`module`:return await this.loadAsModule();case`global`:default:return this.loadFromGlobal()}}enqueueWorker(e){let t=async()=>e(),n=this.workerQueue.then(t,t);return this.workerQueue=n.then(()=>void 0,()=>void 0),n}isWorkerDbActive(){return!!this.workerPromiser&&this.workerDbId!==void 0}async loadSQLiteWorkerPromiserFactory(){let e=`worker-${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl||this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`;if(i.has(e))return i.get(e);let t=this.loadSQLiteWorkerPromiserFactoryInternal();return i.set(e,t),t}async loadSQLiteWorkerPromiserFactoryInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.sqliteConfig;switch(e){case`npm`:return await this.loadWorkerPromiserFromNpm();case`cdn`:return await this.loadWorkerPromiserFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadWorkerPromiserFromUrl(t);case`module`:return await this.loadWorkerPromiserAsModule();case`global`:default:return this.loadWorkerPromiserFromGlobal()}}async loadWorkerPromiserFromNpm(){try{let e=(await import(`@sqlite.org/sqlite-wasm`)).sqlite3Worker1Promiser;if(typeof e==`function`)return e;throw Error(`sqlite3Worker1Promiser export not found`)}catch(e){return console.warn(`Failed to load SQLite WASM worker promiser from npm, falling back to CDN:`,e),this.loadWorkerPromiserFromCdn(this.sqliteConfig.cdnBaseUrl,this.sqliteConfig.version,this.sqliteConfig.bundlerFriendly)}}async loadWorkerPromiserFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`${e}@${t}/${r}`,`${e}@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM worker promiser from: ${e}`);let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return console.log(`Successfully loaded SQLite WASM worker promiser from: ${e}`),t;throw Error(`sqlite3Worker1Promiser export not found`)}catch(t){a=t,console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${o}`)}async loadWorkerPromiserFromUrl(e){let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t!=`function`)throw Error(`sqlite3Worker1Promiser export not found in module loaded from ${e}`);return t}async loadWorkerPromiserAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return t}catch(t){console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}throw Error(`Could not load sqlite3Worker1Promiser as ES6 module from any known path`)}loadWorkerPromiserFromGlobal(){let e=globalThis;if(typeof e.sqlite3Worker1Promiser==`function`)return e.sqlite3Worker1Promiser;throw Error(`SQLite WASM worker promiser not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm' | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite`)}createWorkerFromCdn(e){if(typeof Worker>`u`||typeof Blob>`u`||typeof URL>`u`)return;let t=this.sqliteConfig.cdnBaseUrl,n=this.sqliteConfig.version;if(!t||!n)return;let r=`${`${t}@${n}/sqlite-wasm/jswasm`}/sqlite3-bundler-friendly.mjs`,i=e.installSahpool?[` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`,` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`,` }`].join(` | ||
| `):``,a=[`import sqlite3InitModule from ${JSON.stringify(r)};`,`sqlite3InitModule().then(async (sqlite3) => {`,` try {`,i||` // no-op`,` } catch (e) {`,` console.warn("SAHPool VFS installation failed:", e);`,` }`,` sqlite3.initWorker1API();`,`});`,``].join(` | ||
| `),o=new Blob([a],{type:`text/javascript`}),s=URL.createObjectURL(o),c=new Worker(s,{type:`module`});return c.addEventListener(`message`,()=>{URL.revokeObjectURL(s)},{once:!0}),c.addEventListener(`error`,()=>{URL.revokeObjectURL(s)},{once:!0}),c}createSahpoolWorker(){if(this.sqliteConfig.loadStrategy===`cdn`)return this.createWorkerFromCdn({installSahpool:!0})}createCdnWorker(){if(this.sqliteConfig.loadStrategy===`cdn`)return this.createWorkerFromCdn({installSahpool:!1})}async getOrCreateWorkerPromiser(e){if(this.workerPromiser)return this.workerPromiser;if(this.workerPromiserInitPromise)return await this.workerPromiserInitPromise;let t=await this.loadSQLiteWorkerPromiserFactory(),n=this.sqliteConfig.storageBackend===`opfs`||this.sqliteConfig.opfsVfsType===`opfs`||this.sqliteConfig.opfsVfsType===`sahpool`,r=this.sqliteConfig.loadStrategy===`global`&&!n?5e3:2e4;this.workerPromiserInitPromise=new Promise((n,i)=>{let a=!1,o=setTimeout(()=>{a||(a=!0,i(Error(`SQLite worker initialization timed out after ${r}ms`)))},r),s=(e,t)=>{a||(a=!0,clearTimeout(o),e(t))},c=e=>{s(i,e instanceof Error?e:Error(String(e)))},l;try{l=t({...e?.worker?{worker:e.worker}:{},onready:e=>{if(typeof e==`function`){s(n,e);return}if(typeof l==`function`){s(n,l);return}if(l&&typeof l.then==`function`){l.then(e=>s(n,e),e=>c(e));return}c(Error(`sqlite3Worker1Promiser did not provide a usable promiser`))},onerror:e=>{let t=e instanceof Error?e.message:String(e);c(Error(`SQLite worker error: ${t}`))}}),l&&typeof l.then==`function`&&l.then(e=>s(n,e),e=>c(e))}catch(e){c(e)}});let i=await this.workerPromiserInitPromise;return this.workerPromiser=i,i}getTargetOrigin(){let e=globalThis.location?.origin;return typeof e==`string`?e:`*`}async loadFromNpm(){try{let e=await import(`@sqlite.org/sqlite-wasm`);return e.default||e}catch(e){return console.warn(`Failed to load SQLite WASM from npm, falling back to CDN:`,e),this.loadFromCdn(this.sqliteConfig.cdnBaseUrl,this.sqliteConfig.version,this.sqliteConfig.bundlerFriendly)}}async loadFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/${r}`,`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM from: ${e}`);let t=await import(e);return console.log(`Successfully loaded SQLite WASM from: ${e}`),t.default||t}catch(t){a=t,console.warn(`Failed to load SQLite WASM from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load SQLite WASM from any CDN source. Last error: ${o}`)}async loadFromUrl(e){let t=await import(e);return t.default||t.sqlite3InitModule}async loadAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=await import(e);return t.default||t.sqlite3InitModule}catch(t){console.warn(`Failed to load SQLite WASM from ${e}:`,t)}throw Error(`Could not load SQLite WASM as ES6 module from any known path`)}loadFromGlobal(){if(typeof window<`u`){let e=window;if(e.sqlite3InitModule)return e.sqlite3InitModule}throw Error(`SQLite WASM module not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM script in your HTML before using UniSqlite`)}async initialize(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.initializeLeaderElection(),await this.initPromise,this.initialized=!0}}async initializeLeaderElection(){this.elector=(0,n.createLeaderElection)(this.channel),console.log(`Starting leader election for channel:`,`unisqlite-${this.options.path}`);let e=await this.elector.hasLeader();console.log(`Has existing leader:`,e),e?(this.isLeader=!1,console.log(`Leader exists, becoming follower`),this.elector.awaitLeadership().then(async()=>{console.log(`Became leader after previous leader died`),this.isLeader=!0,await this.initializeDatabase()}).catch(e=>{console.error(`Error while awaiting leadership:`,e)})):(console.log(`No leader exists, attempting to become leader...`),await this.elector.awaitLeadership(),this.isLeader=!0,console.log(`Became leader!`),await this.initializeDatabase()),this.channel.addEventListener(`message`,e=>{let t=a(e)?e.data:e;if(console.log(`BroadcastChannel message`,t),this.isLeader&&t?.id&&t.type&&[`query`,`queryRaw`,`run`,`exec`,`transaction`].includes(t.type)){this.handleFollowerRequest(t);return}if(t.id&&this.pendingRequests.has(t.id)&&(t.result!==void 0||t.error)){let{resolve:e,reject:n}=this.pendingRequests.get(t.id);this.pendingRequests.delete(t.id),t.error?n(Error(t.error)):e(t.result)}})}async initializeDatabase(){console.log(`Loading SQLite WASM using strategy: ${this.sqliteConfig.loadStrategy}`);let e=await this.loadSQLiteWasm(),t={print:console.log,printErr:console.error};if(this.sqliteConfig.locateFile)t.locateFile=this.sqliteConfig.locateFile;else if(typeof window<`u`){let e=window;e.sqlite3InitModuleState?.locateFile&&(t.locateFile=e.sqlite3InitModuleState.locateFile)}if(console.log(`Initializing SQLite WASM module...`),this.sqlite3=await e(t),!this.sqlite3)throw Error(`Failed to initialize SQLite WASM module`);let n=this.sqlite3;console.log(`SQLite WASM module initialized successfully`);let r=this.sqliteConfig.storageBackend??`auto`,i=this.options.path===`:memory:`?`:memory:`:`/unisqlite/${this.options.path}`;r===`memory`||this.options.path===`:memory:`?(console.log(`Using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`):await this.tryCreatePersistentDatabase(n,i,r)||(console.log(`All persistent storage backends failed, using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`);try{await this.execInternal(`PRAGMA journal_mode=WAL`),console.log(`Enabled WAL mode`)}catch(e){console.warn(`Could not enable WAL mode:`,e)}}async tryCreatePersistentDatabase(e,t,n){if(n===`opfs`||n===`auto`){let r=this.sqliteConfig.opfsVfsType??`auto`,i=r===`auto`?e.oo1.OpfsDb?[`opfs`]:[`opfs-sahpool`,`opfs`]:r===`sahpool`?[`opfs-sahpool`]:[`opfs`];for(let n of i)if(await this.tryCreateOpfsDatabase(e,t,n))return!0;if(n===`opfs`)throw Error(this.getOpfsUnavailableError())}if(n===`localStorage`||n===`auto`){if(e.oo1.JsStorageDb)if(this.options.path===`local`||this.options.path===`session`)try{return console.log(`Creating localStorage-backed database:`,this.options.path),this.db=new e.oo1.JsStorageDb(this.options.path),this.activeStorageBackend=`localStorage`,console.log(`Successfully created localStorage-backed database`),!0}catch(e){if(console.warn(`localStorage database creation failed:`,e),n===`localStorage`)throw Error(`localStorage database creation failed: ${e instanceof Error?e.message:String(e)}`)}else if(n===`localStorage`)throw Error(`localStorage storage backend requires path to be 'local' or 'session', got '${this.options.path}'. Use storageBackend: 'opfs' for custom database names with persistence.`);else console.log(`Skipping localStorage: path '${this.options.path}' is not 'local' or 'session'`);else if(n===`localStorage`)throw Error(`JsStorageDb is not available in this environment.`)}return!1}getOpfsUnavailableError(){return`OPFS is not available. | ||
| SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood. | ||
| Make sure: | ||
| - You are in a secure context (HTTPS or localhost) | ||
| - For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available | ||
| - Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)`}async tryCreateOpfsDatabase(e,t,n){if(n===`opfs`){if(e.oo1.OpfsDb)try{return console.log(`Creating OPFS-backed database (OpfsDb):`,t),this.db=new e.oo1.OpfsDb(t),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs`,console.log(`Successfully created OPFS-backed database (OpfsDb)`),!0}catch(e){console.warn(`OPFS database creation via OpfsDb failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}if(await this.ensureSahpoolVfs(e))try{return console.log(`Creating SAHPool OPFS-backed database:`,t),this.db=new e.oo1.DB(t,`c`,`opfs-sahpool`),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs-sahpool`,console.log(`Successfully created SAHPool OPFS-backed database`),!0}catch(e){console.warn(`SAHPool OPFS database creation failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}async ensureSahpoolVfs(e){if(globalThis.importScripts===void 0)return!1;let t=`opfs-sahpool`;try{if(e.capi.sqlite3_vfs_find?.(t))return!0}catch(e){console.warn(`SAHPool VFS lookup failed:`,e)}if(typeof e.installOpfsSAHPoolVfs!=`function`)return!1;try{await e.installOpfsSAHPoolVfs({name:t,directory:`/unisqlite-sahpool`})}catch(e){return console.warn(`SAHPool VFS installation failed:`,e),!1}try{return!!e.capi.sqlite3_vfs_find?.(t)}catch{return!0}}async tryCreateWrappedWorkerOpfsDatabase(e,t){try{let n=!this.workerPromiser&&!this.workerPromiserInitPromise?(()=>{let e=t===`opfs-sahpool`?this.createSahpoolWorker():this.createCdnWorker();return e?{worker:e}:void 0})():void 0,r=await(await this.getOrCreateWorkerPromiser(n))(`open`,{filename:`file:${e}`,vfs:t}),i=r.dbId??r.result?.dbId;if(i===void 0)throw Error(`SQLite worker did not return a dbId`);if(typeof i!=`number`&&typeof i!=`string`)throw Error(`SQLite worker returned an invalid dbId (type=${typeof i})`);return this.db=void 0,this.workerDbId=i,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=t,console.log(`Successfully created OPFS-backed database via wrapped worker (${t}):`,e),!0}catch(e){return console.warn(`Wrapped-worker OPFS database creation failed (vfs=${t}):`,e),!1}}normalizeParams(e){if(!e)return;if(Array.isArray(e))return e;let t=Object.keys(e),n=e;return t.map(e=>n[e])}async execInternal(e,t){if(this.isWorkerDbActive()){let n=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);await this.workerPromiser(`exec`,{dbId:this.workerDbId,sql:e})};t?.bypassQueue&&this.workerTxnDepth>0?await n():await this.enqueueWorker(n);return}if(!this.db)throw Error(`Database not initialized`);this.db.exec(e)}async executeSql(e,t,n){try{if(this.isWorkerDbActive()){let r=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);let n=this.normalizeParams(t),r={dbId:this.workerDbId,sql:e,rowMode:`object`,resultRows:[],columnNames:[],countChanges:!0};n&&(r.bind=n);let i=await this.workerPromiser(`exec`,r),a=i.result??i,o=Array.isArray(a.resultRows)?a.resultRows:[],s=Array.isArray(a.columnNames)?a.columnNames:[],c=typeof a.changeCount==`number`?a.changeCount:0,l={dbId:this.workerDbId,sql:`SELECT last_insert_rowid() AS lastInsertRowId`,rowMode:`object`,resultRows:[],columnNames:[]},u=await this.workerPromiser(`exec`,l),d=u.result??u,f=Array.isArray(d.resultRows)?d.resultRows:[];return{rows:o,columns:s,rowsAffected:c,lastInsertRowId:Number(f[0]?.lastInsertRowId??0)}};return n?.bypassQueue&&this.workerTxnDepth>0?await r():await this.enqueueWorker(r)}if(!this.db||!this.sqlite3)throw Error(`Database not initialized`);let r=this.normalizeParams(t),i=[],a=[],o=this.db.prepare(e),s=o;r&&o.bind(r);let c=typeof s.getColumnCount==`function`?s.getColumnCount():typeof s.columnCount==`number`?s.columnCount:0;if(c>0)if(typeof s.getColumnNames==`function`)a=s.getColumnNames();else for(let e=0;e<c;e++)a.push(o.getColumnName(e));for(;o.step();){let e={};for(let t=0;t<c;t++)e[a[t]]=o.get(t);i.push(e)}return o.finalize(),{rows:i,columns:a,rowsAffected:this.db.changes(),lastInsertRowId:Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)||0)}}catch(e){let t=e instanceof Error?e.message:String(e),n=Error(`SQLite error: ${t}`);throw e instanceof Error&&(n.cause=e),n}}async handleFollowerRequest(e){try{console.log(`Handling follower request`,e.type,e.id);let t;switch(e.type){case`query`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for query request`);t=(await this.executeSql(e.sql,e.params)).rows;break;case`queryRaw`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for queryRaw request`);t=await this.executeSql(e.sql,e.params);break;case`run`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for run request`);let n=await this.executeSql(e.sql,e.params);t={rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0};break;case`exec`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for exec request`);await this.execInternal(e.sql),t=null;break;case`transaction`:await this.execInternal(`BEGIN`);try{await this.execInternal(`COMMIT`),t=null}catch(e){throw await this.execInternal(`ROLLBACK`),e}break}await this.channel.postMessage({id:e.id,type:`response`,result:t},this.getTargetOrigin())}catch(t){let n=t instanceof Error?t.message:String(t);await this.channel.postMessage({id:e.id,type:`response`,error:n},this.getTargetOrigin())}}async query(e,t){return await this.initialize(),this.isLeader?(await this.executeSql(e,t)).rows:await this.sendToLeader(`query`,e,t)}async queryRaw(e,t){return await this.initialize(),this.isLeader?await this.executeSql(e,t):await this.sendToLeader(`queryRaw`,e,t)}async run(e,t){if(await this.initialize(),this.isLeader){let n=await this.executeSql(e,t);return{rowsAffected:n.rowsAffected,lastInsertRowId:n.lastInsertRowId}}else return await this.sendToLeader(`run`,e,t)}async exec(e){await this.initialize(),this.isLeader?await this.execInternal(e):await this.sendToLeader(`exec`,e)}async sendToLeader(e,t,n){return new Promise((r,i)=>{let a=crypto.randomUUID();this.pendingRequests.set(a,{resolve:r,reject:i}),console.log(`Sending to leader`,{id:a,type:e,sql:t,params:n}),this.channel.postMessage({id:a,type:e,sql:t,params:n},this.getTargetOrigin()),setTimeout(()=>{this.pendingRequests.has(a)&&(this.pendingRequests.delete(a),i(Error(`Request timeout`)))},5e3)})}getDatabase(){return this.db}async executeSQL(e,t,n){return await this.executeSql(e,t,n)}async transaction(e){if(await this.initialize(),!this.isLeader)throw Error(`Transactions must be executed on leader tab`);if(this.isWorkerDbActive())return await this.enqueueWorker(async()=>{this.workerTxnDepth++;try{await this.execInternal(`BEGIN`,{bypassQueue:!0});let t=e(new s(this,!1)),n=t instanceof Promise?await t:t;return await this.execInternal(`COMMIT`,{bypassQueue:!0}),n}catch(e){try{await this.execInternal(`ROLLBACK`,{bypassQueue:!0})}catch(e){console.error(`Rollback failed:`,e)}throw e}finally{this.workerTxnDepth--}});if(!this.db)throw Error(`Database not initialized`);this.db.exec(`BEGIN`);try{let t=e(new s(this,!1)),n=t instanceof Promise?await t:t;return this.db.exec(`COMMIT`),n}catch(e){throw this.db.exec(`ROLLBACK`),e}}async asyncTransaction(e,t){if(await this.initialize(),this.isLeader){let n=t?.timeoutMs??3e4;if(this.isWorkerDbActive())return await this.enqueueWorker(async()=>{this.workerTxnDepth++;let t,r=!1,i=!1;try{await this.execInternal(`BEGIN IMMEDIATE`,{bypassQueue:!0}),i=!0;let a=new Promise((e,i)=>{t=setTimeout(()=>{r||i(Error(`Async transaction timeout after ${n}ms`))},n)}),o=new s(this,!0),c=await Promise.race([e(o),a]);return r=!0,t&&clearTimeout(t),await this.execInternal(`COMMIT`,{bypassQueue:!0}),c}catch(e){r=!0,t&&clearTimeout(t);try{i&&await this.execInternal(`ROLLBACK`,{bypassQueue:!0})}catch(e){console.error(`Rollback failed:`,e)}throw e}finally{this.workerTxnDepth--}});if(!this.db)throw Error(`Database not initialized`);this.db.exec(`BEGIN IMMEDIATE`);let r,i=!1;try{let t=new Promise((e,t)=>{r=setTimeout(()=>{i||t(Error(`Async transaction timeout after ${n}ms`))},n)}),a=new s(this,!0),o=await Promise.race([e(a),t]);return i=!0,r&&clearTimeout(r),this.db.exec(`COMMIT`),o}catch(e){i=!0,r&&clearTimeout(r);try{this.db.exec(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}else throw Error(`Async transactions must be executed on leader tab`)}getConnectionType(){return`direct`}async close(){if(await this.initialize(),this.workerPromiser){let e=this.workerPromiser,t=this.workerDbId;try{t!==void 0&&await this.enqueueWorker(async()=>{await e(`close`,{dbId:t})})}catch(e){console.warn(`Failed to close SQLite worker database:`,e)}finally{this.workerDbId=void 0;let t=e.worker;t?.terminate&&t.terminate(),this.workerPromiser=void 0}}this.db&&=(this.db.close(),void 0),this.elector&&await this.elector.die(),await this.channel.close()}getSQLiteInfo(){return{config:this.sqliteConfig,isInitialized:this.initialized,isLeader:this.isLeader,hasDatabase:!!this.db||this.workerDbId!==void 0,hasSQLite3:!!this.sqlite3,usesWorker:this.isWorkerDbActive(),activeStorageBackend:this.activeStorageBackend,activeOpfsVfs:this.activeOpfsVfs}}getStorageBackend(){return this.activeStorageBackend}},s=class{constructor(e,t){this.parent=e,this.isAsync=t}async query(e,t){return(await this.parent.executeSQL(e,t,{bypassQueue:!0})).rows}async queryRaw(e,t){return await this.parent.executeSQL(e,t,{bypassQueue:!0})}async run(e,t){let n=await this.parent.executeSQL(e,t,{bypassQueue:!0});return{rowsAffected:n.rowsAffected||0,lastInsertRowId:n.lastInsertRowId||0}}async exec(e){await this.parent.execInternal(e,{bypassQueue:!0})}getConnectionType(){return`syncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e){if(this.isAsync)return await e(this);throw Error(`asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.`)}async close(){}};Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return o}}); | ||
| //# sourceMappingURL=browser3.cjs.map | ||
| const e=require(`./browser2.cjs`);exports.BrowserAdapter=e.t; |
+1
-17
@@ -1,17 +0,1 @@ | ||
| import{t as e}from"./base.mjs";import{BroadcastChannel as t,createLeaderElection as n}from"broadcast-channel";const r=new Map,i=new Map;function a(e){return typeof e==`object`&&!!e&&`data`in e}var o=class o extends e{constructor(e){let n=e.path??e.name??`default`;super({...e,path:n}),this.isLeader=!1,this.workerQueue=Promise.resolve(),this.workerTxnDepth=0,this.pendingRequests=new Map,this.initialized=!1,this.activeStorageBackend=`memory`,this.options={...e,path:n},this.channel=new t(`unisqlite-${n}`),this.sqliteConfig={loadStrategy:`global`,storageBackend:`auto`,opfsVfsType:`auto`,cdnBaseUrl:`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm`,version:`3.50.1-build1`,bundlerFriendly:!1,...e.sqlite}}static async create(e){let t=new o(e);return await t.initialize(),t}async loadSQLiteWasm(){let e=`${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl||this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`;if(r.has(e))return r.get(e);let t=this.loadSQLiteWasmInternal();return r.set(e,t),t}async loadSQLiteWasmInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.sqliteConfig;switch(e){case`npm`:return await this.loadFromNpm();case`cdn`:return await this.loadFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadFromUrl(t);case`module`:return await this.loadAsModule();case`global`:default:return this.loadFromGlobal()}}enqueueWorker(e){let t=async()=>e(),n=this.workerQueue.then(t,t);return this.workerQueue=n.then(()=>void 0,()=>void 0),n}isWorkerDbActive(){return!!this.workerPromiser&&this.workerDbId!==void 0}async loadSQLiteWorkerPromiserFactory(){let e=`worker-${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl||this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`;if(i.has(e))return i.get(e);let t=this.loadSQLiteWorkerPromiserFactoryInternal();return i.set(e,t),t}async loadSQLiteWorkerPromiserFactoryInternal(){let{loadStrategy:e,wasmUrl:t,cdnBaseUrl:n,version:r,bundlerFriendly:i}=this.sqliteConfig;switch(e){case`npm`:return await this.loadWorkerPromiserFromNpm();case`cdn`:return await this.loadWorkerPromiserFromCdn(n,r,i);case`url`:if(!t)throw Error(`wasmUrl must be provided when using 'url' loading strategy`);return await this.loadWorkerPromiserFromUrl(t);case`module`:return await this.loadWorkerPromiserAsModule();case`global`:default:return this.loadWorkerPromiserFromGlobal()}}async loadWorkerPromiserFromNpm(){try{let e=(await import(`@sqlite.org/sqlite-wasm`)).sqlite3Worker1Promiser;if(typeof e==`function`)return e;throw Error(`sqlite3Worker1Promiser export not found`)}catch(e){return console.warn(`Failed to load SQLite WASM worker promiser from npm, falling back to CDN:`,e),this.loadWorkerPromiserFromCdn(this.sqliteConfig.cdnBaseUrl,this.sqliteConfig.version,this.sqliteConfig.bundlerFriendly)}}async loadWorkerPromiserFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`${e}@${t}/${r}`,`${e}@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM worker promiser from: ${e}`);let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return console.log(`Successfully loaded SQLite WASM worker promiser from: ${e}`),t;throw Error(`sqlite3Worker1Promiser export not found`)}catch(t){a=t,console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${o}`)}async loadWorkerPromiserFromUrl(e){let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t!=`function`)throw Error(`sqlite3Worker1Promiser export not found in module loaded from ${e}`);return t}async loadWorkerPromiserAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=(await import(e)).sqlite3Worker1Promiser;if(typeof t==`function`)return t}catch(t){console.warn(`Failed to load SQLite WASM worker promiser from ${e}:`,t)}throw Error(`Could not load sqlite3Worker1Promiser as ES6 module from any known path`)}loadWorkerPromiserFromGlobal(){let e=globalThis;if(typeof e.sqlite3Worker1Promiser==`function`)return e.sqlite3Worker1Promiser;throw Error(`SQLite WASM worker promiser not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm' | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite`)}createWorkerFromCdn(e){if(typeof Worker>`u`||typeof Blob>`u`||typeof URL>`u`)return;let t=this.sqliteConfig.cdnBaseUrl,n=this.sqliteConfig.version;if(!t||!n)return;let r=`${`${t}@${n}/sqlite-wasm/jswasm`}/sqlite3-bundler-friendly.mjs`,i=e.installSahpool?[` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`,` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`,` }`].join(` | ||
| `):``,a=[`import sqlite3InitModule from ${JSON.stringify(r)};`,`sqlite3InitModule().then(async (sqlite3) => {`,` try {`,i||` // no-op`,` } catch (e) {`,` console.warn("SAHPool VFS installation failed:", e);`,` }`,` sqlite3.initWorker1API();`,`});`,``].join(` | ||
| `),o=new Blob([a],{type:`text/javascript`}),s=URL.createObjectURL(o),c=new Worker(s,{type:`module`});return c.addEventListener(`message`,()=>{URL.revokeObjectURL(s)},{once:!0}),c.addEventListener(`error`,()=>{URL.revokeObjectURL(s)},{once:!0}),c}createSahpoolWorker(){if(this.sqliteConfig.loadStrategy===`cdn`)return this.createWorkerFromCdn({installSahpool:!0})}createCdnWorker(){if(this.sqliteConfig.loadStrategy===`cdn`)return this.createWorkerFromCdn({installSahpool:!1})}async getOrCreateWorkerPromiser(e){if(this.workerPromiser)return this.workerPromiser;if(this.workerPromiserInitPromise)return await this.workerPromiserInitPromise;let t=await this.loadSQLiteWorkerPromiserFactory(),n=this.sqliteConfig.storageBackend===`opfs`||this.sqliteConfig.opfsVfsType===`opfs`||this.sqliteConfig.opfsVfsType===`sahpool`,r=this.sqliteConfig.loadStrategy===`global`&&!n?5e3:2e4;this.workerPromiserInitPromise=new Promise((n,i)=>{let a=!1,o=setTimeout(()=>{a||(a=!0,i(Error(`SQLite worker initialization timed out after ${r}ms`)))},r),s=(e,t)=>{a||(a=!0,clearTimeout(o),e(t))},c=e=>{s(i,e instanceof Error?e:Error(String(e)))},l;try{l=t({...e?.worker?{worker:e.worker}:{},onready:e=>{if(typeof e==`function`){s(n,e);return}if(typeof l==`function`){s(n,l);return}if(l&&typeof l.then==`function`){l.then(e=>s(n,e),e=>c(e));return}c(Error(`sqlite3Worker1Promiser did not provide a usable promiser`))},onerror:e=>{let t=e instanceof Error?e.message:String(e);c(Error(`SQLite worker error: ${t}`))}}),l&&typeof l.then==`function`&&l.then(e=>s(n,e),e=>c(e))}catch(e){c(e)}});let i=await this.workerPromiserInitPromise;return this.workerPromiser=i,i}getTargetOrigin(){let e=globalThis.location?.origin;return typeof e==`string`?e:`*`}async loadFromNpm(){try{let e=await import(`@sqlite.org/sqlite-wasm`);return e.default||e}catch(e){return console.warn(`Failed to load SQLite WASM from npm, falling back to CDN:`,e),this.loadFromCdn(this.sqliteConfig.cdnBaseUrl,this.sqliteConfig.version,this.sqliteConfig.bundlerFriendly)}}async loadFromCdn(e,t,n){let r=n?`sqlite3-bundler-friendly.mjs`:`index.mjs`,i=[`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/${r}`,`https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${t}/dist/${r}`],a;for(let e of i)try{console.log(`Trying to load SQLite WASM from: ${e}`);let t=await import(e);return console.log(`Successfully loaded SQLite WASM from: ${e}`),t.default||t}catch(t){a=t,console.warn(`Failed to load SQLite WASM from ${e}:`,t)}let o=a instanceof Error?a.message:String(a);throw Error(`Failed to load SQLite WASM from any CDN source. Last error: ${o}`)}async loadFromUrl(e){let t=await import(e);return t.default||t.sqlite3InitModule}async loadAsModule(){if(globalThis.importScripts!==void 0)throw Error(`ES6 module loading not supported in Web Worker context`);for(let e of[`./sqlite3.mjs`,`./sqlite3-bundler-friendly.mjs`,`/sqlite3.mjs`])try{let t=await import(e);return t.default||t.sqlite3InitModule}catch(t){console.warn(`Failed to load SQLite WASM from ${e}:`,t)}throw Error(`Could not load SQLite WASM as ES6 module from any known path`)}loadFromGlobal(){if(typeof window<`u`){let e=window;if(e.sqlite3InitModule)return e.sqlite3InitModule}throw Error(`SQLite WASM module not found globally. Please: | ||
| 1. Install via npm: npm install @sqlite.org/sqlite-wasm | ||
| 2. Or set loadStrategy to 'cdn' for automatic CDN loading | ||
| 3. Or include SQLite WASM script in your HTML before using UniSqlite`)}async initialize(){if(!this.initialized){if(this.initPromise)return this.initPromise;this.initPromise=this.initializeLeaderElection(),await this.initPromise,this.initialized=!0}}async initializeLeaderElection(){this.elector=n(this.channel),console.log(`Starting leader election for channel:`,`unisqlite-${this.options.path}`);let e=await this.elector.hasLeader();console.log(`Has existing leader:`,e),e?(this.isLeader=!1,console.log(`Leader exists, becoming follower`),this.elector.awaitLeadership().then(async()=>{console.log(`Became leader after previous leader died`),this.isLeader=!0,await this.initializeDatabase()}).catch(e=>{console.error(`Error while awaiting leadership:`,e)})):(console.log(`No leader exists, attempting to become leader...`),await this.elector.awaitLeadership(),this.isLeader=!0,console.log(`Became leader!`),await this.initializeDatabase()),this.channel.addEventListener(`message`,e=>{let t=a(e)?e.data:e;if(console.log(`BroadcastChannel message`,t),this.isLeader&&t?.id&&t.type&&[`query`,`queryRaw`,`run`,`exec`,`transaction`].includes(t.type)){this.handleFollowerRequest(t);return}if(t.id&&this.pendingRequests.has(t.id)&&(t.result!==void 0||t.error)){let{resolve:e,reject:n}=this.pendingRequests.get(t.id);this.pendingRequests.delete(t.id),t.error?n(Error(t.error)):e(t.result)}})}async initializeDatabase(){console.log(`Loading SQLite WASM using strategy: ${this.sqliteConfig.loadStrategy}`);let e=await this.loadSQLiteWasm(),t={print:console.log,printErr:console.error};if(this.sqliteConfig.locateFile)t.locateFile=this.sqliteConfig.locateFile;else if(typeof window<`u`){let e=window;e.sqlite3InitModuleState?.locateFile&&(t.locateFile=e.sqlite3InitModuleState.locateFile)}if(console.log(`Initializing SQLite WASM module...`),this.sqlite3=await e(t),!this.sqlite3)throw Error(`Failed to initialize SQLite WASM module`);let n=this.sqlite3;console.log(`SQLite WASM module initialized successfully`);let r=this.sqliteConfig.storageBackend??`auto`,i=this.options.path===`:memory:`?`:memory:`:`/unisqlite/${this.options.path}`;r===`memory`||this.options.path===`:memory:`?(console.log(`Using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`):await this.tryCreatePersistentDatabase(n,i,r)||(console.log(`All persistent storage backends failed, using in-memory database`),this.db=new n.oo1.DB(`:memory:`),this.activeStorageBackend=`memory`);try{await this.execInternal(`PRAGMA journal_mode=WAL`),console.log(`Enabled WAL mode`)}catch(e){console.warn(`Could not enable WAL mode:`,e)}}async tryCreatePersistentDatabase(e,t,n){if(n===`opfs`||n===`auto`){let r=this.sqliteConfig.opfsVfsType??`auto`,i=r===`auto`?e.oo1.OpfsDb?[`opfs`]:[`opfs-sahpool`,`opfs`]:r===`sahpool`?[`opfs-sahpool`]:[`opfs`];for(let n of i)if(await this.tryCreateOpfsDatabase(e,t,n))return!0;if(n===`opfs`)throw Error(this.getOpfsUnavailableError())}if(n===`localStorage`||n===`auto`){if(e.oo1.JsStorageDb)if(this.options.path===`local`||this.options.path===`session`)try{return console.log(`Creating localStorage-backed database:`,this.options.path),this.db=new e.oo1.JsStorageDb(this.options.path),this.activeStorageBackend=`localStorage`,console.log(`Successfully created localStorage-backed database`),!0}catch(e){if(console.warn(`localStorage database creation failed:`,e),n===`localStorage`)throw Error(`localStorage database creation failed: ${e instanceof Error?e.message:String(e)}`)}else if(n===`localStorage`)throw Error(`localStorage storage backend requires path to be 'local' or 'session', got '${this.options.path}'. Use storageBackend: 'opfs' for custom database names with persistence.`);else console.log(`Skipping localStorage: path '${this.options.path}' is not 'local' or 'session'`);else if(n===`localStorage`)throw Error(`JsStorageDb is not available in this environment.`)}return!1}getOpfsUnavailableError(){return`OPFS is not available. | ||
| SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood. | ||
| Make sure: | ||
| - You are in a secure context (HTTPS or localhost) | ||
| - For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available | ||
| - Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)`}async tryCreateOpfsDatabase(e,t,n){if(n===`opfs`){if(e.oo1.OpfsDb)try{return console.log(`Creating OPFS-backed database (OpfsDb):`,t),this.db=new e.oo1.OpfsDb(t),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs`,console.log(`Successfully created OPFS-backed database (OpfsDb)`),!0}catch(e){console.warn(`OPFS database creation via OpfsDb failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}if(await this.ensureSahpoolVfs(e))try{return console.log(`Creating SAHPool OPFS-backed database:`,t),this.db=new e.oo1.DB(t,`c`,`opfs-sahpool`),this.workerDbId=void 0,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=`opfs-sahpool`,console.log(`Successfully created SAHPool OPFS-backed database`),!0}catch(e){console.warn(`SAHPool OPFS database creation failed:`,e)}return await this.tryCreateWrappedWorkerOpfsDatabase(t,n)}async ensureSahpoolVfs(e){if(globalThis.importScripts===void 0)return!1;let t=`opfs-sahpool`;try{if(e.capi.sqlite3_vfs_find?.(t))return!0}catch(e){console.warn(`SAHPool VFS lookup failed:`,e)}if(typeof e.installOpfsSAHPoolVfs!=`function`)return!1;try{await e.installOpfsSAHPoolVfs({name:t,directory:`/unisqlite-sahpool`})}catch(e){return console.warn(`SAHPool VFS installation failed:`,e),!1}try{return!!e.capi.sqlite3_vfs_find?.(t)}catch{return!0}}async tryCreateWrappedWorkerOpfsDatabase(e,t){try{let n=!this.workerPromiser&&!this.workerPromiserInitPromise?(()=>{let e=t===`opfs-sahpool`?this.createSahpoolWorker():this.createCdnWorker();return e?{worker:e}:void 0})():void 0,r=await(await this.getOrCreateWorkerPromiser(n))(`open`,{filename:`file:${e}`,vfs:t}),i=r.dbId??r.result?.dbId;if(i===void 0)throw Error(`SQLite worker did not return a dbId`);if(typeof i!=`number`&&typeof i!=`string`)throw Error(`SQLite worker returned an invalid dbId (type=${typeof i})`);return this.db=void 0,this.workerDbId=i,this.activeStorageBackend=`opfs`,this.activeOpfsVfs=t,console.log(`Successfully created OPFS-backed database via wrapped worker (${t}):`,e),!0}catch(e){return console.warn(`Wrapped-worker OPFS database creation failed (vfs=${t}):`,e),!1}}normalizeParams(e){if(!e)return;if(Array.isArray(e))return e;let t=Object.keys(e),n=e;return t.map(e=>n[e])}async execInternal(e,t){if(this.isWorkerDbActive()){let n=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);await this.workerPromiser(`exec`,{dbId:this.workerDbId,sql:e})};t?.bypassQueue&&this.workerTxnDepth>0?await n():await this.enqueueWorker(n);return}if(!this.db)throw Error(`Database not initialized`);this.db.exec(e)}async executeSql(e,t,n){try{if(this.isWorkerDbActive()){let r=async()=>{if(!this.workerPromiser||this.workerDbId===void 0)throw Error(`Database not initialized`);let n=this.normalizeParams(t),r={dbId:this.workerDbId,sql:e,rowMode:`object`,resultRows:[],columnNames:[],countChanges:!0};n&&(r.bind=n);let i=await this.workerPromiser(`exec`,r),a=i.result??i,o=Array.isArray(a.resultRows)?a.resultRows:[],s=Array.isArray(a.columnNames)?a.columnNames:[],c=typeof a.changeCount==`number`?a.changeCount:0,l={dbId:this.workerDbId,sql:`SELECT last_insert_rowid() AS lastInsertRowId`,rowMode:`object`,resultRows:[],columnNames:[]},u=await this.workerPromiser(`exec`,l),d=u.result??u,f=Array.isArray(d.resultRows)?d.resultRows:[];return{rows:o,columns:s,rowsAffected:c,lastInsertRowId:Number(f[0]?.lastInsertRowId??0)}};return n?.bypassQueue&&this.workerTxnDepth>0?await r():await this.enqueueWorker(r)}if(!this.db||!this.sqlite3)throw Error(`Database not initialized`);let r=this.normalizeParams(t),i=[],a=[],o=this.db.prepare(e),s=o;r&&o.bind(r);let c=typeof s.getColumnCount==`function`?s.getColumnCount():typeof s.columnCount==`number`?s.columnCount:0;if(c>0)if(typeof s.getColumnNames==`function`)a=s.getColumnNames();else for(let e=0;e<c;e++)a.push(o.getColumnName(e));for(;o.step();){let e={};for(let t=0;t<c;t++)e[a[t]]=o.get(t);i.push(e)}return o.finalize(),{rows:i,columns:a,rowsAffected:this.db.changes(),lastInsertRowId:Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)||0)}}catch(e){let t=e instanceof Error?e.message:String(e),n=Error(`SQLite error: ${t}`);throw e instanceof Error&&(n.cause=e),n}}async handleFollowerRequest(e){try{console.log(`Handling follower request`,e.type,e.id);let t;switch(e.type){case`query`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for query request`);t=(await this.executeSql(e.sql,e.params)).rows;break;case`queryRaw`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for queryRaw request`);t=await this.executeSql(e.sql,e.params);break;case`run`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for run request`);let n=await this.executeSql(e.sql,e.params);t={rowsAffected:n.rowsAffected??0,lastInsertRowId:n.lastInsertRowId??0};break;case`exec`:if(typeof e.sql!=`string`)throw Error(`Missing SQL for exec request`);await this.execInternal(e.sql),t=null;break;case`transaction`:await this.execInternal(`BEGIN`);try{await this.execInternal(`COMMIT`),t=null}catch(e){throw await this.execInternal(`ROLLBACK`),e}break}await this.channel.postMessage({id:e.id,type:`response`,result:t},this.getTargetOrigin())}catch(t){let n=t instanceof Error?t.message:String(t);await this.channel.postMessage({id:e.id,type:`response`,error:n},this.getTargetOrigin())}}async query(e,t){return await this.initialize(),this.isLeader?(await this.executeSql(e,t)).rows:await this.sendToLeader(`query`,e,t)}async queryRaw(e,t){return await this.initialize(),this.isLeader?await this.executeSql(e,t):await this.sendToLeader(`queryRaw`,e,t)}async run(e,t){if(await this.initialize(),this.isLeader){let n=await this.executeSql(e,t);return{rowsAffected:n.rowsAffected,lastInsertRowId:n.lastInsertRowId}}else return await this.sendToLeader(`run`,e,t)}async exec(e){await this.initialize(),this.isLeader?await this.execInternal(e):await this.sendToLeader(`exec`,e)}async sendToLeader(e,t,n){return new Promise((r,i)=>{let a=crypto.randomUUID();this.pendingRequests.set(a,{resolve:r,reject:i}),console.log(`Sending to leader`,{id:a,type:e,sql:t,params:n}),this.channel.postMessage({id:a,type:e,sql:t,params:n},this.getTargetOrigin()),setTimeout(()=>{this.pendingRequests.has(a)&&(this.pendingRequests.delete(a),i(Error(`Request timeout`)))},5e3)})}getDatabase(){return this.db}async executeSQL(e,t,n){return await this.executeSql(e,t,n)}async transaction(e){if(await this.initialize(),!this.isLeader)throw Error(`Transactions must be executed on leader tab`);if(this.isWorkerDbActive())return await this.enqueueWorker(async()=>{this.workerTxnDepth++;try{await this.execInternal(`BEGIN`,{bypassQueue:!0});let t=e(new s(this,!1)),n=t instanceof Promise?await t:t;return await this.execInternal(`COMMIT`,{bypassQueue:!0}),n}catch(e){try{await this.execInternal(`ROLLBACK`,{bypassQueue:!0})}catch(e){console.error(`Rollback failed:`,e)}throw e}finally{this.workerTxnDepth--}});if(!this.db)throw Error(`Database not initialized`);this.db.exec(`BEGIN`);try{let t=e(new s(this,!1)),n=t instanceof Promise?await t:t;return this.db.exec(`COMMIT`),n}catch(e){throw this.db.exec(`ROLLBACK`),e}}async asyncTransaction(e,t){if(await this.initialize(),this.isLeader){let n=t?.timeoutMs??3e4;if(this.isWorkerDbActive())return await this.enqueueWorker(async()=>{this.workerTxnDepth++;let t,r=!1,i=!1;try{await this.execInternal(`BEGIN IMMEDIATE`,{bypassQueue:!0}),i=!0;let a=new Promise((e,i)=>{t=setTimeout(()=>{r||i(Error(`Async transaction timeout after ${n}ms`))},n)}),o=new s(this,!0),c=await Promise.race([e(o),a]);return r=!0,t&&clearTimeout(t),await this.execInternal(`COMMIT`,{bypassQueue:!0}),c}catch(e){r=!0,t&&clearTimeout(t);try{i&&await this.execInternal(`ROLLBACK`,{bypassQueue:!0})}catch(e){console.error(`Rollback failed:`,e)}throw e}finally{this.workerTxnDepth--}});if(!this.db)throw Error(`Database not initialized`);this.db.exec(`BEGIN IMMEDIATE`);let r,i=!1;try{let t=new Promise((e,t)=>{r=setTimeout(()=>{i||t(Error(`Async transaction timeout after ${n}ms`))},n)}),a=new s(this,!0),o=await Promise.race([e(a),t]);return i=!0,r&&clearTimeout(r),this.db.exec(`COMMIT`),o}catch(e){i=!0,r&&clearTimeout(r);try{this.db.exec(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}else throw Error(`Async transactions must be executed on leader tab`)}getConnectionType(){return`direct`}async close(){if(await this.initialize(),this.workerPromiser){let e=this.workerPromiser,t=this.workerDbId;try{t!==void 0&&await this.enqueueWorker(async()=>{await e(`close`,{dbId:t})})}catch(e){console.warn(`Failed to close SQLite worker database:`,e)}finally{this.workerDbId=void 0;let t=e.worker;t?.terminate&&t.terminate(),this.workerPromiser=void 0}}this.db&&=(this.db.close(),void 0),this.elector&&await this.elector.die(),await this.channel.close()}getSQLiteInfo(){return{config:this.sqliteConfig,isInitialized:this.initialized,isLeader:this.isLeader,hasDatabase:!!this.db||this.workerDbId!==void 0,hasSQLite3:!!this.sqlite3,usesWorker:this.isWorkerDbActive(),activeStorageBackend:this.activeStorageBackend,activeOpfsVfs:this.activeOpfsVfs}}getStorageBackend(){return this.activeStorageBackend}},s=class{constructor(e,t){this.parent=e,this.isAsync=t}async query(e,t){return(await this.parent.executeSQL(e,t,{bypassQueue:!0})).rows}async queryRaw(e,t){return await this.parent.executeSQL(e,t,{bypassQueue:!0})}async run(e,t){let n=await this.parent.executeSQL(e,t,{bypassQueue:!0});return{rowsAffected:n.rowsAffected||0,lastInsertRowId:n.lastInsertRowId||0}}async exec(e){await this.parent.execInternal(e,{bypassQueue:!0})}getConnectionType(){return`syncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e){if(this.isAsync)return await e(this);throw Error(`asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.`)}async close(){}};export{o as t}; | ||
| //# sourceMappingURL=browser3.mjs.map | ||
| import{a as e,c as t,d as n,f as r,i,l as a,m as o,n as s,o as c,p as l,r as u,s as d,t as f,u as p}from"./browser2.mjs";export{f as BrowserAdapter}; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| const e=require(`./cloudflare-do2.cjs`);async function t(e){let t=n();switch(t){case`node`:let{NodeAdapter:n}=await Promise.resolve().then(()=>require(`./node3.cjs`));return new n(e);case`browser`:let{BrowserAdapter:r}=await Promise.resolve().then(()=>require(`./browser2.cjs`));return r.create(e);case`cloudflare`:let{CloudflareDOAdapter:i}=await Promise.resolve().then(()=>require(`./cloudflare-do.cjs`));return new i(e);default:throw Error(`Unsupported platform: ${t}`)}}function n(){if(typeof globalThis<`u`){if(globalThis.process?.versions?.node)return`node`;if(globalThis.navigator?.userAgent===`Cloudflare-Workers`)return`cloudflare`;if(globalThis.navigator?.userAgent)return`browser`}return`unknown`}exports.CloudflareDOAdapter=e.t,exports.createCloudflareDOAdapter=e.n,exports.openStore=t; | ||
| const e=require(`./cloudflare-do2.cjs`);async function t(e){let t=n();switch(t){case`node`:let{NodeAdapter:n}=await Promise.resolve().then(()=>require(`./node2.cjs`));return new n(e);case`browser`:let{BrowserAdapter:r}=await Promise.resolve().then(()=>require(`./browser3.cjs`));return r.create(e);case`cloudflare`:let{CloudflareDOAdapter:i}=await Promise.resolve().then(()=>require(`./cloudflare-do.cjs`));return new i(e);default:throw Error(`Unsupported platform: ${t}`)}}function n(){if(typeof globalThis<`u`){if(globalThis.process?.versions?.node)return`node`;if(globalThis.navigator?.userAgent===`Cloudflare-Workers`)return`cloudflare`;if(globalThis.navigator?.userAgent)return`browser`}return`unknown`}exports.CloudflareDOAdapter=e.t,exports.createCloudflareDOAdapter=e.n,exports.openStore=t; | ||
| //# sourceMappingURL=index.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.cjs","names":["CloudflareDOAdapter"],"sources":["../src/index.ts"],"sourcesContent":["export type {\n ChangeEvent,\n ConnectionType,\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n SQLiteWasmConfig,\n UniStoreConnection,\n UniStoreOptions,\n} from \"./types.js\";\n\nexport type {\n BrowserSQLiteDatabase,\n BrowserSQLiteStatement,\n NodeSQLiteDatabase,\n NodeSQLiteStatement,\n} from \"./platform-types.js\";\n\n// Export the new Cloudflare DO adapter\nexport { CloudflareDOAdapter, createCloudflareDOAdapter } from \"./adapters/cloudflare-do.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"node\":\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n\n case \"browser\":\n const { BrowserAdapter } = await import(\"./adapters/browser.js\");\n return BrowserAdapter.create(options);\n\n case \"cloudflare\":\n const { CloudflareDOAdapter } = await import(\"./adapters/cloudflare-do.js\");\n return new CloudflareDOAdapter(options);\n\n default:\n throw new Error(`Unsupported platform: ${platform}`);\n }\n}\n\ntype Platform = \"node\" | \"browser\" | \"cloudflare\" | \"unknown\";\n\nfunction detectPlatform(): Platform {\n if (typeof globalThis !== \"undefined\") {\n if (globalThis.process?.versions?.node) {\n return \"node\";\n }\n\n if (globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cloudflare\";\n }\n\n if (globalThis.navigator?.userAgent) {\n return \"browser\";\n }\n }\n\n return \"unknown\";\n}\n"],"mappings":"wCAwBA,eAAsB,EAAU,EAAuD,CACrF,IAAM,EAAW,GAAgB,CAEjC,OAAQ,EAAR,CACE,IAAK,OACH,GAAM,CAAE,eAAgB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,cAAA,CAAA,CAC9B,OAAO,IAAI,EAAY,EAAQ,CAEjC,IAAK,UACH,GAAM,CAAE,kBAAmB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,iBAAA,CAAA,CACjC,OAAO,EAAe,OAAO,EAAQ,CAEvC,IAAK,aACH,GAAM,CAAE,oBAAA,GAAwB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,sBAAA,CAAA,CACtC,OAAO,IAAIA,EAAoB,EAAQ,CAEzC,QACE,MAAU,MAAM,yBAAyB,IAAW,EAM1D,SAAS,GAA2B,CAClC,GAAI,OAAO,WAAe,IAAa,CACrC,GAAI,WAAW,SAAS,UAAU,KAChC,MAAO,OAGT,GAAI,WAAW,WAAW,YAAc,qBACtC,MAAO,aAGT,GAAI,WAAW,WAAW,UACxB,MAAO,UAIX,MAAO"} | ||
| {"version":3,"file":"index.cjs","names":["CloudflareDOAdapter"],"sources":["../src/index.ts"],"sourcesContent":["export type {\n ChangeEvent,\n ConnectionType,\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n SQLiteWasmConfig,\n UniStoreConnection,\n UniStoreOptions,\n} from \"./types.js\";\n\nexport type {\n BrowserSQLiteDatabase,\n BrowserSQLiteStatement,\n NodeSQLiteDatabase,\n NodeSQLiteStatement,\n} from \"./platform-types.js\";\n\n// Export the new Cloudflare DO adapter\nexport { CloudflareDOAdapter, createCloudflareDOAdapter } from \"./adapters/cloudflare-do.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"node\":\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n\n case \"browser\":\n const { BrowserAdapter } = await import(\"./adapters/browser/index.js\");\n return BrowserAdapter.create(options);\n\n case \"cloudflare\":\n const { CloudflareDOAdapter } = await import(\"./adapters/cloudflare-do.js\");\n return new CloudflareDOAdapter(options);\n\n default:\n throw new Error(`Unsupported platform: ${platform}`);\n }\n}\n\ntype Platform = \"node\" | \"browser\" | \"cloudflare\" | \"unknown\";\n\nfunction detectPlatform(): Platform {\n if (typeof globalThis !== \"undefined\") {\n if (globalThis.process?.versions?.node) {\n return \"node\";\n }\n\n if (globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cloudflare\";\n }\n\n if (globalThis.navigator?.userAgent) {\n return \"browser\";\n }\n }\n\n return \"unknown\";\n}\n"],"mappings":"wCAwBA,eAAsB,EAAU,EAAuD,CACrF,IAAM,EAAW,GAAgB,CAEjC,OAAQ,EAAR,CACE,IAAK,OACH,GAAM,CAAE,eAAgB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,cAAA,CAAA,CAC9B,OAAO,IAAI,EAAY,EAAQ,CAEjC,IAAK,UACH,GAAM,CAAE,kBAAmB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,iBAAA,CAAA,CACjC,OAAO,EAAe,OAAO,EAAQ,CAEvC,IAAK,aACH,GAAM,CAAE,oBAAA,GAAwB,MAAA,QAAA,SAAA,CAAA,SAAA,QAAM,sBAAA,CAAA,CACtC,OAAO,IAAIA,EAAoB,EAAQ,CAEzC,QACE,MAAU,MAAM,yBAAyB,IAAW,EAM1D,SAAS,GAA2B,CAClC,GAAI,OAAO,WAAe,IAAa,CACrC,GAAI,WAAW,SAAS,UAAU,KAChC,MAAO,OAGT,GAAI,WAAW,WAAW,YAAc,qBACtC,MAAO,aAGT,GAAI,WAAW,WAAW,UACxB,MAAO,UAIX,MAAO"} |
+1
-1
@@ -1,2 +0,2 @@ | ||
| import{n as e,t}from"./cloudflare-do2.mjs";async function n(e){let t=r();switch(t){case`node`:let{NodeAdapter:n}=await import(`./node2.mjs`);return new n(e);case`browser`:let{BrowserAdapter:r}=await import(`./browser2.mjs`);return r.create(e);case`cloudflare`:let{CloudflareDOAdapter:i}=await import(`./cloudflare-do.mjs`);return new i(e);default:throw Error(`Unsupported platform: ${t}`)}}function r(){if(typeof globalThis<`u`){if(globalThis.process?.versions?.node)return`node`;if(globalThis.navigator?.userAgent===`Cloudflare-Workers`)return`cloudflare`;if(globalThis.navigator?.userAgent)return`browser`}return`unknown`}export{t as CloudflareDOAdapter,e as createCloudflareDOAdapter,n as openStore}; | ||
| import{n as e,t}from"./cloudflare-do2.mjs";async function n(e){let t=r();switch(t){case`node`:let{NodeAdapter:n}=await import(`./node2.mjs`);return new n(e);case`browser`:let{BrowserAdapter:r}=await import(`./browser3.mjs`);return r.create(e);case`cloudflare`:let{CloudflareDOAdapter:i}=await import(`./cloudflare-do.mjs`);return new i(e);default:throw Error(`Unsupported platform: ${t}`)}}function r(){if(typeof globalThis<`u`){if(globalThis.process?.versions?.node)return`node`;if(globalThis.navigator?.userAgent===`Cloudflare-Workers`)return`cloudflare`;if(globalThis.navigator?.userAgent)return`browser`}return`unknown`}export{t as CloudflareDOAdapter,e as createCloudflareDOAdapter,n as openStore}; | ||
| //# sourceMappingURL=index.mjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.mjs","names":["CloudflareDOAdapter"],"sources":["../src/index.ts"],"sourcesContent":["export type {\n ChangeEvent,\n ConnectionType,\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n SQLiteWasmConfig,\n UniStoreConnection,\n UniStoreOptions,\n} from \"./types.js\";\n\nexport type {\n BrowserSQLiteDatabase,\n BrowserSQLiteStatement,\n NodeSQLiteDatabase,\n NodeSQLiteStatement,\n} from \"./platform-types.js\";\n\n// Export the new Cloudflare DO adapter\nexport { CloudflareDOAdapter, createCloudflareDOAdapter } from \"./adapters/cloudflare-do.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"node\":\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n\n case \"browser\":\n const { BrowserAdapter } = await import(\"./adapters/browser.js\");\n return BrowserAdapter.create(options);\n\n case \"cloudflare\":\n const { CloudflareDOAdapter } = await import(\"./adapters/cloudflare-do.js\");\n return new CloudflareDOAdapter(options);\n\n default:\n throw new Error(`Unsupported platform: ${platform}`);\n }\n}\n\ntype Platform = \"node\" | \"browser\" | \"cloudflare\" | \"unknown\";\n\nfunction detectPlatform(): Platform {\n if (typeof globalThis !== \"undefined\") {\n if (globalThis.process?.versions?.node) {\n return \"node\";\n }\n\n if (globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cloudflare\";\n }\n\n if (globalThis.navigator?.userAgent) {\n return \"browser\";\n }\n }\n\n return \"unknown\";\n}\n"],"mappings":"2CAwBA,eAAsB,EAAU,EAAuD,CACrF,IAAM,EAAW,GAAgB,CAEjC,OAAQ,EAAR,CACE,IAAK,OACH,GAAM,CAAE,eAAgB,MAAM,OAAO,eACrC,OAAO,IAAI,EAAY,EAAQ,CAEjC,IAAK,UACH,GAAM,CAAE,kBAAmB,MAAM,OAAO,kBACxC,OAAO,EAAe,OAAO,EAAQ,CAEvC,IAAK,aACH,GAAM,CAAE,oBAAA,GAAwB,MAAM,OAAO,uBAC7C,OAAO,IAAIA,EAAoB,EAAQ,CAEzC,QACE,MAAU,MAAM,yBAAyB,IAAW,EAM1D,SAAS,GAA2B,CAClC,GAAI,OAAO,WAAe,IAAa,CACrC,GAAI,WAAW,SAAS,UAAU,KAChC,MAAO,OAGT,GAAI,WAAW,WAAW,YAAc,qBACtC,MAAO,aAGT,GAAI,WAAW,WAAW,UACxB,MAAO,UAIX,MAAO"} | ||
| {"version":3,"file":"index.mjs","names":["CloudflareDOAdapter"],"sources":["../src/index.ts"],"sourcesContent":["export type {\n ChangeEvent,\n ConnectionType,\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n SQLiteWasmConfig,\n UniStoreConnection,\n UniStoreOptions,\n} from \"./types.js\";\n\nexport type {\n BrowserSQLiteDatabase,\n BrowserSQLiteStatement,\n NodeSQLiteDatabase,\n NodeSQLiteStatement,\n} from \"./platform-types.js\";\n\n// Export the new Cloudflare DO adapter\nexport { CloudflareDOAdapter, createCloudflareDOAdapter } from \"./adapters/cloudflare-do.js\";\n\nimport type { UniStoreConnection, UniStoreOptions } from \"./types.js\";\n\nexport async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> {\n const platform = detectPlatform();\n\n switch (platform) {\n case \"node\":\n const { NodeAdapter } = await import(\"./adapters/node.js\");\n return new NodeAdapter(options);\n\n case \"browser\":\n const { BrowserAdapter } = await import(\"./adapters/browser/index.js\");\n return BrowserAdapter.create(options);\n\n case \"cloudflare\":\n const { CloudflareDOAdapter } = await import(\"./adapters/cloudflare-do.js\");\n return new CloudflareDOAdapter(options);\n\n default:\n throw new Error(`Unsupported platform: ${platform}`);\n }\n}\n\ntype Platform = \"node\" | \"browser\" | \"cloudflare\" | \"unknown\";\n\nfunction detectPlatform(): Platform {\n if (typeof globalThis !== \"undefined\") {\n if (globalThis.process?.versions?.node) {\n return \"node\";\n }\n\n if (globalThis.navigator?.userAgent === \"Cloudflare-Workers\") {\n return \"cloudflare\";\n }\n\n if (globalThis.navigator?.userAgent) {\n return \"browser\";\n }\n }\n\n return \"unknown\";\n}\n"],"mappings":"2CAwBA,eAAsB,EAAU,EAAuD,CACrF,IAAM,EAAW,GAAgB,CAEjC,OAAQ,EAAR,CACE,IAAK,OACH,GAAM,CAAE,eAAgB,MAAM,OAAO,eACrC,OAAO,IAAI,EAAY,EAAQ,CAEjC,IAAK,UACH,GAAM,CAAE,kBAAmB,MAAM,OAAO,kBACxC,OAAO,EAAe,OAAO,EAAQ,CAEvC,IAAK,aACH,GAAM,CAAE,oBAAA,GAAwB,MAAM,OAAO,uBAC7C,OAAO,IAAIA,EAAoB,EAAQ,CAEzC,QACE,MAAU,MAAM,yBAAyB,IAAW,EAM1D,SAAS,GAA2B,CAClC,GAAI,OAAO,WAAe,IAAa,CACrC,GAAI,WAAW,SAAS,UAAU,KAChC,MAAO,OAGT,GAAI,WAAW,WAAW,YAAc,qBACtC,MAAO,aAGT,GAAI,WAAW,WAAW,UACxB,MAAO,UAIX,MAAO"} |
+1
-1
@@ -1,2 +0,2 @@ | ||
| const e=require(`./node2.cjs`);function t(t){return new e.t(t)}exports.NodeAdapter=e.t,exports.openNodeStore=t; | ||
| const e=require(`./node3.cjs`);function t(t){return new e.t(t)}exports.NodeAdapter=e.t,exports.openNodeStore=t; | ||
| //# sourceMappingURL=node.cjs.map |
+1
-2
@@ -1,2 +0,1 @@ | ||
| var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=require(`./base.cjs`);let l=require(`better-sqlite3`);l=s(l);var u=class extends c.t{constructor(e){super(e),this._closed=!1,this.db=new l.default(e.path,{fileMustExist:!1}),this.db.pragma(`journal_mode = WAL`),this.db.pragma(`busy_timeout = 5000`)}normalizeParams(e){return e?Array.isArray(e)?e:Object.values(e):[]}convertBuffersToUint8Array(e){if(e==null)return e;if(Buffer.isBuffer(e))return new Uint8Array(e);if(Array.isArray(e))return e.map(e=>this.convertBuffersToUint8Array(e));if(typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=this.convertBuffersToUint8Array(r);return t}return e}checkConnection(){if(this._closed||!this.db.open)throw Error(`Database connection is closed`)}async query(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r);return this.convertBuffersToUint8Array(i)}async queryRaw(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=n.columns().map(e=>e.name);return{rows:this.convertBuffersToUint8Array(i),columns:a,rowsAffected:0,lastInsertRowId:0}}async run(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r);return{rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid}}async exec(e){this.checkConnection(),this.db.exec(e)}transaction(e){this.checkConnection(),this.db.exec(`BEGIN`);let t=new f(this.db);return(async()=>{try{let n=await e(t);return this.db.exec(`COMMIT`),n}catch(e){throw this.db.exec(`ROLLBACK`),e}})()}async asyncTransaction(e,t){this.checkConnection();let n=t?.timeoutMs??3e4,r=!1;try{this.db.exec(`BEGIN IMMEDIATE`)}catch(e){if(e.message.includes(`cannot start a transaction within a transaction`))r=!0;else throw e}let i,a=!1;try{let t=new Promise((e,t)=>{i=setTimeout(()=>{a||t(Error(`Async transaction timeout after ${n}ms`))},n)}),o=new p(this.db),s=await Promise.race([e(o),t]);return a=!0,i&&clearTimeout(i),r||this.db.exec(`COMMIT`),s}catch(e){a=!0,i&&clearTimeout(i);try{this.db.exec(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}getConnectionType(){return`direct`}async close(){!this._closed&&this.db.open&&(this.db.close(),this._closed=!0)}get isOpen(){return!this._closed&&this.db.open}},d=class{constructor(e){this.db=e,this.inTransaction=!0}normalizeParams(e){return e?Array.isArray(e)?e:Object.values(e):[]}convertBuffersToUint8Array(e){if(e==null)return e;if(Buffer.isBuffer(e))return new Uint8Array(e);if(Array.isArray(e))return e.map(e=>this.convertBuffersToUint8Array(e));if(typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=this.convertBuffersToUint8Array(r);return t}return e}query(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=this.convertBuffersToUint8Array(i);return Promise.resolve(a)}queryRaw(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=n.columns().map(e=>e.name),o={rows:this.convertBuffersToUint8Array(i),columns:a,rowsAffected:0,lastInsertRowId:0};return Promise.resolve(o)}run(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r),a={rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid};return Promise.resolve(a)}exec(e){return this.db.exec(e),Promise.resolve()}querySync(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r);return this.convertBuffersToUint8Array(i)}runSync(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r);return{rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid}}execSync(e){this.db.exec(e)}async close(){}},f=class extends d{getConnectionType(){return`syncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e,t){throw Error(`asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.`)}},p=class extends d{getConnectionType(){return`asyncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e,t){return await e(this)}};Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return u}}); | ||
| //# sourceMappingURL=node2.cjs.map | ||
| const e=require(`./node3.cjs`);exports.NodeAdapter=e.t; |
+2
-1
@@ -1,1 +0,2 @@ | ||
| const e=require(`./node2.cjs`);exports.NodeAdapter=e.t; | ||
| const e=require(`./chunk.cjs`),t=require(`./base.cjs`);let n=require(`better-sqlite3`);n=e.t(n);var r=class extends t.t{constructor(e){super(e),this._closed=!1,this.db=new n.default(e.path,{fileMustExist:!1}),this.db.pragma(`journal_mode = WAL`),this.db.pragma(`busy_timeout = 5000`)}normalizeParams(e){return e?Array.isArray(e)?e:Object.values(e):[]}convertBuffersToUint8Array(e){if(e==null)return e;if(Buffer.isBuffer(e))return new Uint8Array(e);if(Array.isArray(e))return e.map(e=>this.convertBuffersToUint8Array(e));if(typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=this.convertBuffersToUint8Array(r);return t}return e}checkConnection(){if(this._closed||!this.db.open)throw Error(`Database connection is closed`)}async query(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r);return this.convertBuffersToUint8Array(i)}async queryRaw(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=n.columns().map(e=>e.name);return{rows:this.convertBuffersToUint8Array(i),columns:a,rowsAffected:0,lastInsertRowId:0}}async run(e,t){this.checkConnection();let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r);return{rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid}}async exec(e){this.checkConnection(),this.db.exec(e)}transaction(e){this.checkConnection(),this.db.exec(`BEGIN`);let t=new a(this.db);return(async()=>{try{let n=await e(t);return this.db.exec(`COMMIT`),n}catch(e){throw this.db.exec(`ROLLBACK`),e}})()}async asyncTransaction(e,t){this.checkConnection();let n=t?.timeoutMs??3e4,r=!1;try{this.db.exec(`BEGIN IMMEDIATE`)}catch(e){if(e.message.includes(`cannot start a transaction within a transaction`))r=!0;else throw e}let i,a=!1;try{let t=new Promise((e,t)=>{i=setTimeout(()=>{a||t(Error(`Async transaction timeout after ${n}ms`))},n)}),s=new o(this.db),c=await Promise.race([e(s),t]);return a=!0,i&&clearTimeout(i),r||this.db.exec(`COMMIT`),c}catch(e){a=!0,i&&clearTimeout(i);try{this.db.exec(`ROLLBACK`)}catch(e){console.error(`Rollback failed:`,e)}throw e}}getConnectionType(){return`direct`}async close(){!this._closed&&this.db.open&&(this.db.close(),this._closed=!0)}get isOpen(){return!this._closed&&this.db.open}},i=class{constructor(e){this.db=e,this.inTransaction=!0}normalizeParams(e){return e?Array.isArray(e)?e:Object.values(e):[]}convertBuffersToUint8Array(e){if(e==null)return e;if(Buffer.isBuffer(e))return new Uint8Array(e);if(Array.isArray(e))return e.map(e=>this.convertBuffersToUint8Array(e));if(typeof e==`object`){let t={};for(let[n,r]of Object.entries(e))t[n]=this.convertBuffersToUint8Array(r);return t}return e}query(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=this.convertBuffersToUint8Array(i);return Promise.resolve(a)}queryRaw(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r),a=n.columns().map(e=>e.name),o={rows:this.convertBuffersToUint8Array(i),columns:a,rowsAffected:0,lastInsertRowId:0};return Promise.resolve(o)}run(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r),a={rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid};return Promise.resolve(a)}exec(e){return this.db.exec(e),Promise.resolve()}querySync(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.all(r);return this.convertBuffersToUint8Array(i)}runSync(e,t){let n=this.db.prepare(e),r=this.normalizeParams(t),i=n.run(r);return{rowsAffected:i.changes,lastInsertRowId:i.lastInsertRowid}}execSync(e){this.db.exec(e)}async close(){}},a=class extends i{getConnectionType(){return`syncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e,t){throw Error(`asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.`)}},o=class extends i{getConnectionType(){return`asyncTxn`}async transaction(e){return await e(this)}async asyncTransaction(e,t){return await e(this)}};Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return r}}); | ||
| //# sourceMappingURL=node3.cjs.map |
+103
-0
@@ -21,2 +21,105 @@ //#region src/types.d.ts | ||
| /** | ||
| * Custom Worker URL for bundler integration. | ||
| * | ||
| * When using bundlers like Vite, Webpack, or Rollup, the default CDN-based Worker | ||
| * loading may not work correctly in production builds. This option allows you to | ||
| * provide a pre-bundled Worker URL that the bundler can process. | ||
| * | ||
| * **Why is this needed?** | ||
| * | ||
| * By default, UniSQLite creates Workers dynamically using Blob URLs that load | ||
| * SQLite WASM from a CDN. This works but has drawbacks: | ||
| * - Requires runtime CDN access (no offline support) | ||
| * - Bypasses bundler optimizations | ||
| * - May violate Content Security Policy (CSP) | ||
| * | ||
| * By providing a `workerUrl`, you enable: | ||
| * - Offline support (Worker code is bundled) | ||
| * - Bundler optimizations (tree-shaking, minification) | ||
| * - CSP compliance (no external scripts or blob: URLs) | ||
| * - Version consistency (same SQLite version as your bundle) | ||
| * | ||
| * **Usage with Vite:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Import with Vite's ?worker&url suffix | ||
| * import sqliteWorkerUrl from './sqlite-worker?worker&url'; | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **Usage with Webpack:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.js | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Use Webpack's worker-loader or asset modules | ||
| * const sqliteWorkerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl.href, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **With SAHPool VFS (recommended for no COOP/COEP):** | ||
| * | ||
| * ```typescript | ||
| * // sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then(async (sqlite3) => { | ||
| * // Install SAHPool VFS if available | ||
| * if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| * try { | ||
| * await sqlite3.installOpfsSAHPoolVfs({ | ||
| * name: 'opfs-sahpool', | ||
| * directory: '/unisqlite-sahpool', | ||
| * }); | ||
| * } catch (e) { | ||
| * console.warn('SAHPool VFS installation failed:', e); | ||
| * } | ||
| * } | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * // Vite | ||
| * import workerUrl from './sqlite-worker?worker&url'; | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl } }); | ||
| * | ||
| * @example | ||
| * // Webpack 5 | ||
| * const workerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl: workerUrl.href } }); | ||
| * | ||
| * @see https://vitejs.dev/guide/features.html#web-workers | ||
| * @see https://webpack.js.org/guides/web-workers/ | ||
| */ | ||
| workerUrl?: string | URL; | ||
| /** | ||
| * Storage backend preference for browser persistence. | ||
@@ -23,0 +126,0 @@ * |
+103
-0
@@ -21,2 +21,105 @@ //#region src/types.d.ts | ||
| /** | ||
| * Custom Worker URL for bundler integration. | ||
| * | ||
| * When using bundlers like Vite, Webpack, or Rollup, the default CDN-based Worker | ||
| * loading may not work correctly in production builds. This option allows you to | ||
| * provide a pre-bundled Worker URL that the bundler can process. | ||
| * | ||
| * **Why is this needed?** | ||
| * | ||
| * By default, UniSQLite creates Workers dynamically using Blob URLs that load | ||
| * SQLite WASM from a CDN. This works but has drawbacks: | ||
| * - Requires runtime CDN access (no offline support) | ||
| * - Bypasses bundler optimizations | ||
| * - May violate Content Security Policy (CSP) | ||
| * | ||
| * By providing a `workerUrl`, you enable: | ||
| * - Offline support (Worker code is bundled) | ||
| * - Bundler optimizations (tree-shaking, minification) | ||
| * - CSP compliance (no external scripts or blob: URLs) | ||
| * - Version consistency (same SQLite version as your bundle) | ||
| * | ||
| * **Usage with Vite:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Import with Vite's ?worker&url suffix | ||
| * import sqliteWorkerUrl from './sqlite-worker?worker&url'; | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **Usage with Webpack:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.js | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Use Webpack's worker-loader or asset modules | ||
| * const sqliteWorkerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl.href, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **With SAHPool VFS (recommended for no COOP/COEP):** | ||
| * | ||
| * ```typescript | ||
| * // sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then(async (sqlite3) => { | ||
| * // Install SAHPool VFS if available | ||
| * if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| * try { | ||
| * await sqlite3.installOpfsSAHPoolVfs({ | ||
| * name: 'opfs-sahpool', | ||
| * directory: '/unisqlite-sahpool', | ||
| * }); | ||
| * } catch (e) { | ||
| * console.warn('SAHPool VFS installation failed:', e); | ||
| * } | ||
| * } | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * // Vite | ||
| * import workerUrl from './sqlite-worker?worker&url'; | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl } }); | ||
| * | ||
| * @example | ||
| * // Webpack 5 | ||
| * const workerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl: workerUrl.href } }); | ||
| * | ||
| * @see https://vitejs.dev/guide/features.html#web-workers | ||
| * @see https://webpack.js.org/guides/web-workers/ | ||
| */ | ||
| workerUrl?: string | URL; | ||
| /** | ||
| * Storage backend preference for browser persistence. | ||
@@ -23,0 +126,0 @@ * |
+6
-6
| { | ||
| "name": "@loro-dev/unisqlite", | ||
| "type": "module", | ||
| "version": "0.3.0", | ||
| "version": "0.4.0", | ||
| "description": "Cross-platform concurrent SQLite access layer", | ||
@@ -58,4 +58,3 @@ "author": "", | ||
| "@sqlite.org/sqlite-wasm": "3.50.1-build1", | ||
| "better-sqlite3": "^11.7.0", | ||
| "broadcast-channel": "^7.0.0" | ||
| "better-sqlite3": "^11.7.0" | ||
| }, | ||
@@ -68,5 +67,2 @@ "peerDependenciesMeta": { | ||
| "optional": true | ||
| }, | ||
| "broadcast-channel": { | ||
| "optional": true | ||
| } | ||
@@ -78,2 +74,3 @@ }, | ||
| "@types/better-sqlite3": "^7.6.13", | ||
| "@types/debug": "^4.1.12", | ||
| "@types/node": "^20.17.19", | ||
@@ -90,2 +87,5 @@ "oblivious-set": "2.0.0", | ||
| }, | ||
| "dependencies": { | ||
| "debug": "^4.4.3" | ||
| }, | ||
| "scripts": { | ||
@@ -92,0 +92,0 @@ "build": "tsdown", |
+193
-0
@@ -239,2 +239,195 @@ # UniSQLite | ||
| ## Bundler Integration (Vite, Webpack, Rollup) | ||
| When using bundlers in production, the default CDN-based Worker loading may not be ideal. UniSQLite provides a `workerUrl` option for proper bundler integration. | ||
| ### Why Custom Worker URL? | ||
| By default, UniSQLite creates Workers dynamically using Blob URLs that load SQLite WASM from a CDN. This works for development but has production drawbacks: | ||
| | Default (CDN) | Custom workerUrl | | ||
| |---------------|------------------| | ||
| | ❌ Requires runtime CDN access | ✅ Fully bundled, works offline | | ||
| | ❌ Bypasses bundler optimizations | ✅ Tree-shaking, minification | | ||
| | ❌ May violate CSP | ✅ CSP compliant | | ||
| | ❌ Version mismatch risk | ✅ Consistent versions | | ||
| ### Vite Setup | ||
| **Step 1: Create a Worker file** | ||
| ```typescript | ||
| // src/sqlite-worker.ts | ||
| import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| sqlite3InitModule().then(async (sqlite3) => { | ||
| // Optional: Install SAHPool VFS (no COOP/COEP required) | ||
| if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| try { | ||
| await sqlite3.installOpfsSAHPoolVfs({ | ||
| name: 'opfs-sahpool', | ||
| directory: '/unisqlite-sahpool', | ||
| }); | ||
| } catch (e) { | ||
| console.warn('SAHPool VFS installation failed:', e); | ||
| } | ||
| } | ||
| // Initialize Worker API | ||
| sqlite3.initWorker1API(); | ||
| }); | ||
| ``` | ||
| **Step 2: Import and use the Worker URL** | ||
| ```typescript | ||
| // src/db.ts | ||
| import { openStore } from '@loro-dev/unisqlite'; | ||
| // Vite processes this import and bundles the worker | ||
| import sqliteWorkerUrl from './sqlite-worker?worker&url'; | ||
| export async function createDatabase(name: string) { | ||
| return await openStore({ | ||
| name, | ||
| sqlite: { | ||
| workerUrl: sqliteWorkerUrl, | ||
| storageBackend: 'opfs', | ||
| }, | ||
| }); | ||
| } | ||
| ``` | ||
| **Step 3: Configure Vite (if needed)** | ||
| ```typescript | ||
| // vite.config.ts | ||
| import { defineConfig } from 'vite'; | ||
| export default defineConfig({ | ||
| optimizeDeps: { | ||
| exclude: ['@sqlite.org/sqlite-wasm'], | ||
| }, | ||
| worker: { | ||
| format: 'es', | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Webpack 5 Setup | ||
| **Step 1: Create a Worker file** | ||
| ```javascript | ||
| // src/sqlite-worker.js | ||
| import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| sqlite3InitModule().then(async (sqlite3) => { | ||
| // Optional: Install SAHPool VFS | ||
| if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| try { | ||
| await sqlite3.installOpfsSAHPoolVfs({ | ||
| name: 'opfs-sahpool', | ||
| directory: '/unisqlite-sahpool', | ||
| }); | ||
| } catch (e) { | ||
| console.warn('SAHPool VFS installation failed:', e); | ||
| } | ||
| } | ||
| sqlite3.initWorker1API(); | ||
| }); | ||
| ``` | ||
| **Step 2: Import and use the Worker URL** | ||
| ```typescript | ||
| // src/db.ts | ||
| import { openStore } from '@loro-dev/unisqlite'; | ||
| // Webpack 5 processes this with asset modules | ||
| const sqliteWorkerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| export async function createDatabase(name: string) { | ||
| return await openStore({ | ||
| name, | ||
| sqlite: { | ||
| workerUrl: sqliteWorkerUrl.href, | ||
| storageBackend: 'opfs', | ||
| }, | ||
| }); | ||
| } | ||
| ``` | ||
| ### TypeScript Declarations | ||
| If TypeScript complains about the `?worker&url` import, add this to your declarations: | ||
| ```typescript | ||
| // src/vite-env.d.ts or src/global.d.ts | ||
| declare module '*?worker&url' { | ||
| const workerUrl: string; | ||
| export default workerUrl; | ||
| } | ||
| ``` | ||
| ### Choosing VFS Type | ||
| When creating your Worker, you can choose which VFS to install: | ||
| | VFS | Requirements | When to Use | | ||
| |-----|--------------|-------------| | ||
| | `opfs` (default) | COOP/COEP headers | Best performance, requires `SharedArrayBuffer` | | ||
| | `opfs-sahpool` | None | No special headers needed, works everywhere | | ||
| **For SAHPool (recommended for wider compatibility):** | ||
| ```typescript | ||
| // sqlite-worker.ts | ||
| import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| sqlite3InitModule().then(async (sqlite3) => { | ||
| // Install SAHPool - works without COOP/COEP | ||
| if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| await sqlite3.installOpfsSAHPoolVfs({ | ||
| name: 'opfs-sahpool', | ||
| directory: '/unisqlite-sahpool', | ||
| }); | ||
| } | ||
| sqlite3.initWorker1API(); | ||
| }); | ||
| ``` | ||
| Then configure UniSQLite to use SAHPool: | ||
| ```typescript | ||
| const db = await openStore({ | ||
| name: 'mydb', | ||
| sqlite: { | ||
| workerUrl: sqliteWorkerUrl, | ||
| storageBackend: 'opfs', | ||
| opfsVfsType: 'sahpool', // Use SAHPool VFS | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Fallback Behavior | ||
| If `workerUrl` is not provided, UniSQLite falls back to CDN-based Worker creation. This is useful for: | ||
| - Development (zero configuration) | ||
| - Quick prototyping | ||
| - Environments where bundling Workers is not possible | ||
| ```typescript | ||
| // Development: no workerUrl needed | ||
| const db = await openStore({ | ||
| name: 'mydb', | ||
| sqlite: { | ||
| loadStrategy: 'cdn', // Load from CDN | ||
| storageBackend: 'opfs', | ||
| }, | ||
| }); | ||
| ``` | ||
| ## Platform-specific entrypoints | ||
@@ -241,0 +434,0 @@ |
@@ -1,193 +0,51 @@ | ||
| import { describe, it, expect, beforeEach, vi } from "vitest"; | ||
| import { BrowserAdapter } from "./browser.js"; | ||
| import type { UniStoreOptions } from "../types.js"; | ||
| import { describe, it, expect, vi } from "vitest"; | ||
| import { BrowserAdapter } from "./browser/index.js"; | ||
| import { HostElection } from "./browser/host-election.js"; | ||
| import { VisibilityManager } from "./browser/visibility-manager.js"; | ||
| import { RpcChannel } from "./browser/rpc-channel.js"; | ||
| // These tests are skipped by default since they require browser environment | ||
| const describeBrowser = process.env.UNISQLITE_BROWSER_TESTS === "1" ? describe : describe.skip; | ||
| // Mock broadcast-channel | ||
| vi.mock("broadcast-channel", () => ({ | ||
| BroadcastChannel: vi.fn().mockImplementation(() => ({ | ||
| postMessage: vi.fn(), | ||
| close: vi.fn(), | ||
| onmessage: null, | ||
| })), | ||
| createLeaderElection: vi.fn().mockImplementation(() => ({ | ||
| hasLeader: vi.fn().mockResolvedValue(false), | ||
| awaitLeadership: vi.fn().mockResolvedValue(undefined), | ||
| die: vi.fn().mockResolvedValue(undefined), | ||
| onduplicate: null, | ||
| })), | ||
| })); | ||
| // Mock @sqlite.org/sqlite-wasm | ||
| vi.mock("@sqlite.org/sqlite-wasm", () => ({ | ||
| default: vi.fn().mockResolvedValue({ | ||
| oo1: { | ||
| OpfsDb: vi.fn().mockImplementation((_name: string) => ({ | ||
| exec: vi.fn(), | ||
| prepare: vi.fn().mockReturnValue({ | ||
| bind: vi.fn(), | ||
| getColumnCount: vi.fn().mockReturnValue(2), | ||
| getColumnName: vi.fn().mockImplementation((i: number) => (i === 0 ? "id" : "name")), | ||
| step: vi.fn().mockReturnValueOnce(true).mockReturnValueOnce(false), | ||
| get: vi.fn().mockImplementation((i: number) => (i === 0 ? 1 : "test")), | ||
| finalize: vi.fn(), | ||
| }), | ||
| changes: vi.fn().mockReturnValue(1), | ||
| lastInsertRowid: vi.fn().mockReturnValue(1), | ||
| close: vi.fn(), | ||
| })), | ||
| }, | ||
| }), | ||
| })); | ||
| describeBrowser("BrowserAdapter Unit Tests", () => { | ||
| let adapter: BrowserAdapter; | ||
| const options: UniStoreOptions = { path: "test-db" }; | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| describe("Browser Adapter Unit Tests (Node environment)", () => { | ||
| it("should export BrowserAdapter class", () => { | ||
| expect(BrowserAdapter).toBeDefined(); | ||
| expect(typeof BrowserAdapter).toBe("function"); | ||
| }); | ||
| it("should initialize leader election on construction", async () => { | ||
| adapter = await BrowserAdapter.create(options); | ||
| const { createLeaderElection } = await import("broadcast-channel"); | ||
| expect(createLeaderElection).toHaveBeenCalled(); | ||
| it("should export HostElection class", () => { | ||
| expect(HostElection).toBeDefined(); | ||
| expect(typeof HostElection).toBe("function"); | ||
| }); | ||
| it("should create a BroadcastChannel with correct name", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 10)); | ||
| const { BroadcastChannel } = await import("broadcast-channel"); | ||
| expect(BroadcastChannel).toHaveBeenCalledWith("unisqlite-test-db"); | ||
| it("should export VisibilityManager class", () => { | ||
| expect(VisibilityManager).toBeDefined(); | ||
| expect(typeof VisibilityManager).toBe("function"); | ||
| }); | ||
| it("should handle query method when leader", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| // Wait for initialization | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| // Force leader status | ||
| (adapter as any).isLeader = true; | ||
| (adapter as any).db = { | ||
| prepare: vi.fn().mockReturnValue({ | ||
| bind: vi.fn(), | ||
| getColumnCount: vi.fn().mockReturnValue(2), | ||
| getColumnName: vi.fn().mockImplementation((i: number) => (i === 0 ? "id" : "name")), | ||
| step: vi.fn().mockReturnValueOnce(true).mockReturnValueOnce(false), | ||
| get: vi.fn().mockImplementation((i: number) => (i === 0 ? 1 : "test")), | ||
| finalize: vi.fn(), | ||
| }), | ||
| changes: vi.fn().mockReturnValue(0), | ||
| lastInsertRowid: vi.fn().mockReturnValue(0), | ||
| }; | ||
| const result = await adapter.query("SELECT * FROM test"); | ||
| expect(result).toEqual([{ id: 1, name: "test" }]); | ||
| it("should export RpcChannel class", () => { | ||
| expect(RpcChannel).toBeDefined(); | ||
| expect(typeof RpcChannel).toBe("function"); | ||
| }); | ||
| it("should handle run method with proper result", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| (adapter as any).isLeader = true; | ||
| (adapter as any).db = { | ||
| prepare: vi.fn().mockReturnValue({ | ||
| bind: vi.fn(), | ||
| getColumnCount: vi.fn().mockReturnValue(0), | ||
| step: vi.fn().mockReturnValue(false), | ||
| finalize: vi.fn(), | ||
| }), | ||
| changes: vi.fn().mockReturnValue(1), | ||
| lastInsertRowid: vi.fn().mockReturnValue(42), | ||
| exec: vi.fn(), | ||
| }; | ||
| const result = await adapter.run("INSERT INTO test (name) VALUES (?)", ["test"]); | ||
| expect(result).toEqual({ | ||
| rowsAffected: 1, | ||
| lastInsertRowId: 42, | ||
| }); | ||
| it("HostElection.isSupported should return boolean", () => { | ||
| // Note: Node.js 18+ has navigator.locks, so this may return true | ||
| const result = HostElection.isSupported(); | ||
| expect(typeof result).toBe("boolean"); | ||
| }); | ||
| it("should handle exec method", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| const mockExec = vi.fn(); | ||
| (adapter as any).isLeader = true; | ||
| (adapter as any).db = { exec: mockExec }; | ||
| await adapter.exec("CREATE TABLE test (id INTEGER PRIMARY KEY)"); | ||
| expect(mockExec).toHaveBeenCalledWith("CREATE TABLE test (id INTEGER PRIMARY KEY)"); | ||
| it("VisibilityManager should assume visible in non-browser environment", () => { | ||
| const manager = new VisibilityManager(); | ||
| // In Node.js, document is undefined, so it should assume visible | ||
| expect(manager.isVisible()).toBe(true); | ||
| manager.destroy(); | ||
| }); | ||
| }); | ||
| it("should handle transactions", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| const mockExec = vi.fn(); | ||
| (adapter as any).isLeader = true; | ||
| (adapter as any).db = { | ||
| exec: mockExec, | ||
| prepare: vi.fn().mockReturnValue({ | ||
| bind: vi.fn(), | ||
| getColumnCount: vi.fn().mockReturnValue(0), | ||
| step: vi.fn().mockReturnValue(false), | ||
| finalize: vi.fn(), | ||
| }), | ||
| changes: vi.fn().mockReturnValue(1), | ||
| lastInsertRowid: vi.fn().mockReturnValue(1), | ||
| }; | ||
| let transactionExecuted = false; | ||
| await adapter.transaction((tx) => { | ||
| transactionExecuted = true; | ||
| return tx.run("INSERT INTO test VALUES (1)"); | ||
| }); | ||
| expect(transactionExecuted).toBe(true); | ||
| expect(mockExec).toHaveBeenCalledWith("BEGIN"); | ||
| expect(mockExec).toHaveBeenCalledWith("COMMIT"); | ||
| describeBrowser("BrowserAdapter Browser Tests", () => { | ||
| // These tests would run in actual browser environment (e.g., Playwright) | ||
| it("placeholder for browser-specific tests", () => { | ||
| // Browser-specific tests should be in e2e tests | ||
| expect(true).toBe(true); | ||
| }); | ||
| it("should rollback transaction on error", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| const mockExec = vi.fn(); | ||
| (adapter as any).isLeader = true; | ||
| (adapter as any).db = { exec: mockExec }; | ||
| await expect( | ||
| adapter.transaction(async () => { | ||
| throw new Error("Test error"); | ||
| }) | ||
| ).rejects.toThrow("Test error"); | ||
| expect(mockExec).toHaveBeenCalledWith("BEGIN"); | ||
| expect(mockExec).toHaveBeenCalledWith("ROLLBACK"); | ||
| expect(mockExec).not.toHaveBeenCalledWith("COMMIT"); | ||
| }); | ||
| it("should clean up resources on close", async () => { | ||
| adapter = new BrowserAdapter(options); | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
| const mockClose = vi.fn(); | ||
| const mockDie = vi.fn().mockResolvedValue(undefined); | ||
| const mockChannelClose = vi.fn(); | ||
| (adapter as any).db = { close: mockClose }; | ||
| (adapter as any).elector = { die: mockDie }; | ||
| (adapter as any).channel = { close: mockChannelClose }; | ||
| await adapter.close(); | ||
| expect(mockClose).toHaveBeenCalled(); | ||
| expect(mockDie).toHaveBeenCalled(); | ||
| expect(mockChannelClose).toHaveBeenCalled(); | ||
| }); | ||
| }); |
| import { afterEach, beforeEach, describe, expect, it } from "vitest"; | ||
| import { openStore } from "../index.js"; | ||
| import { BrowserAdapter } from "./browser.js"; | ||
| import { BrowserAdapter } from "./browser/index.js"; | ||
| import type { UniStoreConnection } from "../types.js"; | ||
@@ -5,0 +5,0 @@ import * as fs from "node:fs"; |
+2
-2
@@ -15,6 +15,6 @@ export type { | ||
| import type { UniStoreConnection, UniStoreOptions } from "./types.js"; | ||
| import { BrowserAdapter } from "./adapters/browser.js"; | ||
| import { BrowserAdapter } from "./adapters/browser/index.js"; | ||
| export async function openStore(options: UniStoreOptions): Promise<UniStoreConnection> { | ||
| return new BrowserAdapter(options); | ||
| return await BrowserAdapter.create(options); | ||
| } |
+1
-1
@@ -34,3 +34,3 @@ export type { | ||
| case "browser": | ||
| const { BrowserAdapter } = await import("./adapters/browser.js"); | ||
| const { BrowserAdapter } = await import("./adapters/browser/index.js"); | ||
| return BrowserAdapter.create(options); | ||
@@ -37,0 +37,0 @@ |
+104
-0
@@ -23,2 +23,106 @@ export type SQLiteValue = string | number | boolean | null | Uint8Array; | ||
| /** | ||
| * Custom Worker URL for bundler integration. | ||
| * | ||
| * When using bundlers like Vite, Webpack, or Rollup, the default CDN-based Worker | ||
| * loading may not work correctly in production builds. This option allows you to | ||
| * provide a pre-bundled Worker URL that the bundler can process. | ||
| * | ||
| * **Why is this needed?** | ||
| * | ||
| * By default, UniSQLite creates Workers dynamically using Blob URLs that load | ||
| * SQLite WASM from a CDN. This works but has drawbacks: | ||
| * - Requires runtime CDN access (no offline support) | ||
| * - Bypasses bundler optimizations | ||
| * - May violate Content Security Policy (CSP) | ||
| * | ||
| * By providing a `workerUrl`, you enable: | ||
| * - Offline support (Worker code is bundled) | ||
| * - Bundler optimizations (tree-shaking, minification) | ||
| * - CSP compliance (no external scripts or blob: URLs) | ||
| * - Version consistency (same SQLite version as your bundle) | ||
| * | ||
| * **Usage with Vite:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Import with Vite's ?worker&url suffix | ||
| * import sqliteWorkerUrl from './sqlite-worker?worker&url'; | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **Usage with Webpack:** | ||
| * | ||
| * ```typescript | ||
| * // 1. Create a worker file: src/sqlite-worker.js | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then((sqlite3) => { | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * | ||
| * // 2. Use Webpack's worker-loader or asset modules | ||
| * const sqliteWorkerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * | ||
| * // 3. Pass to openStore | ||
| * const db = await openStore({ | ||
| * name: 'mydb', | ||
| * sqlite: { | ||
| * workerUrl: sqliteWorkerUrl.href, | ||
| * storageBackend: 'opfs', | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| * | ||
| * **With SAHPool VFS (recommended for no COOP/COEP):** | ||
| * | ||
| * ```typescript | ||
| * // sqlite-worker.ts | ||
| * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; | ||
| * | ||
| * sqlite3InitModule().then(async (sqlite3) => { | ||
| * // Install SAHPool VFS if available | ||
| * if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') { | ||
| * try { | ||
| * await sqlite3.installOpfsSAHPoolVfs({ | ||
| * name: 'opfs-sahpool', | ||
| * directory: '/unisqlite-sahpool', | ||
| * }); | ||
| * } catch (e) { | ||
| * console.warn('SAHPool VFS installation failed:', e); | ||
| * } | ||
| * } | ||
| * sqlite3.initWorker1API(); | ||
| * }); | ||
| * ``` | ||
| * | ||
| * @example | ||
| * // Vite | ||
| * import workerUrl from './sqlite-worker?worker&url'; | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl } }); | ||
| * | ||
| * @example | ||
| * // Webpack 5 | ||
| * const workerUrl = new URL('./sqlite-worker.js', import.meta.url); | ||
| * const db = await openStore({ name: 'db', sqlite: { workerUrl: workerUrl.href } }); | ||
| * | ||
| * @see https://vitejs.dev/guide/features.html#web-workers | ||
| * @see https://webpack.js.org/guides/web-workers/ | ||
| */ | ||
| workerUrl?: string | URL; | ||
| /** | ||
| * Storage backend preference for browser persistence. | ||
@@ -25,0 +129,0 @@ * |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| {"version":3,"file":"node2.cjs","names":["BaseAdapter","Database"],"sources":["../src/adapters/node.ts"],"sourcesContent":["import { BaseAdapter } from \"./base.js\";\nimport type {\n QueryResult,\n RunResult,\n SQLiteParams,\n SQLiteValue,\n UniStoreConnection,\n UniStoreOptions,\n ConnectionType,\n} from \"../types.js\";\nimport Database from \"better-sqlite3\";\n\nexport class NodeAdapter extends BaseAdapter {\n private db: Database.Database;\n private _closed: boolean = false;\n\n constructor(options: UniStoreOptions) {\n super(options);\n // Open database with WAL mode for better concurrency\n this.db = new Database(options.path, { fileMustExist: false });\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"busy_timeout = 5000\"); // 5 second timeout for locks\n }\n\n private normalizeParams(params?: SQLiteParams): unknown[] {\n if (!params) return [];\n if (Array.isArray(params)) return params;\n // Convert object params to array format for better-sqlite3\n // This is a simplified conversion - in practice you'd need to handle named parameters\n return Object.values(params);\n }\n\n private convertBuffersToUint8Array<T>(value: T): T {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Buffer.isBuffer(value)) {\n return new Uint8Array(value) as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => this.convertBuffersToUint8Array(item)) as T;\n }\n\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = this.convertBuffersToUint8Array(val);\n }\n return result as T;\n }\n\n return value;\n }\n\n private checkConnection(): void {\n if (this._closed || !this.db.open) {\n throw new Error(\"Database connection is closed\");\n }\n }\n\n async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n return this.convertBuffersToUint8Array(rows);\n }\n\n async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const columns = stmt.columns().map((col) => col.name);\n\n return {\n rows: this.convertBuffersToUint8Array(rows),\n columns,\n rowsAffected: 0,\n lastInsertRowId: 0,\n };\n }\n\n async run(sql: string, params?: SQLiteParams): Promise<RunResult> {\n this.checkConnection();\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n return {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n }\n\n async exec(sql: string): Promise<void> {\n this.checkConnection();\n this.db.exec(sql);\n }\n\n transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n this.checkConnection();\n // better-sqlite3 supports synchronous transactions\n this.db.exec(\"BEGIN\");\n const transactionAdapter = new SyncTransactionAdapter(this.db);\n return (async () => {\n try {\n const result = await fn(transactionAdapter);\n this.db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n })();\n }\n\n /**\n * Execute an async transaction with manual transaction management.\n * This allows async operations within the transaction but comes with timeout support\n * to prevent long-running transactions from blocking the database.\n *\n * @param fn Transaction function that can contain async operations\n * @param options Transaction options including timeout (default: 30000ms)\n */\n async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, options?: { timeoutMs?: number }): Promise<T> {\n this.checkConnection();\n\n const timeoutMs = options?.timeoutMs ?? 30000; // Default 30 seconds\n let alreadyInTransaction = false;\n\n // Manual transaction management using BEGIN/COMMIT/ROLLBACK\n try {\n this.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n if ((e as Error).message.includes(\"cannot start a transaction within a transaction\")) {\n alreadyInTransaction = true;\n } else {\n throw e;\n }\n }\n\n let timeoutHandle: NodeJS.Timeout | undefined;\n let transactionCompleted = false;\n\n try {\n // Set up timeout\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(() => {\n if (!transactionCompleted) {\n reject(new Error(`Async transaction timeout after ${timeoutMs}ms`));\n }\n }, timeoutMs);\n });\n\n const transactionAdapter = new AsyncTransactionAdapter(this.db);\n\n // Race between transaction execution and timeout\n const result = await Promise.race([fn(transactionAdapter), timeoutPromise]);\n\n transactionCompleted = true;\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n }\n\n // If we get here, the transaction completed successfully\n if (!alreadyInTransaction) {\n this.db.exec(\"COMMIT\");\n }\n return result;\n } catch (error) {\n transactionCompleted = true;\n if (timeoutHandle) {\n clearTimeout(timeoutHandle);\n }\n\n try {\n this.db.exec(\"ROLLBACK\");\n } catch (rollbackError) {\n console.error(\"Rollback failed:\", rollbackError);\n }\n\n throw error;\n }\n }\n\n getConnectionType(): ConnectionType {\n return \"direct\";\n }\n\n async close(): Promise<void> {\n if (!this._closed && this.db.open) {\n this.db.close();\n this._closed = true;\n }\n }\n\n get isOpen(): boolean {\n return !this._closed && this.db.open;\n }\n}\n\nabstract class BaseTransactionAdapter implements UniStoreConnection {\n public readonly inTransaction = true;\n\n constructor(protected db: Database.Database) {}\n\n private normalizeParams(params?: SQLiteParams): unknown[] {\n if (!params) return [];\n if (Array.isArray(params)) return params;\n return Object.values(params);\n }\n\n private convertBuffersToUint8Array<T>(value: T): T {\n if (value === null || value === undefined) {\n return value;\n }\n\n if (Buffer.isBuffer(value)) {\n return new Uint8Array(value) as T;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => this.convertBuffersToUint8Array(item)) as T;\n }\n\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n result[key] = this.convertBuffersToUint8Array(val);\n }\n return result as T;\n }\n\n return value;\n }\n\n // Synchronous database methods for use in transactions\n query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const result = this.convertBuffersToUint8Array(rows);\n return Promise.resolve(result);\n }\n\n queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n const columns = stmt.columns().map((col) => col.name);\n\n const result = {\n rows: this.convertBuffersToUint8Array(rows),\n columns,\n rowsAffected: 0,\n lastInsertRowId: 0,\n };\n return Promise.resolve(result);\n }\n\n run(sql: string, params?: SQLiteParams): Promise<RunResult> {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n const result = {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n return Promise.resolve(result);\n }\n\n exec(sql: string): Promise<void> {\n this.db.exec(sql);\n return Promise.resolve();\n }\n\n // Synchronous versions for use in sync transactions\n querySync<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): T[] {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const rows = stmt.all(paramsArray) as T[];\n return this.convertBuffersToUint8Array(rows);\n }\n\n runSync(sql: string, params?: SQLiteParams): RunResult {\n const stmt = this.db.prepare(sql);\n const paramsArray = this.normalizeParams(params);\n const info = stmt.run(paramsArray);\n\n return {\n rowsAffected: info.changes,\n lastInsertRowId: info.lastInsertRowid as number,\n };\n }\n\n execSync(sql: string): void {\n this.db.exec(sql);\n }\n\n abstract getConnectionType(): ConnectionType;\n abstract transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T>;\n abstract asyncTransaction<T>(\n fn: (tx: UniStoreConnection) => Promise<T>,\n options?: { timeoutMs?: number }\n ): Promise<T>;\n\n async close(): Promise<void> {\n // No-op for transaction\n }\n}\n\nclass SyncTransactionAdapter extends BaseTransactionAdapter {\n getConnectionType(): ConnectionType {\n return \"syncTxn\";\n }\n\n async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n // For syncTxn, we can execute the transaction using itself as the connection\n // This allows nested operations within the same transaction\n return await fn(this);\n }\n\n async asyncTransaction<T>(\n _fn: (tx: UniStoreConnection) => Promise<T>,\n _options?: { timeoutMs?: number }\n ): Promise<T> {\n // syncTxn connections cannot run asyncTransaction\n throw new Error(\n \"asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection.\"\n );\n }\n}\n\nclass AsyncTransactionAdapter extends BaseTransactionAdapter {\n getConnectionType(): ConnectionType {\n return \"asyncTxn\";\n }\n\n async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> {\n // For asyncTxn, we can execute the transaction using itself as the connection\n // This allows nested operations within the same transaction\n return await fn(this);\n }\n\n async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, _options?: { timeoutMs?: number }): Promise<T> {\n // For asyncTxn, we can execute asyncTransaction using itself as the connection\n // This allows nested async operations within the same transaction\n return await fn(this);\n }\n}\n"],"mappings":"miBAYA,IAAa,EAAb,cAAiCA,EAAAA,CAAY,CAI3C,YAAY,EAA0B,CACpC,MAAM,EAAQ,cAHW,GAKzB,KAAK,GAAK,IAAIC,EAAAA,QAAS,EAAQ,KAAM,CAAE,cAAe,GAAO,CAAC,CAC9D,KAAK,GAAG,OAAO,qBAAqB,CACpC,KAAK,GAAG,OAAO,sBAAsB,CAGvC,gBAAwB,EAAkC,CAKxD,OAJK,EACD,MAAM,QAAQ,EAAO,CAAS,EAG3B,OAAO,OAAO,EAAO,CAJR,EAAE,CAOxB,2BAAsC,EAAa,CACjD,GAAI,GAAU,KACZ,OAAO,EAGT,GAAI,OAAO,SAAS,EAAM,CACxB,OAAO,IAAI,WAAW,EAAM,CAG9B,GAAI,MAAM,QAAQ,EAAM,CACtB,OAAO,EAAM,IAAK,GAAS,KAAK,2BAA2B,EAAK,CAAC,CAGnE,GAAI,OAAO,GAAU,SAAU,CAC7B,IAAM,EAAkC,EAAE,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAQ,OAAO,QAAQ,EAAM,CAC5C,EAAO,GAAO,KAAK,2BAA2B,EAAI,CAEpD,OAAO,EAGT,OAAO,EAGT,iBAAgC,CAC9B,GAAI,KAAK,SAAW,CAAC,KAAK,GAAG,KAC3B,MAAU,MAAM,gCAAgC,CAIpD,MAAM,MAAuC,EAAa,EAAqC,CAC7F,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAClC,OAAO,KAAK,2BAA2B,EAAK,CAG9C,MAAM,SAA0C,EAAa,EAAgD,CAC3G,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAU,EAAK,SAAS,CAAC,IAAK,GAAQ,EAAI,KAAK,CAErD,MAAO,CACL,KAAM,KAAK,2BAA2B,EAAK,CAC3C,UACA,aAAc,EACd,gBAAiB,EAClB,CAGH,MAAM,IAAI,EAAa,EAA2C,CAChE,KAAK,iBAAiB,CACtB,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAElC,MAAO,CACL,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CAGH,MAAM,KAAK,EAA4B,CACrC,KAAK,iBAAiB,CACtB,KAAK,GAAG,KAAK,EAAI,CAGnB,YAAe,EAA4D,CACzE,KAAK,iBAAiB,CAEtB,KAAK,GAAG,KAAK,QAAQ,CACrB,IAAM,EAAqB,IAAI,EAAuB,KAAK,GAAG,CAC9D,OAAQ,SAAY,CAClB,GAAI,CACF,IAAM,EAAS,MAAM,EAAG,EAAmB,CAE3C,OADA,KAAK,GAAG,KAAK,SAAS,CACf,QACA,EAAO,CAEd,MADA,KAAK,GAAG,KAAK,WAAW,CAClB,MAEN,CAWN,MAAM,iBAAoB,EAA4C,EAA8C,CAClH,KAAK,iBAAiB,CAEtB,IAAM,EAAY,GAAS,WAAa,IACpC,EAAuB,GAG3B,GAAI,CACF,KAAK,GAAG,KAAK,kBAAkB,OACxB,EAAG,CACV,GAAK,EAAY,QAAQ,SAAS,kDAAkD,CAClF,EAAuB,QAEvB,MAAM,EAIV,IAAI,EACA,EAAuB,GAE3B,GAAI,CAEF,IAAM,EAAiB,IAAI,SAAgB,EAAG,IAAW,CACvD,EAAgB,eAAiB,CAC1B,GACH,EAAW,MAAM,mCAAmC,EAAU,IAAI,CAAC,EAEpE,EAAU,EACb,CAEI,EAAqB,IAAI,EAAwB,KAAK,GAAG,CAGzD,EAAS,MAAM,QAAQ,KAAK,CAAC,EAAG,EAAmB,CAAE,EAAe,CAAC,CAW3E,MATA,GAAuB,GACnB,GACF,aAAa,EAAc,CAIxB,GACH,KAAK,GAAG,KAAK,SAAS,CAEjB,QACA,EAAO,CACd,EAAuB,GACnB,GACF,aAAa,EAAc,CAG7B,GAAI,CACF,KAAK,GAAG,KAAK,WAAW,OACjB,EAAe,CACtB,QAAQ,MAAM,mBAAoB,EAAc,CAGlD,MAAM,GAIV,mBAAoC,CAClC,MAAO,SAGT,MAAM,OAAuB,CACvB,CAAC,KAAK,SAAW,KAAK,GAAG,OAC3B,KAAK,GAAG,OAAO,CACf,KAAK,QAAU,IAInB,IAAI,QAAkB,CACpB,MAAO,CAAC,KAAK,SAAW,KAAK,GAAG,OAIrB,EAAf,KAAoE,CAGlE,YAAY,EAAiC,CAAvB,KAAA,GAAA,qBAFU,GAIhC,gBAAwB,EAAkC,CAGxD,OAFK,EACD,MAAM,QAAQ,EAAO,CAAS,EAC3B,OAAO,OAAO,EAAO,CAFR,EAAE,CAKxB,2BAAsC,EAAa,CACjD,GAAI,GAAU,KACZ,OAAO,EAGT,GAAI,OAAO,SAAS,EAAM,CACxB,OAAO,IAAI,WAAW,EAAM,CAG9B,GAAI,MAAM,QAAQ,EAAM,CACtB,OAAO,EAAM,IAAK,GAAS,KAAK,2BAA2B,EAAK,CAAC,CAGnE,GAAI,OAAO,GAAU,SAAU,CAC7B,IAAM,EAAkC,EAAE,CAC1C,IAAK,GAAM,CAAC,EAAK,KAAQ,OAAO,QAAQ,EAAM,CAC5C,EAAO,GAAO,KAAK,2BAA2B,EAAI,CAEpD,OAAO,EAGT,OAAO,EAIT,MAAuC,EAAa,EAAqC,CACvF,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAS,KAAK,2BAA2B,EAAK,CACpD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,SAA0C,EAAa,EAAgD,CACrG,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAC5B,EAAU,EAAK,SAAS,CAAC,IAAK,GAAQ,EAAI,KAAK,CAE/C,EAAS,CACb,KAAM,KAAK,2BAA2B,EAAK,CAC3C,UACA,aAAc,EACd,gBAAiB,EAClB,CACD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,IAAI,EAAa,EAA2C,CAC1D,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAE5B,EAAS,CACb,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CACD,OAAO,QAAQ,QAAQ,EAAO,CAGhC,KAAK,EAA4B,CAE/B,OADA,KAAK,GAAG,KAAK,EAAI,CACV,QAAQ,SAAS,CAI1B,UAA2C,EAAa,EAA4B,CAClF,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAClC,OAAO,KAAK,2BAA2B,EAAK,CAG9C,QAAQ,EAAa,EAAkC,CACrD,IAAM,EAAO,KAAK,GAAG,QAAQ,EAAI,CAC3B,EAAc,KAAK,gBAAgB,EAAO,CAC1C,EAAO,EAAK,IAAI,EAAY,CAElC,MAAO,CACL,aAAc,EAAK,QACnB,gBAAiB,EAAK,gBACvB,CAGH,SAAS,EAAmB,CAC1B,KAAK,GAAG,KAAK,EAAI,CAUnB,MAAM,OAAuB,IAKzB,EAAN,cAAqC,CAAuB,CAC1D,mBAAoC,CAClC,MAAO,UAGT,MAAM,YAAe,EAA4D,CAG/E,OAAO,MAAM,EAAG,KAAK,CAGvB,MAAM,iBACJ,EACA,EACY,CAEZ,MAAU,MACR,qHACD,GAIC,EAAN,cAAsC,CAAuB,CAC3D,mBAAoC,CAClC,MAAO,WAGT,MAAM,YAAe,EAA4D,CAG/E,OAAO,MAAM,EAAG,KAAK,CAGvB,MAAM,iBAAoB,EAA4C,EAA+C,CAGnH,OAAO,MAAM,EAAG,KAAK"} |
| import { BaseAdapter } from "./base.js"; | ||
| import type { | ||
| UniStoreConnection, | ||
| UniStoreOptions, | ||
| SQLiteWasmConfig, | ||
| SQLiteParams, | ||
| SQLiteValue, | ||
| QueryResult, | ||
| RunResult, | ||
| ConnectionType, | ||
| } from "../types.js"; | ||
| import { BroadcastChannel, createLeaderElection } from "broadcast-channel"; | ||
| // Type definitions for SQLite WASM (using unknown for flexibility with actual implementation) | ||
| interface SQLiteStatement { | ||
| bind: (params: unknown) => unknown; | ||
| step(): boolean; | ||
| get(index: number): SQLiteValue; | ||
| getColumnName(index: number): string; | ||
| columnCount: number; | ||
| finalize(): void; | ||
| } | ||
| interface SQLiteDatabase { | ||
| prepare(sql: string): SQLiteStatement; | ||
| exec(sql: string): void; | ||
| changes(): number; | ||
| close(): void; | ||
| pointer: unknown; | ||
| } | ||
| interface SQLiteAPI { | ||
| oo1: { | ||
| DB: new (filename?: string, flags?: string, vfs?: string) => SQLiteDatabase; | ||
| JsStorageDb?: new (filename: string) => SQLiteDatabase; | ||
| OpfsDb?: new (filename: string) => SQLiteDatabase; | ||
| }; | ||
| capi: { | ||
| sqlite3_last_insert_rowid(db: unknown): unknown; | ||
| sqlite3_vfs_find?: (name: string) => unknown; | ||
| }; | ||
| installOpfsSAHPoolVfs?: (options?: { name?: string; directory?: string }) => Promise<unknown>; | ||
| } | ||
| interface SQLiteInitOptions { | ||
| print?: (message: string) => void; | ||
| printErr?: (message: string) => void; | ||
| locateFile?: (filename: string) => string; | ||
| } | ||
| type SQLiteInitModule = (options?: SQLiteInitOptions) => Promise<SQLiteAPI>; | ||
| type SQLiteWorkerPromiser = (type: string, args?: unknown) => Promise<unknown>; | ||
| type SQLiteWorker1PromiserFactory = (config: Record<string, unknown>) => SQLiteWorkerPromiser; | ||
| interface WindowWithSQLite extends Window { | ||
| sqlite3InitModule?: SQLiteInitModule; | ||
| sqlite3InitModuleState?: { | ||
| locateFile: (filename: string) => string; | ||
| }; | ||
| } | ||
| interface LeaderElector { | ||
| hasLeader(): Promise<boolean>; | ||
| awaitLeadership(): Promise<void>; | ||
| die(): Promise<void>; | ||
| } | ||
| interface ExtendedUniStoreOptions extends UniStoreOptions { | ||
| sqlite?: SQLiteWasmConfig; | ||
| } | ||
| // Cache for loaded SQLite modules to avoid reloading | ||
| const sqliteModuleCache = new Map<string, Promise<SQLiteInitModule>>(); | ||
| const sqliteWorkerPromiserCache = new Map<string, Promise<SQLiteWorker1PromiserFactory>>(); | ||
| interface BrowserMessage { | ||
| id: string; | ||
| type?: "query" | "queryRaw" | "run" | "exec" | "transaction" | "response"; | ||
| sql?: string; | ||
| params?: SQLiteParams; | ||
| result?: unknown; | ||
| error?: string; | ||
| } | ||
| function isMessageEvent<T>(event: unknown): event is MessageEvent<T> { | ||
| return typeof event === "object" && event !== null && "data" in (event as Record<string, unknown>); | ||
| } | ||
| type BroadcastChannelWithOrigin<T> = BroadcastChannel<T> & { | ||
| postMessage(msg: T, targetOrigin?: string): Promise<void>; | ||
| }; | ||
| export class BrowserAdapter extends BaseAdapter { | ||
| private channel: BroadcastChannelWithOrigin<BrowserMessage>; | ||
| private elector: LeaderElector | undefined; | ||
| public isLeader = false; // Made public for testing | ||
| private sqlite3: SQLiteAPI | undefined; | ||
| private db: SQLiteDatabase | undefined; | ||
| private workerPromiser: SQLiteWorkerPromiser | undefined; | ||
| private workerPromiserInitPromise: Promise<SQLiteWorkerPromiser> | undefined; | ||
| private workerDbId: string | number | undefined; | ||
| private workerQueue: Promise<void> = Promise.resolve(); | ||
| private workerTxnDepth = 0; | ||
| private activeOpfsVfs: "opfs" | "opfs-sahpool" | undefined; | ||
| private pendingRequests = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void }>(); | ||
| private initialized = false; | ||
| private initPromise?: Promise<void>; | ||
| private sqliteConfig: SQLiteWasmConfig; | ||
| private activeStorageBackend: "opfs" | "localStorage" | "memory" = "memory"; | ||
| constructor(options: ExtendedUniStoreOptions) { | ||
| const path = options.path ?? (options as { name?: string }).name ?? "default"; | ||
| super({ ...options, path }); | ||
| this.options = { ...options, path }; | ||
| this.channel = new BroadcastChannel(`unisqlite-${path}`) as BroadcastChannelWithOrigin<BrowserMessage>; | ||
| this.sqliteConfig = { | ||
| loadStrategy: "global", // Default to global since it's more reliable in browser tests | ||
| storageBackend: "auto", // Default to auto: try OPFS, then localStorage, then memory | ||
| opfsVfsType: "auto", | ||
| cdnBaseUrl: "https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm", | ||
| version: "3.50.1-build1", | ||
| bundlerFriendly: false, | ||
| ...options.sqlite, | ||
| }; | ||
| } | ||
| static async create(options: ExtendedUniStoreOptions): Promise<BrowserAdapter> { | ||
| const adapter = new BrowserAdapter(options); | ||
| await adapter.initialize(); | ||
| return adapter; | ||
| } | ||
| private async loadSQLiteWasm(): Promise<SQLiteInitModule> { | ||
| const cacheKey = `${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl || this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`; | ||
| if (sqliteModuleCache.has(cacheKey)) { | ||
| return sqliteModuleCache.get(cacheKey)!; | ||
| } | ||
| const loadPromise = this.loadSQLiteWasmInternal(); | ||
| sqliteModuleCache.set(cacheKey, loadPromise); | ||
| return loadPromise; | ||
| } | ||
| private async loadSQLiteWasmInternal(): Promise<SQLiteInitModule> { | ||
| const { loadStrategy, wasmUrl, cdnBaseUrl, version, bundlerFriendly } = this.sqliteConfig; | ||
| switch (loadStrategy) { | ||
| case "npm": | ||
| return await this.loadFromNpm(); | ||
| case "cdn": | ||
| return await this.loadFromCdn(cdnBaseUrl!, version!, bundlerFriendly); | ||
| case "url": | ||
| if (!wasmUrl) { | ||
| throw new Error("wasmUrl must be provided when using 'url' loading strategy"); | ||
| } | ||
| return await this.loadFromUrl(wasmUrl); | ||
| case "module": | ||
| return await this.loadAsModule(); | ||
| case "global": | ||
| default: | ||
| return this.loadFromGlobal(); | ||
| } | ||
| } | ||
| private enqueueWorker<T>(fn: () => Promise<T>): Promise<T> { | ||
| const run = async () => fn(); | ||
| const next = this.workerQueue.then(run, run); | ||
| this.workerQueue = next.then( | ||
| () => undefined, | ||
| () => undefined | ||
| ); | ||
| return next; | ||
| } | ||
| private isWorkerDbActive(): boolean { | ||
| return !!this.workerPromiser && this.workerDbId !== undefined; | ||
| } | ||
| private async loadSQLiteWorkerPromiserFactory(): Promise<SQLiteWorker1PromiserFactory> { | ||
| const cacheKey = `worker-${this.sqliteConfig.loadStrategy}-${this.sqliteConfig.wasmUrl || this.sqliteConfig.cdnBaseUrl}-${this.sqliteConfig.version}`; | ||
| if (sqliteWorkerPromiserCache.has(cacheKey)) { | ||
| return sqliteWorkerPromiserCache.get(cacheKey)!; | ||
| } | ||
| const loadPromise = this.loadSQLiteWorkerPromiserFactoryInternal(); | ||
| sqliteWorkerPromiserCache.set(cacheKey, loadPromise); | ||
| return loadPromise; | ||
| } | ||
| private async loadSQLiteWorkerPromiserFactoryInternal(): Promise<SQLiteWorker1PromiserFactory> { | ||
| const { loadStrategy, wasmUrl, cdnBaseUrl, version, bundlerFriendly } = this.sqliteConfig; | ||
| switch (loadStrategy) { | ||
| case "npm": | ||
| return await this.loadWorkerPromiserFromNpm(); | ||
| case "cdn": | ||
| return await this.loadWorkerPromiserFromCdn(cdnBaseUrl!, version!, bundlerFriendly); | ||
| case "url": | ||
| if (!wasmUrl) { | ||
| throw new Error("wasmUrl must be provided when using 'url' loading strategy"); | ||
| } | ||
| return await this.loadWorkerPromiserFromUrl(wasmUrl); | ||
| case "module": | ||
| return await this.loadWorkerPromiserAsModule(); | ||
| case "global": | ||
| default: | ||
| return this.loadWorkerPromiserFromGlobal(); | ||
| } | ||
| } | ||
| private async loadWorkerPromiserFromNpm(): Promise<SQLiteWorker1PromiserFactory> { | ||
| try { | ||
| const module = await import("@sqlite.org/sqlite-wasm"); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }).sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| return promiser; | ||
| } | ||
| throw new Error("sqlite3Worker1Promiser export not found"); | ||
| } catch (error) { | ||
| console.warn("Failed to load SQLite WASM worker promiser from npm, falling back to CDN:", error); | ||
| return this.loadWorkerPromiserFromCdn( | ||
| this.sqliteConfig.cdnBaseUrl!, | ||
| this.sqliteConfig.version!, | ||
| this.sqliteConfig.bundlerFriendly | ||
| ); | ||
| } | ||
| } | ||
| private async loadWorkerPromiserFromCdn( | ||
| baseUrl: string, | ||
| version: string, | ||
| bundlerFriendly?: boolean | ||
| ): Promise<SQLiteWorker1PromiserFactory> { | ||
| const filename = bundlerFriendly ? "sqlite3-bundler-friendly.mjs" : "index.mjs"; | ||
| const urlsToTry = [ | ||
| `${baseUrl}@${version}/${filename}`, | ||
| `${baseUrl}@${version}/dist/${filename}`, | ||
| ]; | ||
| let lastError: unknown; | ||
| for (const url of urlsToTry) { | ||
| try { | ||
| console.log(`Trying to load SQLite WASM worker promiser from: ${url}`); | ||
| const module = await import(url); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }).sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| console.log(`Successfully loaded SQLite WASM worker promiser from: ${url}`); | ||
| return promiser; | ||
| } | ||
| throw new Error("sqlite3Worker1Promiser export not found"); | ||
| } catch (error) { | ||
| lastError = error; | ||
| console.warn(`Failed to load SQLite WASM worker promiser from ${url}:`, error); | ||
| } | ||
| } | ||
| const errorMessage = lastError instanceof Error ? lastError.message : String(lastError); | ||
| throw new Error(`Failed to load sqlite3Worker1Promiser from any CDN source. Last error: ${errorMessage}`); | ||
| } | ||
| private async loadWorkerPromiserFromUrl(url: string): Promise<SQLiteWorker1PromiserFactory> { | ||
| const module = await import(url); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }).sqlite3Worker1Promiser; | ||
| if (typeof promiser !== "function") { | ||
| throw new Error(`sqlite3Worker1Promiser export not found in module loaded from ${url}`); | ||
| } | ||
| return promiser; | ||
| } | ||
| private async loadWorkerPromiserAsModule(): Promise<SQLiteWorker1PromiserFactory> { | ||
| // For ES6 module environments | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts !== "undefined") { | ||
| // Web Worker environment | ||
| throw new Error("ES6 module loading not supported in Web Worker context"); | ||
| } | ||
| // Try common module paths | ||
| const possiblePaths = ["./sqlite3.mjs", "./sqlite3-bundler-friendly.mjs", "/sqlite3.mjs"]; | ||
| for (const path of possiblePaths) { | ||
| try { | ||
| const module = await import(path); | ||
| const promiser = (module as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }).sqlite3Worker1Promiser; | ||
| if (typeof promiser === "function") { | ||
| return promiser; | ||
| } | ||
| } catch (error) { | ||
| console.warn(`Failed to load SQLite WASM worker promiser from ${path}:`, error); | ||
| } | ||
| } | ||
| throw new Error("Could not load sqlite3Worker1Promiser as ES6 module from any known path"); | ||
| } | ||
| private loadWorkerPromiserFromGlobal(): SQLiteWorker1PromiserFactory { | ||
| const globalWithPromiser = globalThis as unknown as { sqlite3Worker1Promiser?: SQLiteWorker1PromiserFactory }; | ||
| if (typeof globalWithPromiser.sqlite3Worker1Promiser === "function") { | ||
| return globalWithPromiser.sqlite3Worker1Promiser; | ||
| } | ||
| throw new Error( | ||
| "SQLite WASM worker promiser not found globally. Please:\n" + | ||
| "1. Install via npm: npm install @sqlite.org/sqlite-wasm and set loadStrategy to 'npm'\n" + | ||
| "2. Or set loadStrategy to 'cdn' for automatic CDN loading\n" + | ||
| "3. Or include SQLite WASM worker promiser script in your HTML before using UniSqlite" | ||
| ); | ||
| } | ||
| private createWorkerFromCdn(options: { installSahpool: boolean }): Worker | undefined { | ||
| if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") { | ||
| return undefined; | ||
| } | ||
| const baseUrl = this.sqliteConfig.cdnBaseUrl; | ||
| const version = this.sqliteConfig.version; | ||
| if (!baseUrl || !version) { | ||
| return undefined; | ||
| } | ||
| const jswasmDir = `${baseUrl}@${version}/sqlite-wasm/jswasm`; | ||
| const sqliteModuleUrl = `${jswasmDir}/sqlite3-bundler-friendly.mjs`; | ||
| const installSahpool = options.installSahpool | ||
| ? [ | ||
| ` if (typeof sqlite3.installOpfsSAHPoolVfs === "function") {`, | ||
| ` await sqlite3.installOpfsSAHPoolVfs({ name: "opfs-sahpool", directory: "/unisqlite-sahpool" });`, | ||
| " }", | ||
| ].join("\n") | ||
| : ""; | ||
| const source = [ | ||
| `import sqlite3InitModule from ${JSON.stringify(sqliteModuleUrl)};`, | ||
| "sqlite3InitModule().then(async (sqlite3) => {", | ||
| " try {", | ||
| installSahpool || " // no-op", | ||
| " } catch (e) {", | ||
| ' console.warn("SAHPool VFS installation failed:", e);', | ||
| " }", | ||
| " sqlite3.initWorker1API();", | ||
| "});", | ||
| "", | ||
| ].join("\n"); | ||
| const blob = new Blob([source], { type: "text/javascript" }); | ||
| const workerUrl = URL.createObjectURL(blob); | ||
| const worker = new Worker(workerUrl, { type: "module" }); | ||
| worker.addEventListener( | ||
| "message", | ||
| () => { | ||
| URL.revokeObjectURL(workerUrl); | ||
| }, | ||
| { once: true } | ||
| ); | ||
| worker.addEventListener( | ||
| "error", | ||
| () => { | ||
| URL.revokeObjectURL(workerUrl); | ||
| }, | ||
| { once: true } | ||
| ); | ||
| return worker; | ||
| } | ||
| private createSahpoolWorker(): Worker | undefined { | ||
| if (this.sqliteConfig.loadStrategy === "cdn") { | ||
| return this.createWorkerFromCdn({ installSahpool: true }); | ||
| } | ||
| return undefined; | ||
| } | ||
| private createCdnWorker(): Worker | undefined { | ||
| if (this.sqliteConfig.loadStrategy === "cdn") { | ||
| return this.createWorkerFromCdn({ installSahpool: false }); | ||
| } | ||
| return undefined; | ||
| } | ||
| private async getOrCreateWorkerPromiser(options?: { worker?: Worker | (() => Worker) }): Promise<SQLiteWorkerPromiser> { | ||
| if (this.workerPromiser) { | ||
| return this.workerPromiser; | ||
| } | ||
| if (this.workerPromiserInitPromise) { | ||
| return await this.workerPromiserInitPromise; | ||
| } | ||
| const factory = await this.loadSQLiteWorkerPromiserFactory(); | ||
| const wantsOpfs = | ||
| this.sqliteConfig.storageBackend === "opfs" || this.sqliteConfig.opfsVfsType === "opfs" || this.sqliteConfig.opfsVfsType === "sahpool"; | ||
| const timeoutMs = this.sqliteConfig.loadStrategy === "global" && !wantsOpfs ? 5000 : 20000; | ||
| this.workerPromiserInitPromise = new Promise<SQLiteWorkerPromiser>((resolve, reject) => { | ||
| let settled = false; | ||
| const timeoutId = setTimeout(() => { | ||
| if (settled) return; | ||
| settled = true; | ||
| reject(new Error(`SQLite worker initialization timed out after ${timeoutMs}ms`)); | ||
| }, timeoutMs); | ||
| const settle = <T>(fn: (value: T) => void, value: T) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| clearTimeout(timeoutId); | ||
| fn(value); | ||
| }; | ||
| const settleError = (error: unknown) => { | ||
| const err = error instanceof Error ? error : new Error(String(error)); | ||
| settle(reject, err); | ||
| }; | ||
| let factoryResult: unknown; | ||
| try { | ||
| factoryResult = factory({ | ||
| ...(options?.worker ? { worker: options.worker } : {}), | ||
| onready: (readyPromiser?: unknown) => { | ||
| if (typeof readyPromiser === "function") { | ||
| settle(resolve, readyPromiser as SQLiteWorkerPromiser); | ||
| return; | ||
| } | ||
| if (typeof factoryResult === "function") { | ||
| settle(resolve, factoryResult as SQLiteWorkerPromiser); | ||
| return; | ||
| } | ||
| if (factoryResult && typeof (factoryResult as { then?: unknown }).then === "function") { | ||
| (factoryResult as Promise<SQLiteWorkerPromiser>).then( | ||
| (p) => settle(resolve, p), | ||
| (e) => settleError(e) | ||
| ); | ||
| return; | ||
| } | ||
| settleError(new Error("sqlite3Worker1Promiser did not provide a usable promiser")); | ||
| }, | ||
| onerror: (event: unknown) => { | ||
| const message = event instanceof Error ? event.message : String(event); | ||
| settleError(new Error(`SQLite worker error: ${message}`)); | ||
| }, | ||
| }); | ||
| if (factoryResult && typeof (factoryResult as { then?: unknown }).then === "function") { | ||
| (factoryResult as Promise<SQLiteWorkerPromiser>).then( | ||
| (p) => settle(resolve, p), | ||
| (e) => settleError(e) | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| settleError(error); | ||
| } | ||
| }); | ||
| const promiser = await this.workerPromiserInitPromise; | ||
| this.workerPromiser = promiser; | ||
| return promiser; | ||
| } | ||
| private getTargetOrigin(): string { | ||
| const origin = globalThis.location?.origin; | ||
| return typeof origin === "string" ? origin : "*"; | ||
| } | ||
| private async loadFromNpm(): Promise<SQLiteInitModule> { | ||
| try { | ||
| // Try to import from npm package | ||
| const module = await import("@sqlite.org/sqlite-wasm"); | ||
| // Handle different module export formats | ||
| return (module.default || (module as unknown as SQLiteInitModule)) as SQLiteInitModule; | ||
| } catch (error) { | ||
| console.warn("Failed to load SQLite WASM from npm, falling back to CDN:", error); | ||
| return this.loadFromCdn( | ||
| this.sqliteConfig.cdnBaseUrl!, | ||
| this.sqliteConfig.version!, | ||
| this.sqliteConfig.bundlerFriendly | ||
| ); | ||
| } | ||
| } | ||
| private async loadFromCdn(baseUrl: string, version: string, bundlerFriendly?: boolean): Promise<SQLiteInitModule> { | ||
| // Use the correct URL structure for @sqlite.org/sqlite-wasm | ||
| const filename = bundlerFriendly ? "sqlite3-bundler-friendly.mjs" : "index.mjs"; | ||
| // List of CDN URLs to try in order | ||
| const urlsToTry = [ | ||
| // Working jsDelivr structure (matches test.html) | ||
| `https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${version}/${filename}`, | ||
| // Alternative with dist directory | ||
| `https://cdn.jsdelivr.net/npm/@sqlite.org/sqlite-wasm@${version}/dist/${filename}`, | ||
| ]; | ||
| let lastError: unknown; | ||
| for (const url of urlsToTry) { | ||
| try { | ||
| console.log(`Trying to load SQLite WASM from: ${url}`); | ||
| const module = await import(url); | ||
| console.log(`Successfully loaded SQLite WASM from: ${url}`); | ||
| return (module.default || module) as SQLiteInitModule; | ||
| } catch (error) { | ||
| lastError = error; | ||
| console.warn(`Failed to load SQLite WASM from ${url}:`, error); | ||
| } | ||
| } | ||
| const errorMessage = lastError instanceof Error ? lastError.message : String(lastError); | ||
| throw new Error(`Failed to load SQLite WASM from any CDN source. Last error: ${errorMessage}`); | ||
| } | ||
| private async loadFromUrl(url: string): Promise<SQLiteInitModule> { | ||
| const module = await import(url); | ||
| return module.default || module.sqlite3InitModule; | ||
| } | ||
| private async loadAsModule(): Promise<SQLiteInitModule> { | ||
| // For ES6 module environments | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts !== "undefined") { | ||
| // Web Worker environment | ||
| throw new Error("ES6 module loading not supported in Web Worker context"); | ||
| } | ||
| // Try common module paths | ||
| const possiblePaths = ["./sqlite3.mjs", "./sqlite3-bundler-friendly.mjs", "/sqlite3.mjs"]; | ||
| for (const path of possiblePaths) { | ||
| try { | ||
| const module = await import(path); | ||
| return module.default || module.sqlite3InitModule; | ||
| } catch (error) { | ||
| console.warn(`Failed to load SQLite WASM from ${path}:`, error); | ||
| } | ||
| } | ||
| throw new Error("Could not load SQLite WASM as ES6 module from any known path"); | ||
| } | ||
| private loadFromGlobal(): SQLiteInitModule { | ||
| if (typeof window !== "undefined") { | ||
| const windowWithSQLite = window as WindowWithSQLite; | ||
| if (windowWithSQLite.sqlite3InitModule) { | ||
| return windowWithSQLite.sqlite3InitModule; | ||
| } | ||
| } | ||
| throw new Error( | ||
| "SQLite WASM module not found globally. Please:\n" + | ||
| "1. Install via npm: npm install @sqlite.org/sqlite-wasm\n" + | ||
| "2. Or set loadStrategy to 'cdn' for automatic CDN loading\n" + | ||
| "3. Or include SQLite WASM script in your HTML before using UniSqlite" | ||
| ); | ||
| } | ||
| private async initialize() { | ||
| if (this.initialized) return; | ||
| if (this.initPromise) return this.initPromise; | ||
| this.initPromise = this.initializeLeaderElection(); | ||
| await this.initPromise; | ||
| this.initialized = true; | ||
| } | ||
| private async initializeLeaderElection() { | ||
| this.elector = createLeaderElection(this.channel) as LeaderElector; | ||
| console.log("Starting leader election for channel:", `unisqlite-${this.options.path}`); | ||
| // Check if we can become leader immediately | ||
| const hasLeader = await (this.elector as { hasLeader: () => Promise<boolean> }).hasLeader(); | ||
| console.log("Has existing leader:", hasLeader); | ||
| if (!hasLeader) { | ||
| // No leader exists, try to become leader | ||
| console.log("No leader exists, attempting to become leader..."); | ||
| await (this.elector as { awaitLeadership: () => Promise<void> }).awaitLeadership(); | ||
| this.isLeader = true; | ||
| console.log("Became leader!"); | ||
| await this.initializeDatabase(); | ||
| } else { | ||
| // Leader already exists, we're a follower | ||
| this.isLeader = false; | ||
| console.log("Leader exists, becoming follower"); | ||
| // Set up to become leader if current leader dies | ||
| void (this.elector as { awaitLeadership: () => Promise<void> }).awaitLeadership().then(async () => { | ||
| console.log("Became leader after previous leader died"); | ||
| this.isLeader = true; | ||
| await this.initializeDatabase(); | ||
| }).catch((error) => { | ||
| console.error("Error while awaiting leadership:", error); | ||
| }); | ||
| } | ||
| // Listen for messages from other tabs | ||
| this.channel.addEventListener("message", (event: MessageEvent<BrowserMessage> | BrowserMessage) => { | ||
| const msg: BrowserMessage = isMessageEvent<BrowserMessage>(event) ? event.data : event; | ||
| console.log("BroadcastChannel message", msg); | ||
| // Leader handles incoming work from followers | ||
| if ( | ||
| this.isLeader && | ||
| msg?.id && | ||
| msg.type && | ||
| ["query", "queryRaw", "run", "exec", "transaction"].includes(msg.type as string) | ||
| ) { | ||
| void this.handleFollowerRequest(msg); | ||
| return; | ||
| } | ||
| if (msg.id && this.pendingRequests.has(msg.id) && (msg.result !== undefined || msg.error)) { | ||
| const { resolve, reject } = this.pendingRequests.get(msg.id)!; | ||
| this.pendingRequests.delete(msg.id); | ||
| if (msg.error) { | ||
| reject(new Error(msg.error)); | ||
| } else { | ||
| resolve(msg.result); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| private async initializeDatabase() { | ||
| console.log(`Loading SQLite WASM using strategy: ${this.sqliteConfig.loadStrategy}`); | ||
| // Load SQLite WASM module | ||
| const sqlite3InitModule = await this.loadSQLiteWasm(); | ||
| // Configure initialization options | ||
| const initOptions: SQLiteInitOptions = { | ||
| print: console.log, | ||
| printErr: console.error, | ||
| }; | ||
| // Use custom locateFile if provided | ||
| if (this.sqliteConfig.locateFile) { | ||
| initOptions.locateFile = this.sqliteConfig.locateFile; | ||
| } else if (typeof window !== "undefined") { | ||
| const windowWithSQLite = window as WindowWithSQLite; | ||
| if (windowWithSQLite.sqlite3InitModuleState?.locateFile) { | ||
| initOptions.locateFile = windowWithSQLite.sqlite3InitModuleState.locateFile; | ||
| } | ||
| } | ||
| console.log("Initializing SQLite WASM module..."); | ||
| this.sqlite3 = await sqlite3InitModule(initOptions); | ||
| if (!this.sqlite3) { | ||
| throw new Error("Failed to initialize SQLite WASM module"); | ||
| } | ||
| const sqlite3 = this.sqlite3; | ||
| console.log("SQLite WASM module initialized successfully"); | ||
| // Database setup based on storage backend preference | ||
| const storageBackend = this.sqliteConfig.storageBackend ?? "auto"; | ||
| const dbFileName = this.options.path === ":memory:" ? ":memory:" : `/unisqlite/${this.options.path}`; | ||
| // If explicitly requesting in-memory storage or path is :memory: | ||
| if (storageBackend === "memory" || this.options.path === ":memory:") { | ||
| console.log("Using in-memory database"); | ||
| this.db = new sqlite3.oo1.DB(":memory:"); | ||
| this.activeStorageBackend = "memory"; | ||
| } else { | ||
| // Try storage backends based on preference | ||
| const dbCreated = await this.tryCreatePersistentDatabase(sqlite3, dbFileName, storageBackend); | ||
| if (!dbCreated) { | ||
| console.log("All persistent storage backends failed, using in-memory database"); | ||
| this.db = new sqlite3.oo1.DB(":memory:"); | ||
| this.activeStorageBackend = "memory"; | ||
| } | ||
| } | ||
| // Enable WAL mode for better performance | ||
| try { | ||
| await this.execInternal("PRAGMA journal_mode=WAL"); | ||
| console.log("Enabled WAL mode"); | ||
| } catch (e) { | ||
| console.warn("Could not enable WAL mode:", e); | ||
| } | ||
| } | ||
| private async tryCreatePersistentDatabase( | ||
| sqlite3: SQLiteAPI, | ||
| dbFileName: string, | ||
| storageBackend: "opfs" | "localStorage" | "auto" | ||
| ): Promise<boolean> { | ||
| // Try OPFS first (if requested or auto) | ||
| if (storageBackend === "opfs" || storageBackend === "auto") { | ||
| const opfsVfsType = this.sqliteConfig.opfsVfsType ?? "auto"; | ||
| const vfsOrder: Array<"opfs-sahpool" | "opfs"> = | ||
| opfsVfsType === "auto" | ||
| ? sqlite3.oo1.OpfsDb | ||
| ? ["opfs"] | ||
| : ["opfs-sahpool", "opfs"] | ||
| : opfsVfsType === "sahpool" | ||
| ? ["opfs-sahpool"] | ||
| : ["opfs"]; | ||
| for (const vfsName of vfsOrder) { | ||
| const created = await this.tryCreateOpfsDatabase(sqlite3, dbFileName, vfsName); | ||
| if (created) { | ||
| return true; | ||
| } | ||
| } | ||
| if (storageBackend === "opfs") { | ||
| throw new Error(this.getOpfsUnavailableError()); | ||
| } | ||
| } | ||
| // Try localStorage (if requested or auto) | ||
| if (storageBackend === "localStorage" || storageBackend === "auto") { | ||
| if (sqlite3.oo1.JsStorageDb) { | ||
| // JsStorageDb only accepts 'local' or 'session' as database names | ||
| const isValidLocalStoragePath = this.options.path === "local" || this.options.path === "session"; | ||
| if (isValidLocalStoragePath) { | ||
| try { | ||
| console.log("Creating localStorage-backed database:", this.options.path); | ||
| this.db = new sqlite3.oo1.JsStorageDb(this.options.path); | ||
| this.activeStorageBackend = "localStorage"; | ||
| console.log("Successfully created localStorage-backed database"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("localStorage database creation failed:", e); | ||
| if (storageBackend === "localStorage") { | ||
| throw new Error(`localStorage database creation failed: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| } else if (storageBackend === "localStorage") { | ||
| throw new Error( | ||
| `localStorage storage backend requires path to be 'local' or 'session', got '${this.options.path}'. ` + | ||
| "Use storageBackend: 'opfs' for custom database names with persistence." | ||
| ); | ||
| } else { | ||
| console.log(`Skipping localStorage: path '${this.options.path}' is not 'local' or 'session'`); | ||
| } | ||
| } else if (storageBackend === "localStorage") { | ||
| throw new Error("JsStorageDb is not available in this environment."); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| private getOpfsUnavailableError(): string { | ||
| return ( | ||
| "OPFS is not available.\n\n" + | ||
| "SQLite WASM OPFS backends only work in Worker contexts. When running on the main thread, UniSQLite " + | ||
| "will try to use SQLite WASM's wrapped-worker API (sqlite3Worker1Promiser) under the hood.\n\n" + | ||
| "Make sure:\n" + | ||
| "- You are in a secure context (HTTPS or localhost)\n" + | ||
| "- For VFS 'opfs': COOP/COEP headers are set and SharedArrayBuffer is available\n" + | ||
| "- Your sqlite load strategy can load sqlite3Worker1Promiser (use sqlite.loadStrategy = 'npm' or 'cdn', or provide it globally)" | ||
| ); | ||
| } | ||
| private async tryCreateOpfsDatabase( | ||
| sqlite3: SQLiteAPI, | ||
| dbFileName: string, | ||
| vfsName: "opfs" | "opfs-sahpool" | ||
| ): Promise<boolean> { | ||
| if (vfsName === "opfs") { | ||
| if (sqlite3.oo1.OpfsDb) { | ||
| try { | ||
| console.log("Creating OPFS-backed database (OpfsDb):", dbFileName); | ||
| this.db = new sqlite3.oo1.OpfsDb(dbFileName); | ||
| this.workerDbId = undefined; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = "opfs"; | ||
| console.log("Successfully created OPFS-backed database (OpfsDb)"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("OPFS database creation via OpfsDb failed:", e); | ||
| } | ||
| } | ||
| // OpfsDb is worker-only in sqlite-wasm; fall back to wrapped worker when unavailable. | ||
| return await this.tryCreateWrappedWorkerOpfsDatabase(dbFileName, vfsName); | ||
| } | ||
| // SAHPool: install VFS (if supported) and open via regular DB with explicit VFS name. | ||
| const sahpoolReady = await this.ensureSahpoolVfs(sqlite3); | ||
| if (sahpoolReady) { | ||
| try { | ||
| console.log("Creating SAHPool OPFS-backed database:", dbFileName); | ||
| this.db = new sqlite3.oo1.DB(dbFileName, "c", "opfs-sahpool"); | ||
| this.workerDbId = undefined; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = "opfs-sahpool"; | ||
| console.log("Successfully created SAHPool OPFS-backed database"); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn("SAHPool OPFS database creation failed:", e); | ||
| } | ||
| } | ||
| return await this.tryCreateWrappedWorkerOpfsDatabase(dbFileName, vfsName); | ||
| } | ||
| private async ensureSahpoolVfs(sqlite3: SQLiteAPI): Promise<boolean> { | ||
| // SAHPool relies on OPFS SyncAccessHandle APIs which are worker-only. | ||
| if (typeof (globalThis as unknown as { importScripts?: unknown }).importScripts === "undefined") { | ||
| return false; | ||
| } | ||
| const vfsName = "opfs-sahpool"; | ||
| try { | ||
| const existing = sqlite3.capi.sqlite3_vfs_find?.(vfsName); | ||
| if (existing) { | ||
| return true; | ||
| } | ||
| } catch (e) { | ||
| console.warn("SAHPool VFS lookup failed:", e); | ||
| } | ||
| if (typeof sqlite3.installOpfsSAHPoolVfs !== "function") { | ||
| return false; | ||
| } | ||
| try { | ||
| await sqlite3.installOpfsSAHPoolVfs({ | ||
| name: vfsName, | ||
| directory: "/unisqlite-sahpool", | ||
| }); | ||
| } catch (e) { | ||
| console.warn("SAHPool VFS installation failed:", e); | ||
| return false; | ||
| } | ||
| try { | ||
| const installed = sqlite3.capi.sqlite3_vfs_find?.(vfsName); | ||
| return !!installed; | ||
| } catch { | ||
| return true; | ||
| } | ||
| } | ||
| private async tryCreateWrappedWorkerOpfsDatabase( | ||
| dbFileName: string, | ||
| vfsName: "opfs" | "opfs-sahpool" | ||
| ): Promise<boolean> { | ||
| try { | ||
| const workerOptions = | ||
| !this.workerPromiser && !this.workerPromiserInitPromise | ||
| ? (() => { | ||
| const worker = vfsName === "opfs-sahpool" ? this.createSahpoolWorker() : this.createCdnWorker(); | ||
| return worker ? { worker } : undefined; | ||
| })() | ||
| : undefined; | ||
| const promiser = await this.getOrCreateWorkerPromiser(workerOptions); | ||
| const openMsg = (await promiser("open", { | ||
| filename: `file:${dbFileName}`, | ||
| vfs: vfsName, | ||
| })) as { dbId?: unknown; result?: { dbId?: unknown; vfs?: string } }; | ||
| const rawDbId = openMsg.dbId ?? openMsg.result?.dbId; | ||
| if (rawDbId === undefined) { | ||
| throw new Error("SQLite worker did not return a dbId"); | ||
| } | ||
| if (typeof rawDbId !== "number" && typeof rawDbId !== "string") { | ||
| throw new Error(`SQLite worker returned an invalid dbId (type=${typeof rawDbId})`); | ||
| } | ||
| this.db = undefined; | ||
| this.workerDbId = rawDbId; | ||
| this.activeStorageBackend = "opfs"; | ||
| this.activeOpfsVfs = vfsName; | ||
| console.log(`Successfully created OPFS-backed database via wrapped worker (${vfsName}):`, dbFileName); | ||
| return true; | ||
| } catch (e) { | ||
| console.warn(`Wrapped-worker OPFS database creation failed (vfs=${vfsName}):`, e); | ||
| return false; | ||
| } | ||
| } | ||
| private normalizeParams(params?: SQLiteParams): SQLiteValue[] | undefined { | ||
| if (!params) return undefined; | ||
| if (Array.isArray(params)) { | ||
| return params; | ||
| } | ||
| // Convert named parameters to array | ||
| // This is a simplified version - real implementation would need to parse SQL | ||
| const keys = Object.keys(params); | ||
| const paramRecord: Record<string, SQLiteValue> = params; | ||
| return keys.map((key) => paramRecord[key]); | ||
| } | ||
| public async execInternal(sql: string, options?: { bypassQueue?: boolean }): Promise<void> { | ||
| if (this.isWorkerDbActive()) { | ||
| const run = async () => { | ||
| if (!this.workerPromiser || this.workerDbId === undefined) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| await this.workerPromiser("exec", { dbId: this.workerDbId, sql }); | ||
| }; | ||
| if (options?.bypassQueue && this.workerTxnDepth > 0) { | ||
| await run(); | ||
| } else { | ||
| await this.enqueueWorker(run); | ||
| } | ||
| return; | ||
| } | ||
| if (!this.db) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| this.db.exec(sql); | ||
| } | ||
| private async executeSql<T>( | ||
| sql: string, | ||
| params?: SQLiteParams, | ||
| options?: { bypassQueue?: boolean } | ||
| ): Promise<QueryResult<T>> { | ||
| try { | ||
| if (this.isWorkerDbActive()) { | ||
| const run = async (): Promise<QueryResult<T>> => { | ||
| if (!this.workerPromiser || this.workerDbId === undefined) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| const normalizedParams = this.normalizeParams(params); | ||
| const execArgs: Record<string, unknown> = { | ||
| dbId: this.workerDbId, | ||
| sql, | ||
| rowMode: "object", | ||
| resultRows: [], | ||
| columnNames: [], | ||
| countChanges: true, | ||
| }; | ||
| if (normalizedParams) { | ||
| execArgs.bind = normalizedParams; | ||
| } | ||
| const execMsg = await this.workerPromiser("exec", execArgs); | ||
| const execOut = (execMsg as { result?: unknown }).result ?? execMsg; | ||
| const execOutRecord = execOut as Record<string, unknown>; | ||
| const rows = Array.isArray(execOutRecord.resultRows) ? (execOutRecord.resultRows as T[]) : []; | ||
| const columns = Array.isArray(execOutRecord.columnNames) ? (execOutRecord.columnNames as string[]) : []; | ||
| const rowsAffected = typeof execOutRecord.changeCount === "number" ? execOutRecord.changeCount : 0; | ||
| // sqlite3Worker1 does not expose sqlite3_last_insert_rowid(), so fetch it via a follow-up query. | ||
| const metaArgs: Record<string, unknown> = { | ||
| dbId: this.workerDbId, | ||
| sql: "SELECT last_insert_rowid() AS lastInsertRowId", | ||
| rowMode: "object", | ||
| resultRows: [], | ||
| columnNames: [], | ||
| }; | ||
| const metaMsg = await this.workerPromiser("exec", metaArgs); | ||
| const metaOut = (metaMsg as { result?: unknown }).result ?? metaMsg; | ||
| const metaOutRecord = metaOut as Record<string, unknown>; | ||
| const metaRows = Array.isArray(metaOutRecord.resultRows) ? (metaOutRecord.resultRows as Array<Record<string, unknown>>) : []; | ||
| const lastInsertRowId = Number(metaRows[0]?.lastInsertRowId ?? 0); | ||
| return { | ||
| rows, | ||
| columns, | ||
| rowsAffected, | ||
| lastInsertRowId, | ||
| }; | ||
| }; | ||
| if (options?.bypassQueue && this.workerTxnDepth > 0) { | ||
| return await run(); | ||
| } | ||
| return await this.enqueueWorker(run); | ||
| } | ||
| if (!this.db || !this.sqlite3) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| const normalizedParams = this.normalizeParams(params); | ||
| const rows: T[] = []; | ||
| let columns: string[] = []; | ||
| const stmt = this.db.prepare(sql); | ||
| const stmtWithMeta = stmt as unknown as { | ||
| getColumnCount?: () => number; | ||
| columnCount?: number; | ||
| getColumnNames?: () => string[]; | ||
| }; | ||
| if (normalizedParams) { | ||
| stmt.bind(normalizedParams); | ||
| } | ||
| // Get column metadata; bundler-friendly builds expose columnCount as a getter | ||
| const columnCount = | ||
| typeof stmtWithMeta.getColumnCount === "function" | ||
| ? stmtWithMeta.getColumnCount() | ||
| : typeof stmtWithMeta.columnCount === "number" | ||
| ? stmtWithMeta.columnCount | ||
| : 0; | ||
| if (columnCount > 0) { | ||
| if (typeof stmtWithMeta.getColumnNames === "function") { | ||
| columns = stmtWithMeta.getColumnNames(); | ||
| } else { | ||
| for (let i = 0; i < columnCount; i++) { | ||
| columns.push((stmt as { getColumnName: (index: number) => string }).getColumnName(i)); | ||
| } | ||
| } | ||
| } | ||
| // Execute and collect results | ||
| while (stmt.step()) { | ||
| const row: Record<string, SQLiteValue> = {}; | ||
| for (let i = 0; i < columnCount; i++) { | ||
| row[columns[i]] = (stmt as { get: (index: number) => SQLiteValue }).get(i); | ||
| } | ||
| rows.push(row as T); | ||
| } | ||
| stmt.finalize(); | ||
| return { | ||
| rows, | ||
| columns, | ||
| rowsAffected: this.db.changes(), | ||
| lastInsertRowId: Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer) || 0), | ||
| }; | ||
| } catch (error: unknown) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| const sqlError = new Error(`SQLite error: ${errorMessage}`); | ||
| if (error instanceof Error) { | ||
| sqlError.cause = error; | ||
| } | ||
| throw sqlError; | ||
| } | ||
| } | ||
| private async handleFollowerRequest(msg: BrowserMessage) { | ||
| try { | ||
| console.log("Handling follower request", msg.type, msg.id); | ||
| let result: unknown; | ||
| switch (msg.type) { | ||
| case "query": | ||
| if (typeof msg.sql !== "string") { | ||
| throw new Error("Missing SQL for query request"); | ||
| } | ||
| const queryResult = await this.executeSql(msg.sql, msg.params); | ||
| result = queryResult.rows; | ||
| break; | ||
| case "queryRaw": | ||
| if (typeof msg.sql !== "string") { | ||
| throw new Error("Missing SQL for queryRaw request"); | ||
| } | ||
| result = await this.executeSql(msg.sql, msg.params); | ||
| break; | ||
| case "run": | ||
| if (typeof msg.sql !== "string") { | ||
| throw new Error("Missing SQL for run request"); | ||
| } | ||
| const runResult = await this.executeSql(msg.sql, msg.params); | ||
| result = { | ||
| rowsAffected: runResult.rowsAffected ?? 0, | ||
| lastInsertRowId: runResult.lastInsertRowId ?? 0, | ||
| }; | ||
| break; | ||
| case "exec": | ||
| if (typeof msg.sql !== "string") { | ||
| throw new Error("Missing SQL for exec request"); | ||
| } | ||
| await this.execInternal(msg.sql); | ||
| result = null; | ||
| break; | ||
| case "transaction": | ||
| // Simplified transaction handling | ||
| await this.execInternal("BEGIN"); | ||
| try { | ||
| // Transaction would be more complex in real implementation | ||
| await this.execInternal("COMMIT"); | ||
| result = null; | ||
| } catch (e) { | ||
| await this.execInternal("ROLLBACK"); | ||
| throw e; | ||
| } | ||
| break; | ||
| } | ||
| await this.channel.postMessage({ | ||
| id: msg.id, | ||
| type: "response", | ||
| result, | ||
| }, this.getTargetOrigin()); | ||
| } catch (error: unknown) { | ||
| const errorMessage = error instanceof Error ? error.message : String(error); | ||
| await this.channel.postMessage({ | ||
| id: msg.id, | ||
| type: "response", | ||
| error: errorMessage, | ||
| }, this.getTargetOrigin()); | ||
| } | ||
| } | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| await this.initialize(); | ||
| if (this.isLeader) { | ||
| const result = await this.executeSql<T>(sql, params); | ||
| return result.rows; | ||
| } else { | ||
| // Send request to leader | ||
| return (await this.sendToLeader("query", sql, params)) as T[]; | ||
| } | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| await this.initialize(); | ||
| if (this.isLeader) { | ||
| return await this.executeSql<T>(sql, params); | ||
| } else { | ||
| return (await this.sendToLeader("queryRaw", sql, params)) as QueryResult<T>; | ||
| } | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| await this.initialize(); | ||
| if (this.isLeader) { | ||
| const result = await this.executeSql(sql, params); | ||
| return { | ||
| rowsAffected: result.rowsAffected!, | ||
| lastInsertRowId: result.lastInsertRowId!, | ||
| }; | ||
| } else { | ||
| return (await this.sendToLeader("run", sql, params)) as RunResult; | ||
| } | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| await this.initialize(); | ||
| if (this.isLeader) { | ||
| await this.execInternal(sql); | ||
| } else { | ||
| await this.sendToLeader("exec", sql); | ||
| } | ||
| } | ||
| private async sendToLeader( | ||
| type: "query" | "queryRaw" | "run" | "exec" | "transaction", | ||
| sql: string, | ||
| params?: SQLiteParams | ||
| ): Promise<unknown> { | ||
| return new Promise((resolve, reject) => { | ||
| const id = crypto.randomUUID(); | ||
| this.pendingRequests.set(id, { resolve, reject }); | ||
| console.log("Sending to leader", { id, type, sql, params }); | ||
| void this.channel.postMessage({ | ||
| id, | ||
| type, | ||
| sql, | ||
| params, | ||
| }, this.getTargetOrigin()); | ||
| // Timeout after 5 seconds | ||
| setTimeout(() => { | ||
| if (this.pendingRequests.has(id)) { | ||
| this.pendingRequests.delete(id); | ||
| reject(new Error("Request timeout")); | ||
| } | ||
| }, 5000); | ||
| }); | ||
| } | ||
| // Public method to access database for transaction adapter | ||
| public getDatabase(): SQLiteDatabase | undefined { | ||
| return this.db; | ||
| } | ||
| // Public method to execute SQL for transaction adapter | ||
| public async executeSQL<T>( | ||
| sql: string, | ||
| params?: SQLiteParams, | ||
| options?: { bypassQueue?: boolean } | ||
| ): Promise<QueryResult<T>> { | ||
| return await this.executeSql<T>(sql, params, options); | ||
| } | ||
| /** | ||
| * Execute a synchronous transaction | ||
| */ | ||
| async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> { | ||
| await this.initialize(); | ||
| if (!this.isLeader) { | ||
| // For simplicity, transactions must be executed on leader | ||
| // In real implementation, we'd need to proxy the entire transaction | ||
| throw new Error("Transactions must be executed on leader tab"); | ||
| } | ||
| if (this.isWorkerDbActive()) { | ||
| return await this.enqueueWorker(async () => { | ||
| this.workerTxnDepth++; | ||
| try { | ||
| await this.execInternal("BEGIN", { bypassQueue: true }); | ||
| const result = fn(new BrowserTransactionAdapter(this, false)); | ||
| const value = result instanceof Promise ? await result : result; | ||
| await this.execInternal("COMMIT", { bypassQueue: true }); | ||
| return value; | ||
| } catch (e) { | ||
| try { | ||
| await this.execInternal("ROLLBACK", { bypassQueue: true }); | ||
| } catch (rollbackError) { | ||
| console.error("Rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } finally { | ||
| this.workerTxnDepth--; | ||
| } | ||
| }); | ||
| } | ||
| if (!this.db) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| this.db.exec("BEGIN"); | ||
| try { | ||
| const result = fn(new BrowserTransactionAdapter(this, false)); | ||
| const value = result instanceof Promise ? await result : result; | ||
| this.db.exec("COMMIT"); | ||
| return value; | ||
| } catch (e) { | ||
| this.db.exec("ROLLBACK"); | ||
| throw e; | ||
| } | ||
| } | ||
| /** | ||
| * Execute an async transaction with manual transaction management | ||
| */ | ||
| async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, options?: { timeoutMs?: number }): Promise<T> { | ||
| await this.initialize(); | ||
| if (this.isLeader) { | ||
| const timeoutMs = options?.timeoutMs ?? 30000; // Default 30 seconds | ||
| if (this.isWorkerDbActive()) { | ||
| return await this.enqueueWorker(async () => { | ||
| this.workerTxnDepth++; | ||
| let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | ||
| let transactionCompleted = false; | ||
| let began = false; | ||
| try { | ||
| await this.execInternal("BEGIN IMMEDIATE", { bypassQueue: true }); | ||
| began = true; | ||
| // Set up timeout | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => { | ||
| if (!transactionCompleted) { | ||
| reject(new Error(`Async transaction timeout after ${timeoutMs}ms`)); | ||
| } | ||
| }, timeoutMs); | ||
| }); | ||
| const transactionAdapter = new BrowserTransactionAdapter(this, true); | ||
| // Race between transaction execution and timeout | ||
| const result = await Promise.race([fn(transactionAdapter), timeoutPromise]); | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| await this.execInternal("COMMIT", { bypassQueue: true }); | ||
| return result; | ||
| } catch (e) { | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| try { | ||
| if (began) { | ||
| await this.execInternal("ROLLBACK", { bypassQueue: true }); | ||
| } | ||
| } catch (rollbackError) { | ||
| console.error("Rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } finally { | ||
| this.workerTxnDepth--; | ||
| } | ||
| }); | ||
| } | ||
| if (!this.db) { | ||
| throw new Error("Database not initialized"); | ||
| } | ||
| this.db.exec("BEGIN IMMEDIATE"); | ||
| let timeoutHandle: ReturnType<typeof setTimeout> | undefined; | ||
| let transactionCompleted = false; | ||
| try { | ||
| // Set up timeout | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timeoutHandle = setTimeout(() => { | ||
| if (!transactionCompleted) { | ||
| reject(new Error(`Async transaction timeout after ${timeoutMs}ms`)); | ||
| } | ||
| }, timeoutMs); | ||
| }); | ||
| const transactionAdapter = new BrowserTransactionAdapter(this, true); | ||
| // Race between transaction execution and timeout | ||
| const result = await Promise.race([fn(transactionAdapter), timeoutPromise]); | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| this.db!.exec("COMMIT"); | ||
| return result; | ||
| } catch (e) { | ||
| transactionCompleted = true; | ||
| if (timeoutHandle) { | ||
| clearTimeout(timeoutHandle); | ||
| } | ||
| try { | ||
| this.db!.exec("ROLLBACK"); | ||
| } catch (rollbackError) { | ||
| console.error("Rollback failed:", rollbackError); | ||
| } | ||
| throw e; | ||
| } | ||
| } else { | ||
| // For simplicity, transactions must be executed on leader | ||
| // In real implementation, we'd need to proxy the entire transaction | ||
| throw new Error("Async transactions must be executed on leader tab"); | ||
| } | ||
| } | ||
| getConnectionType(): ConnectionType { | ||
| return "direct"; | ||
| } | ||
| async close(): Promise<void> { | ||
| await this.initialize(); | ||
| if (this.workerPromiser) { | ||
| const promiser = this.workerPromiser; | ||
| const dbId = this.workerDbId; | ||
| try { | ||
| if (dbId !== undefined) { | ||
| await this.enqueueWorker(async () => { | ||
| await promiser("close", { dbId }); | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| console.warn("Failed to close SQLite worker database:", e); | ||
| } finally { | ||
| this.workerDbId = undefined; | ||
| const worker = (promiser as unknown as { worker?: { terminate?: () => void } }).worker; | ||
| if (worker?.terminate) { | ||
| worker.terminate(); | ||
| } | ||
| this.workerPromiser = undefined; | ||
| } | ||
| } | ||
| if (this.db) { | ||
| (this.db as { close: () => void }).close(); | ||
| this.db = undefined; | ||
| } | ||
| if (this.elector) { | ||
| await (this.elector as { die: () => Promise<void> }).die(); | ||
| } | ||
| await this.channel.close(); | ||
| } | ||
| /** | ||
| * Get information about the loaded SQLite WASM configuration and current state. | ||
| * | ||
| * @returns Object containing: | ||
| * - `config`: The SQLiteWasmConfig used to initialize the adapter | ||
| * - `isInitialized`: Whether the adapter has been initialized | ||
| * - `isLeader`: Whether this tab is the leader (has the database connection) | ||
| * - `hasDatabase`: Whether a database connection exists | ||
| * - `hasSQLite3`: Whether the SQLite WASM module is loaded | ||
| * - `usesWorker`: Whether UniSQLite is using a wrapped worker for SQLite operations | ||
| * - `activeStorageBackend`: The storage backend currently in use ('opfs', 'localStorage', or 'memory') | ||
| * - `activeOpfsVfs`: The OPFS VFS name currently in use ('opfs' or 'opfs-sahpool'), when applicable | ||
| * | ||
| * @example | ||
| * const info = db.getSQLiteInfo(); | ||
| * console.log('Storage:', info.activeStorageBackend); // 'opfs' | ||
| * console.log('Is leader:', info.isLeader); // true | ||
| */ | ||
| public getSQLiteInfo() { | ||
| return { | ||
| config: this.sqliteConfig, | ||
| isInitialized: this.initialized, | ||
| isLeader: this.isLeader, | ||
| hasDatabase: !!this.db || this.workerDbId !== undefined, | ||
| hasSQLite3: !!this.sqlite3, | ||
| usesWorker: this.isWorkerDbActive(), | ||
| activeStorageBackend: this.activeStorageBackend, | ||
| activeOpfsVfs: this.activeOpfsVfs, | ||
| }; | ||
| } | ||
| /** | ||
| * Get the active storage backend being used for persistence. | ||
| * | ||
| * This indicates which storage mechanism is currently being used: | ||
| * - `'opfs'`: Origin Private File System (persistent, supports custom names) | ||
| * - `'localStorage'`: localStorage-backed storage (persistent, limited names) | ||
| * - `'memory'`: In-memory storage (not persistent) | ||
| * | ||
| * @returns The active storage backend type | ||
| * | ||
| * @example | ||
| * const db = await openStore({ | ||
| * path: 'my-workspace.db', | ||
| * sqlite: { storageBackend: 'auto' } | ||
| * }); | ||
| * | ||
| * const backend = db.getStorageBackend(); | ||
| * if (backend === 'memory') { | ||
| * console.warn('Data will not persist across page reloads'); | ||
| * } | ||
| */ | ||
| public getStorageBackend(): "opfs" | "localStorage" | "memory" { | ||
| return this.activeStorageBackend; | ||
| } | ||
| } | ||
| class BrowserTransactionAdapter implements UniStoreConnection { | ||
| constructor( | ||
| private parent: BrowserAdapter, | ||
| private isAsync: boolean | ||
| ) { } | ||
| async query<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<T[]> { | ||
| const result = await this.parent.executeSQL(sql, params, { bypassQueue: true }); | ||
| return result.rows as T[]; | ||
| } | ||
| async queryRaw<T = Record<string, SQLiteValue>>(sql: string, params?: SQLiteParams): Promise<QueryResult<T>> { | ||
| return await this.parent.executeSQL<T>(sql, params, { bypassQueue: true }); | ||
| } | ||
| async run(sql: string, params?: SQLiteParams): Promise<RunResult> { | ||
| const result = await this.parent.executeSQL(sql, params, { bypassQueue: true }); | ||
| return { | ||
| rowsAffected: result.rowsAffected || 0, | ||
| lastInsertRowId: result.lastInsertRowId || 0, | ||
| }; | ||
| } | ||
| async exec(sql: string): Promise<void> { | ||
| await this.parent.execInternal(sql, { bypassQueue: true }); | ||
| } | ||
| getConnectionType(): ConnectionType { | ||
| return "syncTxn"; | ||
| } | ||
| async transaction<T>(fn: (tx: UniStoreConnection) => Promise<T> | T): Promise<T> { | ||
| // For syncTxn, we can execute the transaction using itself as the connection | ||
| return await fn(this); | ||
| } | ||
| async asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>): Promise<T> { | ||
| if (!this.isAsync) { | ||
| throw new Error( | ||
| "asyncTransaction is not supported in syncTxn connections. Use transaction() instead or create a direct connection." | ||
| ); | ||
| } else { | ||
| return await fn(this); | ||
| } | ||
| } | ||
| async close(): Promise<void> { | ||
| // No-op for transaction | ||
| } | ||
| } | ||
| // Export types for better developer experience | ||
| export type { SQLiteWasmConfig, ExtendedUniStoreOptions }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
673374
45.81%84
13.51%5819
43.89%499
63.07%33
-2.94%14
7.69%11
10%+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed