Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

@loro-dev/unisqlite

Package Overview
Dependencies
Maintainers
2
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@loro-dev/unisqlite - npm Package Compare versions

Comparing version
0.3.0
to
0.4.0
dist/browser2.cjs.map

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

+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));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,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"}

@@ -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

@@ -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 +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 +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,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,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,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 +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;

@@ -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

@@ -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 @@ *

@@ -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 @@ *

{
"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";

@@ -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);
}

@@ -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 @@

@@ -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 };