From 3b0a76d3f94b05c247c3e3ac0d61dfbd87561bd5 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 27 Aug 2024 16:21:23 +0200 Subject: [PATCH 01/25] Add wa-sqlite implementation. --- package.json | 2 +- packages/api/src/impl.ts | 14 +- packages/api/test/src/impl-tests.ts | 14 +- packages/driver-tests/src/driver-tests.ts | 14 +- packages/driver-tests/src/test.ts | 2 +- packages/wa-sqlite-driver/package.json | 33 + packages/wa-sqlite-driver/src/index.ts | 1 + .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 304 ++ packages/wa-sqlite-driver/tsconfig.json | 14 + packages/wa-sqlite-driver/vitest.config.ts | 25 + pnpm-lock.yaml | 2445 +++++++++++++++-- test/src/wa-sqlite.test.ts | 14 + 12 files changed, 2647 insertions(+), 235 deletions(-) create mode 100644 packages/wa-sqlite-driver/package.json create mode 100644 packages/wa-sqlite-driver/src/index.ts create mode 100644 packages/wa-sqlite-driver/src/wa-sqlite-driver.ts create mode 100644 packages/wa-sqlite-driver/tsconfig.json create mode 100644 packages/wa-sqlite-driver/vitest.config.ts create mode 100644 test/src/wa-sqlite.test.ts diff --git a/package.json b/package.json index 2848d6a..f316ed3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "ts-node": "^10.9.2", "tsx": "^4.16.2", "typescript": "^5.4.5", - "vitest": "^1.5.0" + "vitest": "^2.0.5" }, "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" } diff --git a/packages/api/src/impl.ts b/packages/api/src/impl.ts index 8edff3d..34f42c8 100644 --- a/packages/api/src/impl.ts +++ b/packages/api/src/impl.ts @@ -303,12 +303,12 @@ export class ConnectionImpl implements SqliteConnection { } async begin(options?: TransactionOptions): Promise { - await this.init(); + this.init(); if ((options?.type ?? 'exclusive') == 'exclusive') { - await this._beginExclusive!.select(); + await this._beginExclusive!.run(); } else { - await this._begin!.select(); + await this._begin!.run(); } return new BeginTransactionImpl(this); @@ -321,18 +321,18 @@ export class ConnectionImpl implements SqliteConnection { this.init(); if ((options?.type ?? 'exclusive') == 'exclusive') { - await this._beginExclusive!.select(); + await this._beginExclusive!.run(); } else { - await this._begin!.select(); + await this._begin!.run(); } try { const tx = new TransactionImpl(this); const result = await callback(tx); - await this.commit!.select(); + await this.commit!.run(); return result; } catch (e) { - await this.rollback!.select(); + await this.rollback!.run(); throw e; } } diff --git a/packages/api/test/src/impl-tests.ts b/packages/api/test/src/impl-tests.ts index d09c847..58f8716 100644 --- a/packages/api/test/src/impl-tests.ts +++ b/packages/api/test/src/impl-tests.ts @@ -12,13 +12,13 @@ export function describeImplTests( let dbPath: string; const open = async () => { - const dir = path.dirname(dbPath); - try { - await fs.mkdir(dir); - } catch (e) {} - try { - await fs.rm(dbPath); - } catch (e) {} + // const dir = path.dirname(dbPath); + // try { + // await fs.mkdir(dir); + // } catch (e) {} + // try { + // await fs.rm(dbPath); + // } catch (e) {} return factory(dbPath); }; diff --git a/packages/driver-tests/src/driver-tests.ts b/packages/driver-tests/src/driver-tests.ts index 6e185e5..6c8452f 100644 --- a/packages/driver-tests/src/driver-tests.ts +++ b/packages/driver-tests/src/driver-tests.ts @@ -19,13 +19,13 @@ export function describeDriverTests( let dbPath: string; const open = async () => { - const dir = path.dirname(dbPath); - try { - await fs.mkdir(dir); - } catch (e) {} - try { - await fs.rm(dbPath); - } catch (e) {} + // const dir = path.dirname(dbPath); + // try { + // await fs.mkdir(dir); + // } catch (e) {} + // try { + // await fs.rm(dbPath); + // } catch (e) {} const db = factory(dbPath); return db; }; diff --git a/packages/driver-tests/src/test.ts b/packages/driver-tests/src/test.ts index c929f99..9845255 100644 --- a/packages/driver-tests/src/test.ts +++ b/packages/driver-tests/src/test.ts @@ -5,7 +5,7 @@ export interface TestContext { fullName: string; } -export const isVitest = process.env.VITEST == 'true'; +export const isVitest = true || process.env.VITEST == 'true'; export const isMocha = !isVitest; let testImpl, describeImpl, beforeEachImpl; diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json new file mode 100644 index 0000000..9a85276 --- /dev/null +++ b/packages/wa-sqlite-driver/package.json @@ -0,0 +1,33 @@ +{ + "name": "@sqlite-js/wa-sqlite-driver", + "version": "0.0.1", + "description": "", + "type": "module", + "scripts": { + "build": "tsc -b", + "test": "pnpm build && vitest", + "clean": "tsc -b --clean && tsc -b ./test/tsconfig.json --clean && rm -rf lib test/lib" + }, + "exports": { + ".": "./lib/index.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@journeyapps/wa-sqlite": "^0.3.0", + "@sqlite-js/driver": "workspace:^", + "async-mutex": "^0.5.0" + }, + "devDependencies": { + "vite-plugin-top-level-await": "^1.4.2", + "vite-plugin-wasm": "^3.3.0", + "@sqlite-js/driver-tests": "workspace:^", + "@types/node": "^22.3.0", + "typescript": "^5.5.4", + "@vitest/browser": "^2.0.5", + "vitest": "^2.0.5", + "playwright": "^1.45.3", + "webdriverio": "^8.39.1" + } +} diff --git a/packages/wa-sqlite-driver/src/index.ts b/packages/wa-sqlite-driver/src/index.ts new file mode 100644 index 0000000..9c628f2 --- /dev/null +++ b/packages/wa-sqlite-driver/src/index.ts @@ -0,0 +1 @@ +export * from './wa-sqlite-driver.js'; diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts new file mode 100644 index 0000000..191688a --- /dev/null +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -0,0 +1,304 @@ +import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; +import * as SQLite from '@journeyapps/wa-sqlite'; +import { + PrepareOptions, + ResetOptions, + SqliteChanges, + SqliteDriverConnection, + SqliteDriverConnectionPool, + SqliteDriverStatement, + SqliteParameterBinding, + SqliteRow, + SqliteStepResult, + StepOptions, + UpdateListener +} from '@sqlite-js/driver'; +import { LazyConnectionPool } from '@sqlite-js/driver/util'; +import { SqliteError } from '@sqlite-js/driver'; +import * as mutex from 'async-mutex'; + +// Initialize SQLite. +const module = await SQLiteESMFactory(); +const sqlite3 = SQLite.Factory(module); + +export function waSqlitePool(path: string): SqliteDriverConnectionPool { + return new LazyConnectionPool(async () => { + return await WaSqliteConnection.open(path); + }); +} + +// // Register a custom file system. +// const vfs = await IDBBatchAtomicVFS.create('hello', module); +// // @ts-ignore +// sqlite3.vfs_register(vfs, true); + +const m = new mutex.Mutex(); + +class StatementImpl implements SqliteDriverStatement { + private preparePromise: Promise; + private bindPromise?: Promise; + private columns: string[] = []; + + private stringRef?: number; + private statementRef?: number; + private done = false; + + constructor( + private db: number, + public source: string, + public options: PrepareOptions + ) { + this.preparePromise = this.prepare(); + } + + async prepare() { + await m.runExclusive(() => this._prepare()); + } + + async _prepare() { + try { + this.stringRef = sqlite3.str_new(this.db, this.source); + const strValue = sqlite3.str_value(this.stringRef); + const r = await sqlite3.prepare_v2(this.db, strValue); + if (r == null) { + throw new Error('could not prepare'); + } + + this.statementRef = r?.stmt; + this.columns = sqlite3.column_names(this.statementRef!); + } catch (e: any) { + throw new SqliteError({ + code: 'SQLITE_ERROR', + message: e.message + }); + } + } + + async getColumns(): Promise { + await this.preparePromise; + return sqlite3.column_names(this.statementRef!); + } + + bind(parameters: SqliteParameterBinding): void { + this.bindPromise = this.preparePromise.then(async () => { + await m.runExclusive(() => this.bindImpl(parameters)); + }); + } + + bindImpl(parameters: SqliteParameterBinding): void { + if (Array.isArray(parameters)) { + const count = sqlite3.bind_parameter_count(this.statementRef!); + let pi = 0; + for (let i = 0; i < count; i++) { + const name = sqlite3.bind_parameter_name(this.statementRef!, i + 1); + if (name == '') { + const value = parameters[pi]; + pi++; + if (typeof value != 'undefined') { + sqlite3.bind(this.statementRef!, i + 1, value); + } + } + } + + for (let i = 0; i < parameters.length; i++) { + const value = parameters[i]; + if (typeof value !== 'undefined') { + sqlite3.bind(this.statementRef!, i + 1, value); + } + } + } else if (parameters != null) { + const count = sqlite3.bind_parameter_count(this.statementRef!); + for (let i = 0; i < count; i++) { + const name = sqlite3.bind_parameter_name(this.statementRef!, i + 1); + if (name != '') { + if (name in parameters) { + const value = parameters[name]; + sqlite3.bind(this.statementRef!, i + 1, value); + } else if (name.substring(1) in parameters) { + // Removes the prefix of ? : @ $ + const value = parameters[name.substring(1)]; + sqlite3.bind(this.statementRef!, i + 1, value); + } + } + } + } + } + + async step(n?: number, options?: StepOptions): Promise { + await this.preparePromise; + return await m.runExclusive(() => this._step(n, options)); + } + + async _step(n?: number, options?: StepOptions): Promise { + await this.preparePromise; + + try { + if (this.done) { + return { done: true }; + } + + const stmt = this.statementRef!; + + let rows: SqliteRow[] = []; + + const mapValue = (value: any) => { + if (typeof value == 'number') { + return this.options.bigint ? BigInt(value) : value; + } else if (typeof value == 'bigint') { + return this.options.bigint ? value : Number(value); + } else { + return value; + } + }; + const mapRow = this.options.rawResults + ? (row: any) => row.map(mapValue) + : (row: any[]) => { + return Object.fromEntries( + this.columns.map((c, i) => [c, mapValue(row[i])]) + ); + }; + if (n == null) { + while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) { + const row = sqlite3.row(stmt); + rows.push(mapRow(row)); + } + this.done = true; + return { rows: rows, done: true }; + } else { + while ( + rows.length < n && + (await sqlite3.step(stmt)) === SQLite.SQLITE_ROW + ) { + const row = sqlite3.row(stmt); + rows.push(mapRow(row)); + } + const done = rows.length < n; + this.done = done; + return { rows: rows, done: done }; + } + } catch (e: any) { + throw new SqliteError({ + code: 'SQLITE_ERROR', + message: e.message + }); + } + } + + async _finalize() { + await this.preparePromise; + + if (this.statementRef) { + sqlite3.finalize(this.statementRef); + this.statementRef = undefined; + } + if (this.stringRef) { + sqlite3.str_finish(this.stringRef); + this.stringRef = undefined; + } + } + + finalize(): void { + this._finalize(); + } + + reset(options?: ResetOptions): void { + this.preparePromise.finally(() => { + this.done = false; + sqlite3.reset(this.statementRef!); + + if (options?.clearBindings) { + // No native clear_bidings? + const count = sqlite3.bind_parameter_count(this.statementRef!); + for (let i = 0; i < count; i++) { + sqlite3.bind_null(this.statementRef!, i + 1); + } + } + }); + } + + async run(options?: StepOptions): Promise { + return await m.runExclusive(() => this._run(options)); + } + + async _run(options?: StepOptions): Promise { + await this.preparePromise; + + try { + this.reset(); + const stmt = this.statementRef!; + while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {} + + const changes = sqlite3.changes(this.db); + const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + + return { changes, lastInsertRowId }; + } catch (e: any) { + throw new SqliteError({ + code: 'SQLITE_ERROR', + message: e.message + }); + } finally { + this.reset(); + } + } + + [Symbol.dispose](): void { + this.finalize(); + } +} + +export class WaSqliteConnection implements SqliteDriverConnection { + db: number; + + statements = new Set(); + + static async open(filename: string): Promise { + // Open the database. + const db = await sqlite3.open_v2(filename); + return new WaSqliteConnection(db); + } + + constructor(db: number) { + this.db = db; + } + + async close() { + await m.runExclusive(async () => { + for (let statement of this.statements) { + if (statement.options.persist) { + statement.finalize(); + } + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + await sqlite3.close(this.db); + }); + } + + async getLastChanges(): Promise { + const changes = sqlite3.changes(this.db); + const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + + return { changes, lastInsertRowId }; + } + + prepare(sql: string, options?: PrepareOptions): StatementImpl { + const st = new StatementImpl(this.db, sql, options ?? {}); + // TODO: cleanup on finalize + this.statements.add(st); + return st; + } + + dispose(): void { + // No-op + } + + onUpdate( + listener: UpdateListener, + options?: + | { tables?: string[] | undefined; batchLimit?: number | undefined } + | undefined + ): () => void { + throw new Error('not implemented'); + } +} diff --git a/packages/wa-sqlite-driver/tsconfig.json b/packages/wa-sqlite-driver/tsconfig.json new file mode 100644 index 0000000..f8e62d7 --- /dev/null +++ b/packages/wa-sqlite-driver/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + // FIXME: issues with @journeyapps/wa-sqlite definitions + "strict": false + }, + "include": ["src"], + "references": [{ "path": "../driver" }, { "path": "../driver-tests" }] +} diff --git a/packages/wa-sqlite-driver/vitest.config.ts b/packages/wa-sqlite-driver/vitest.config.ts new file mode 100644 index 0000000..06797bd --- /dev/null +++ b/packages/wa-sqlite-driver/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config'; +import wasm from 'vite-plugin-wasm'; +import topLevelAwait from 'vite-plugin-top-level-await'; + +export default defineConfig({ + esbuild: { target: 'es2022' }, + plugins: [wasm()], + optimizeDeps: { + // Don't optimise these packages as they contain web workers and WASM files. + // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 + exclude: ['@journeyapps/wa-sqlite'], + include: [] + }, + test: { + // environment: 'node', + // include: ['test/src/**/*.test.ts'], + browser: { + enabled: true, + provider: 'webdriverio', + name: 'chrome' + // provider: 'playwright', + // name: 'chromium' + } + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40e2e3d..986a2e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 3.3.3 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) + version: 10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.5.4) tsx: specifier: ^4.16.2 version: 4.17.0 @@ -36,8 +36,8 @@ importers: specifier: ^5.4.5 version: 5.5.4 vitest: - specifier: ^1.5.0 - version: 1.6.0(@types/node@20.14.15) + specifier: ^2.0.5 + version: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) benchmarks: dependencies: @@ -90,7 +90,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0) + version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) packages/better-sqlite3-driver: dependencies: @@ -112,7 +112,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0) + version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) packages/driver: devDependencies: @@ -133,14 +133,54 @@ importers: version: 10.7.3 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0) + version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + devDependencies: + '@types/node': + specifier: ^22.3.0 + version: 22.3.0 + typescript: + specifier: ^5.5.4 + version: 5.5.4 + + packages/wa-sqlite-driver: + dependencies: + '@journeyapps/wa-sqlite': + specifier: ^0.3.0 + version: 0.3.0 + '@sqlite-js/driver': + specifier: workspace:^ + version: link:../driver + async-mutex: + specifier: ^0.5.0 + version: 0.5.0 devDependencies: + '@sqlite-js/driver-tests': + specifier: workspace:^ + version: link:../driver-tests '@types/node': specifier: ^22.3.0 version: 22.3.0 + '@vitest/browser': + specifier: ^2.0.5 + version: 2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13)) + playwright: + specifier: ^1.45.3 + version: 1.46.1 typescript: specifier: ^5.5.4 version: 5.5.4 + vite-plugin-top-level-await: + specifier: ^1.4.2 + version: 1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@22.3.0)) + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.3.0(vite@5.4.1(@types/node@22.3.0)) + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + webdriverio: + specifier: ^8.39.1 + version: 8.40.3(encoding@0.1.13) packages: @@ -160,6 +200,19 @@ packages: resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.25.4': + resolution: {integrity: sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==} + engines: {node: '>=6.9.0'} + + '@bundled-es-modules/cookie@2.0.0': + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -449,6 +502,26 @@ packages: '@gar/promisify@1.1.3': resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@inquirer/confirm@3.1.22': + resolution: {integrity: sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==} + engines: {node: '>=18'} + + '@inquirer/core@9.0.10': + resolution: {integrity: sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.5': + resolution: {integrity: sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==} + engines: {node: '>=18'} + + '@inquirer/type@1.5.2': + resolution: {integrity: sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==} + engines: {node: '>=18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jest/expect-utils@29.7.0': resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -461,6 +534,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@journeyapps/wa-sqlite@0.3.0': + resolution: {integrity: sha512-LQMjcMh92myqzq9kpKFJJ+t1zY7owHTq8TvVYG83luCKzaZepNk86jNB/56fb/vCEy1PQBRc/cI7BTt10SfItA==} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -482,6 +558,10 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mswjs/interceptors@0.29.1': + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} + '@npmcli/fs@1.1.1': resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} @@ -490,6 +570,39 @@ packages: engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.25': + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + + '@promptbook/utils@0.67.5': + resolution: {integrity: sha512-aYKt+Rl4A8XkAsQK2klMifne97ovySnTku5DykwgQ2tLDFpepR5RT/O4JciVeUaaH2856G1HngM9J9FfcGQP8g==} + + '@puppeteer/browsers@1.9.1': + resolution: {integrity: sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==} + engines: {node: '>=16.3.0'} + hasBin: true + + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.20.0': resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} cpu: [arm] @@ -573,10 +686,106 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@swc/core-darwin-arm64@1.7.18': + resolution: {integrity: sha512-MwLc5U+VGPMZm8MjlFBjEB2wyT1EK0NNJ3tn+ps9fmxdFP+PL8EpMiY1O1F2t1ydy2OzBtZz81sycjM9RieFBg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.7.18': + resolution: {integrity: sha512-IkukOQUw7/14VkHp446OkYGCZEHqZg9pTmTdBawlUyz2JwZMSn2VodCl7aFSdGCsU4Cwni8zKA8CCgkCCAELhw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.7.18': + resolution: {integrity: sha512-ATnb6jJaBeXCqrTUawWdoOy7eP9SCI7UMcfXlYIMxX4otKKspLPAEuGA5RaNxlCcj9ObyO0J3YGbtZ6hhD2pjg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.7.18': + resolution: {integrity: sha512-poHtH7zL7lEp9K2inY90lGHJABWxURAOgWNeZqrcR5+jwIe7q5KBisysH09Zf/JNF9+6iNns+U0xgWTNJzBuGA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.7.18': + resolution: {integrity: sha512-qnNI1WmcOV7Wz1ZDyK6WrOlzLvJ01rnni8ec950mMHWkLRMP53QvCvhF3S+7gFplWBwWJTOOPPUqJp/PlSxWyQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.7.18': + resolution: {integrity: sha512-x9SCqCLzwtlqtD5At3I1a7Gco+EuXnzrJGoucmkpeQohshHuwa+cskqsXO6u1Dz0jXJEuHbBZB9va1wYYfjgFg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.7.18': + resolution: {integrity: sha512-qtj8iOpMMgKjzxTv+islmEY0JBsbd93nka0gzcTTmGZxKtL5jSUsYQvkxwNPZr5M9NU1fgaR3n1vE6lFmtY0IQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.7.18': + resolution: {integrity: sha512-ltX/Ol9+Qu4SXmISCeuwVgAjSa8nzHTymknpozzVMgjXUoZMoz6lcynfKL1nCh5XLgqh0XNHUKLti5YFF8LrrA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.7.18': + resolution: {integrity: sha512-RgTcFP3wgyxnQbTCJrlgBJmgpeTXo8t807GU9GxApAXfpLZJ3swJ2GgFUmIJVdLWyffSHF5BEkF3FmF6mtH5AQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.7.18': + resolution: {integrity: sha512-XbZ0wAgzR757+DhQcnv60Y/bK9yuWPhDNRQVFFQVRsowvK3+c6EblyfUSytIidpXgyYFzlprq/9A9ZlO/wvDWw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.7.18': + resolution: {integrity: sha512-qL9v5N5S38ijmqiQRvCFUUx2vmxWT/JJ2rswElnyaHkOHuVoAFhBB90Ywj4RKjh3R0zOjhEcemENTyF3q3G6WQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.12': + resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -589,12 +798,21 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/better-sqlite3@7.6.11': resolution: {integrity: sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -607,6 +825,9 @@ packages: '@types/mocha@10.0.7': resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node@20.14.15': resolution: {integrity: sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==} @@ -616,14 +837,44 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/which@2.0.2': + resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} + + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@vitest/expect@1.6.0': - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitest/browser@2.0.5': + resolution: {integrity: sha512-VbOYtu/6R3d7ASZREcrJmRY/sQuRFO9wMVsEDqfYbWiJRh2fDNi8CL1Csn7Ux31pOcPmmM5QvzFCMpiojvVh8g==} + peerDependencies: + playwright: '*' + safaridriver: '*' + vitest: 2.0.5 + webdriverio: '*' + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} @@ -631,33 +882,56 @@ packages: '@vitest/pretty-format@2.0.5': resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} - '@vitest/runner@2.0.5': resolution: {integrity: sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==} - '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} - '@vitest/snapshot@2.0.5': resolution: {integrity: sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==} - '@vitest/spy@1.6.0': - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} - '@vitest/spy@2.0.5': resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/utils@1.6.0': - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} - '@vitest/utils@2.0.5': resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + '@wdio/config@8.40.3': + resolution: {integrity: sha512-HIi+JnHEDAExhzGRQuZOXw1HWIpe/bsVFHwNISJhY6wS4Nijaigmegs2p14Rv16ydOF19hGrxdKsl8k5STIP2A==} + engines: {node: ^16.13 || >=18} + + '@wdio/logger@8.38.0': + resolution: {integrity: sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==} + engines: {node: ^16.13 || >=18} + + '@wdio/logger@9.0.4': + resolution: {integrity: sha512-b6gcu0PTVb3fgK4kyAH/k5UUWN5FOUdAfhA4PAY/IZvxZTMFYMqnrZb0WRWWWqL6nu9pcrOVtCOdPBvj0cb+Nw==} + engines: {node: '>=18'} + + '@wdio/protocols@8.40.3': + resolution: {integrity: sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==} + + '@wdio/repl@8.40.3': + resolution: {integrity: sha512-mWEiBbaC7CgxvSd2/ozpbZWebnRIc8KRu/J81Hlw/txUWio27S7IpXBlZGVvhEsNzq0+cuxB/8gDkkXvMPbesw==} + engines: {node: ^16.13 || >=18} + + '@wdio/types@8.40.3': + resolution: {integrity: sha512-zK17uyON3Ise3m+XwiF5VrrdZcXXmvqB8AWXoKe88DiksFUPMVoCOuVL2SSX1KnA2YLlZBA55qcFZT99GORVKQ==} + engines: {node: ^16.13 || >=18} + + '@wdio/utils@8.40.3': + resolution: {integrity: sha512-pv/848KGfPN3YXU4QRfTYGkAu4/lejIfoGzGpvGNDcACiVxgZhyRZkJ2xVaSnGaXzF0R7pMozrkU5/DnEhcxMg==} + engines: {node: ^16.13 || >=18} + + '@zip.js/zip.js@2.7.51': + resolution: {integrity: sha512-RKHaebzZZgQkUuzb49/qweN69e8Np9AUZ9QygydDIrbG1njypSAKwkeqIVeuf2JVGBDyB7Z9HKvzPgYrSlv9gw==} + engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-walk@8.3.3: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} @@ -671,6 +945,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + agentkeepalive@4.5.0: resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} engines: {node: '>= 8.0.0'} @@ -683,10 +961,18 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -699,6 +985,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -706,6 +996,14 @@ packages: aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -717,19 +1015,51 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.4.2: + resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} + + bare-fs@2.3.1: + resolution: {integrity: sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==} + + bare-os@2.4.0: + resolution: {integrity: sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==} + + bare-path@2.1.3: + resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} + + bare-stream@2.2.0: + resolution: {integrity: sha512-+o9MG5bPRRBlkVSpfFlMag3n7wMaIZb4YZasU2+/96f+3HTQ4F9DKQeu3K/Sjz1W0umu6xvVq1ON0ipWdMlr3A==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + better-sqlite3@11.1.2: resolution: {integrity: sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==} @@ -756,9 +1086,19 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -767,14 +1107,18 @@ packages: resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} engines: {node: '>= 10'} + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - chai@5.1.1: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} engines: {node: '>=12'} @@ -787,8 +1131,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} @@ -805,6 +1150,11 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + chromium-bidi@0.5.8: + resolution: {integrity: sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==} + peerDependencies: + devtools-protocol: '*' + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -813,9 +1163,21 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -833,22 +1195,69 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css-shorthand-properties@1.1.1: + resolution: {integrity: sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==} + + css-value@0.0.1: + resolution: {integrity: sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.6: resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} engines: {node: '>=6.0'} @@ -862,14 +1271,14 @@ packages: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} engines: {node: '>=10'} + decamelize@6.0.0: + resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -878,13 +1287,35 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deepmerge-ts@5.1.0: + resolution: {integrity: sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==} + engines: {node: '>=16.0.0'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + devtools-protocol@0.0.1232444: + resolution: {integrity: sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==} + + devtools-protocol@0.0.1340018: + resolution: {integrity: sha512-yiZNvYDYW8P93XT1q3QxB/y5n/4D7hGfKN6+342Bp1UJpkqzIlt9QLfZuSG8njk6lt6u2s2ZYyLXW3uABtKmtg==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -897,9 +1328,26 @@ packages: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + edge-paths@3.0.5: + resolution: {integrity: sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==} + engines: {node: '>=14.0.0'} + + edgedriver@5.6.1: + resolution: {integrity: sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==} + hasBin: true + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -939,9 +1387,35 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -954,6 +1428,28 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -969,9 +1465,25 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -979,6 +1491,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -989,6 +1506,11 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + geckodriver@4.4.3: + resolution: {integrity: sha512-79rvaq8pvKVUtuM9XBjQApb04kOVkl3TFRX+zTt1wlmL+wqpt85ocWCdqiENU/3zIzg2Me21eClUcnE7F1kL2w==} + engines: {node: ^16.13 || >=18 || >=20} + hasBin: true + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -996,6 +1518,18 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1003,6 +1537,10 @@ packages: get-tsconfig@4.7.6: resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1010,6 +1548,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -1019,9 +1561,20 @@ packages: engines: {node: '>=12'} deprecated: Glob versions prior to v9 are no longer supported + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1037,6 +1590,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -1044,10 +1600,22 @@ packages: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -1062,6 +1630,12 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1106,6 +1680,9 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1114,6 +1691,14 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1122,9 +1707,19 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1148,9 +1743,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} - js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1158,28 +1750,78 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + ky@0.33.3: + resolution: {integrity: sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==} + engines: {node: '>=14.16'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + locate-app@2.4.32: + resolution: {integrity: sha512-2Dqy98qk0Rm1h9mSExxT/4NBSgVOLw0C6Nr41Go2+UWDkZ6MmjHPa6+5MCE7EJIAQV0JVRNA0iu5K9eJSlozlA==} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.zip@4.2.0: + resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loglevel-plugin-prefix@0.8.4: + resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} + + loglevel@1.9.1: + resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} + engines: {node: '>= 0.6.0'} loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} @@ -1205,6 +1847,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1212,6 +1858,10 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1243,10 +1893,17 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -1255,20 +1912,35 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.7.1: - resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} - mocha@10.7.3: resolution: {integrity: sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==} engines: {node: '>= 14.0.0'} hasBin: true + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.3.5: + resolution: {integrity: sha512-+GUI4gX5YC5Bv33epBrD+BGdmDvBg2XGruiWnI3GbIbRmMMBeZ5gs3mJ51OWSGHgJKztZ8AtZeYMMNMVrje2/Q==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1281,6 +1953,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + node-abi@3.65.0: resolution: {integrity: sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==} engines: {node: '>=10'} @@ -1288,6 +1964,23 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp@8.4.1: resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} engines: {node: '>= 10.12.0'} @@ -1302,6 +1995,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-url@8.0.1: + resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} + engines: {node: '>=14.16'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1318,14 +2015,17 @@ packages: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1334,6 +2034,20 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + pac-proxy-agent@7.0.2: + resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1350,16 +2064,23 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -1367,8 +2088,15 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - pkg-types@1.1.3: - resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==} + playwright-core@1.46.1: + resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.46.1: + resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} + engines: {node: '>=18'} + hasBin: true postcss@8.4.41: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} @@ -1387,10 +2115,25 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -1403,9 +2146,40 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + proxy-agent@6.3.1: + resolution: {integrity: sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + puppeteer-core@21.11.0: + resolution: {integrity: sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==} + engines: {node: '>=16.13.2'} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -1413,28 +2187,60 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + resq@1.11.0: + resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + rgb2hex@0.2.5: + resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -1445,6 +2251,12 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safaridriver@0.1.2: + resolution: {integrity: sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -1456,12 +2268,19 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-error@11.0.3: + resolution: {integrity: sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==} + engines: {node: '>=14.16'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1486,6 +2305,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1498,6 +2321,10 @@ packages: resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} engines: {node: '>= 10'} + socks-proxy-agent@8.0.4: + resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} + engines: {node: '>= 14'} + socks@2.8.3: resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} @@ -1506,6 +2333,17 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spacetrim@0.11.39: + resolution: {integrity: sha512-S/baW29azJ7py5ausQRE2S6uEDQnlxgMHOEEq4V770ooBDD1/9kZnxRcco/tjZYuDuqYXblCk/r3N13ZmvHZ2g==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -1526,13 +2364,30 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamx@2.19.0: + resolution: {integrity: sha512-5z6CNR4gtkPbwlxyEqoDGDmWIzoNJqCBt4Eac1ICP9YaIT08ct712cFj0u1rx4F8luAuL+3Qc+RFIdI4OX00kg==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -1540,6 +2395,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -1552,8 +2411,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -1570,21 +2429,32 @@ packages: tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + + tar-fs@3.0.6: + resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + text-decoder@1.1.1: + resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - tinypool@1.0.0: resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1593,10 +2463,6 @@ packages: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - tinyspy@3.0.0: resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} @@ -1605,6 +2471,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -1619,6 +2496,9 @@ packages: '@swc/wasm': optional: true + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tsx@4.17.0: resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} engines: {node: '>=18.0.0'} @@ -1627,17 +2507,29 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@2.13.0: + resolution: {integrity: sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==} + engines: {node: '>=12.20'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.25.0: + resolution: {integrity: sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==} + engines: {node: '>=16'} typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -1651,22 +2543,49 @@ packages: unique-slug@2.0.2: resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + + userhome@1.0.0: + resolution: {integrity: sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==} + engines: {node: '>= 0.8.0'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-node@2.0.5: resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-top-level-await@1.4.4: + resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==} + peerDependencies: + vite: '>=2.8' + + vite-plugin-wasm@3.3.0: + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + vite@5.4.1: resolution: {integrity: sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1698,15 +2617,15 @@ packages: terser: optional: true - vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + vitest@2.0.5: + resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 + '@vitest/browser': 2.0.5 + '@vitest/ui': 2.0.5 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1723,36 +2642,44 @@ packages: jsdom: optional: true - vitest@2.0.5: - resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} - engines: {node: ^18.0.0 || >=20.0.0} + wait-port@1.1.0: + resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} + engines: {node: '>=10'} hasBin: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + webdriver@8.40.3: + resolution: {integrity: sha512-mc/pxLpgAQphnIaWvix/QXzp9CJpEvIA3YeF9t5plPaTbvbEaCAYYWkTP6e3vYPYWvx57krjGaYkNUnDCBNolA==} + engines: {node: ^16.13 || >=18} + + webdriverio@8.40.3: + resolution: {integrity: sha512-2UQ/Vg2X7tTHmfWmB6QaXuUheodRRNwzT8VK6cHM2JrDHxDZzUawqSt4L7H7ba6/ctuRt5/pgbmKFtU/moLfhA==} + engines: {node: ^16.13 || >=18} peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.0.5 - '@vitest/ui': 2.0.5 - happy-dom: '*' - jsdom: '*' + devtools: ^8.14.0 peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: + devtools: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1764,13 +2691,45 @@ packages: workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1782,6 +2741,10 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs-unparser@2.0.0: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} engines: {node: '>=10'} @@ -1790,6 +2753,13 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -1798,9 +2768,13 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.1.1: - resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} - engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} snapshots: @@ -1823,6 +2797,23 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.1 + '@babel/runtime@7.25.4': + dependencies: + regenerator-runtime: 0.14.1 + + '@bundled-es-modules/cookie@2.0.0': + dependencies: + cookie: 0.5.0 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -1971,6 +2962,42 @@ snapshots: '@gar/promisify@1.1.3': optional: true + '@inquirer/confirm@3.1.22': + dependencies: + '@inquirer/core': 9.0.10 + '@inquirer/type': 1.5.2 + + '@inquirer/core@9.0.10': + dependencies: + '@inquirer/figures': 1.0.5 + '@inquirer/type': 1.5.2 + '@types/mute-stream': 0.0.4 + '@types/node': 22.3.0 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + + '@inquirer/figures@1.0.5': {} + + '@inquirer/type@1.5.2': + dependencies: + mute-stream: 1.0.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jest/expect-utils@29.7.0': dependencies: jest-get-type: 29.6.3 @@ -1988,6 +3015,8 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@journeyapps/wa-sqlite@0.3.0': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -2010,6 +3039,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@mswjs/interceptors@0.29.1': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 @@ -2022,6 +3060,40 @@ snapshots: rimraf: 3.0.2 optional: true + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.25': {} + + '@promptbook/utils@0.67.5': + dependencies: + spacetrim: 0.11.39 + + '@puppeteer/browsers@1.9.1': + dependencies: + debug: 4.3.4 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.1 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-virtual@3.0.2(rollup@4.20.0)': + optionalDependencies: + rollup: 4.20.0 + '@rollup/rollup-android-arm-eabi@4.20.0': optional: true @@ -2072,23 +3144,104 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@tootallnate/once@1.1.2': + '@sindresorhus/is@5.6.0': {} + + '@swc/core-darwin-arm64@1.7.18': optional: true - '@tsconfig/node10@1.0.11': {} + '@swc/core-darwin-x64@1.7.18': + optional: true - '@tsconfig/node12@1.0.11': {} + '@swc/core-linux-arm-gnueabihf@1.7.18': + optional: true + + '@swc/core-linux-arm64-gnu@1.7.18': + optional: true + + '@swc/core-linux-arm64-musl@1.7.18': + optional: true + + '@swc/core-linux-x64-gnu@1.7.18': + optional: true + + '@swc/core-linux-x64-musl@1.7.18': + optional: true + + '@swc/core-win32-arm64-msvc@1.7.18': + optional: true + + '@swc/core-win32-ia32-msvc@1.7.18': + optional: true + + '@swc/core-win32-x64-msvc@1.7.18': + optional: true + + '@swc/core@1.7.18': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.12 + optionalDependencies: + '@swc/core-darwin-arm64': 1.7.18 + '@swc/core-darwin-x64': 1.7.18 + '@swc/core-linux-arm-gnueabihf': 1.7.18 + '@swc/core-linux-arm64-gnu': 1.7.18 + '@swc/core-linux-arm64-musl': 1.7.18 + '@swc/core-linux-x64-gnu': 1.7.18 + '@swc/core-linux-x64-musl': 1.7.18 + '@swc/core-win32-arm64-msvc': 1.7.18 + '@swc/core-win32-ia32-msvc': 1.7.18 + '@swc/core-win32-x64-msvc': 1.7.18 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.12': + dependencies: + '@swc/counter': 0.1.3 + + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/runtime': 7.25.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@tootallnate/once@1.1.2': + optional: true + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} '@tsconfig/node14@1.0.3': {} '@tsconfig/node16@1.0.4': {} + '@types/aria-query@5.0.4': {} + '@types/better-sqlite3@7.6.11': dependencies: '@types/node': 20.14.15 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.5': {} + '@types/http-cache-semantics@4.0.4': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -2101,6 +3254,10 @@ snapshots: '@types/mocha@10.0.7': {} + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 22.3.0 + '@types/node@20.14.15': dependencies: undici-types: 5.26.5 @@ -2111,17 +3268,62 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/statuses@2.0.5': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/which@2.0.2': {} + + '@types/wrap-ansi@3.0.0': {} + + '@types/ws@8.5.12': + dependencies: + '@types/node': 22.3.0 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': dependencies: '@types/yargs-parser': 21.0.3 - '@vitest/expect@1.6.0': + '@types/yauzl@2.10.3': dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.5.0 + '@types/node': 22.3.0 + optional: true + + '@vitest/browser@2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13))': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/utils': 2.0.5 + magic-string: 0.30.11 + msw: 2.3.5(typescript@5.5.4) + sirv: 2.0.4 + vitest: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + ws: 8.18.0 + optionalDependencies: + playwright: 1.46.1 + webdriverio: 8.40.3(encoding@0.1.13) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + + '@vitest/browser@2.0.5(typescript@5.5.4)(vitest@2.0.5)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/utils': 2.0.5 + magic-string: 0.30.11 + msw: 2.3.5(typescript@5.5.4) + sirv: 2.0.4 + vitest: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + optional: true '@vitest/expect@2.0.5': dependencies: @@ -2134,44 +3336,21 @@ snapshots: dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@1.6.0': - dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.2 - '@vitest/runner@2.0.5': dependencies: '@vitest/utils': 2.0.5 pathe: 1.1.2 - '@vitest/snapshot@1.6.0': - dependencies: - magic-string: 0.30.11 - pathe: 1.1.2 - pretty-format: 29.7.0 - '@vitest/snapshot@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 magic-string: 0.30.11 pathe: 1.1.2 - '@vitest/spy@1.6.0': - dependencies: - tinyspy: 2.2.1 - '@vitest/spy@2.0.5': dependencies: tinyspy: 3.0.0 - '@vitest/utils@1.6.0': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - '@vitest/utils@2.0.5': dependencies: '@vitest/pretty-format': 2.0.5 @@ -2179,9 +3358,69 @@ snapshots: loupe: 3.1.1 tinyrainbow: 1.2.0 + '@wdio/config@8.40.3': + dependencies: + '@wdio/logger': 8.38.0 + '@wdio/types': 8.40.3 + '@wdio/utils': 8.40.3 + decamelize: 6.0.0 + deepmerge-ts: 5.1.0 + glob: 10.4.5 + import-meta-resolve: 4.1.0 + transitivePeerDependencies: + - supports-color + + '@wdio/logger@8.38.0': + dependencies: + chalk: 5.3.0 + loglevel: 1.9.1 + loglevel-plugin-prefix: 0.8.4 + strip-ansi: 7.1.0 + + '@wdio/logger@9.0.4': + dependencies: + chalk: 5.3.0 + loglevel: 1.9.1 + loglevel-plugin-prefix: 0.8.4 + strip-ansi: 7.1.0 + + '@wdio/protocols@8.40.3': {} + + '@wdio/repl@8.40.3': + dependencies: + '@types/node': 22.3.0 + + '@wdio/types@8.40.3': + dependencies: + '@types/node': 22.3.0 + + '@wdio/utils@8.40.3': + dependencies: + '@puppeteer/browsers': 1.9.1 + '@wdio/logger': 8.38.0 + '@wdio/types': 8.40.3 + decamelize: 6.0.0 + deepmerge-ts: 5.1.0 + edgedriver: 5.6.1 + geckodriver: 4.4.3 + get-port: 7.1.0 + import-meta-resolve: 4.1.0 + locate-app: 2.4.32 + safaridriver: 0.1.2 + split2: 4.2.0 + wait-port: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@zip.js/zip.js@2.7.51': {} + abbrev@1.1.1: optional: true + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-walk@8.3.3: dependencies: acorn: 8.12.1 @@ -2195,6 +3434,12 @@ snapshots: - supports-color optional: true + agent-base@7.1.1: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + agentkeepalive@4.5.0: dependencies: humanize-ms: 1.2.1 @@ -2208,8 +3453,14 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} + ansi-regex@6.0.1: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -2220,6 +3471,8 @@ snapshots: ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2228,6 +3481,26 @@ snapshots: aproba@2.0.0: optional: true + archiver-utils@5.0.2: + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.5.2 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + are-we-there-yet@3.0.1: dependencies: delegates: 1.0.0 @@ -2238,14 +3511,53 @@ snapshots: argparse@2.0.1: {} - assertion-error@1.1.0: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 assertion-error@2.0.1: {} + ast-types@0.13.4: + dependencies: + tslib: 2.7.0 + + async-mutex@0.5.0: + dependencies: + tslib: 2.7.0 + + async@3.2.6: {} + + b4a@1.6.6: {} + balanced-match@1.0.2: {} + bare-events@2.4.2: + optional: true + + bare-fs@2.3.1: + dependencies: + bare-events: 2.4.2 + bare-path: 2.1.3 + bare-stream: 2.2.0 + optional: true + + bare-os@2.4.0: + optional: true + + bare-path@2.1.3: + dependencies: + bare-os: 2.4.0 + optional: true + + bare-stream@2.2.0: + dependencies: + streamx: 2.19.0 + optional: true + base64-js@1.5.1: {} + basic-ftp@5.0.5: {} + better-sqlite3@11.1.2: dependencies: bindings: 1.5.0 @@ -2279,11 +3591,20 @@ snapshots: browser-stdout@1.3.1: {} + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cac@6.7.14: {} cacache@15.3.0: @@ -2310,17 +3631,19 @@ snapshots: - bluebird optional: true - camelcase@6.3.0: {} + cacheable-lookup@7.0.0: {} - chai@4.5.0: + cacheable-request@10.2.14: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 + + camelcase@6.3.0: {} chai@5.1.1: dependencies: @@ -2341,9 +3664,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + chalk@5.3.0: {} check-error@2.1.1: {} @@ -2363,17 +3684,33 @@ snapshots: chownr@2.0.0: {} + chromium-bidi@0.5.8(devtools-protocol@0.0.1232444): + dependencies: + devtools-protocol: 0.0.1232444 + mitt: 3.0.1 + urlpattern-polyfill: 10.0.0 + ci-info@3.9.0: {} clean-stack@2.2.0: optional: true + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -2389,22 +3726,59 @@ snapshots: color-support@1.1.3: optional: true + commander@9.5.0: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + concat-map@0.0.1: optional: true - confbox@0.1.7: {} - console-control-strings@1.1.0: optional: true + cookie@0.5.0: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.5.2 + create-require@1.1.1: {} + cross-fetch@4.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + css-shorthand-properties@1.1.1: {} + + css-value@0.0.1: {} + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.3.6(supports-color@8.1.1): dependencies: ms: 2.1.2 @@ -2413,31 +3787,66 @@ snapshots: decamelize@4.0.0: {} + decamelize@6.0.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 - deep-eql@5.0.2: {} deep-extend@0.6.0: {} + deepmerge-ts@5.1.0: {} + + defer-to-connect@2.0.1: {} + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + delegates@1.0.0: optional: true + dequal@2.0.3: {} + detect-libc@2.0.3: {} + devtools-protocol@0.0.1232444: {} + + devtools-protocol@0.0.1340018: {} + diff-sequences@29.6.3: {} diff@4.0.2: {} diff@5.2.0: {} + dom-accessibility-api@0.5.16: {} + + eastasianwidth@0.2.0: {} + + edge-paths@3.0.5: + dependencies: + '@types/which': 2.0.2 + which: 2.0.2 + + edgedriver@5.6.1: + dependencies: + '@wdio/logger': 8.38.0 + '@zip.js/zip.js': 2.7.51 + decamelize: 6.0.0 + edge-paths: 3.0.5 + fast-xml-parser: 4.4.1 + node-fetch: 3.3.2 + which: 4.0.0 + emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -2514,10 +3923,28 @@ snapshots: escape-string-regexp@4.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.5 + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + execa@8.0.1: dependencies: cross-spawn: 7.0.3 @@ -2540,6 +3967,33 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + extract-zip@2.0.1: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@2.0.1: {} + + fast-fifo@1.3.2: {} + + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.0.5 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -2553,14 +4007,34 @@ snapshots: flat@5.0.2: {} + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data-encoder@2.1.4: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs-constants@1.0.0: {} + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -2576,22 +4050,61 @@ snapshots: wide-align: 1.1.5 optional: true + geckodriver@4.4.3: + dependencies: + '@wdio/logger': 9.0.4 + '@zip.js/zip.js': 2.7.51 + decamelize: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + node-fetch: 3.3.2 + tar-fs: 3.0.6 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + get-caller-file@2.0.5: {} get-func-name@2.0.2: {} + get-port@7.1.0: {} + + get-stream@5.2.0: + dependencies: + pump: 3.0.0 + + get-stream@6.0.1: {} + get-stream@8.0.1: {} get-tsconfig@4.7.6: dependencies: resolve-pkg-maps: 1.0.0 + get-uri@6.0.3: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + github-from-package@0.0.0: {} glob-parent@5.1.2: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -2610,8 +4123,26 @@ snapshots: minimatch: 5.1.6 once: 1.4.0 + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + graceful-fs@4.2.11: {} + grapheme-splitter@1.0.4: {} + + graphql@16.9.0: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -2621,8 +4152,9 @@ snapshots: he@1.2.0: {} - http-cache-semantics@4.1.1: - optional: true + headers-polyfill@4.0.3: {} + + http-cache-semantics@4.1.1: {} http-proxy-agent@4.0.1: dependencies: @@ -2633,6 +4165,18 @@ snapshots: - supports-color optional: true + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -2641,6 +4185,13 @@ snapshots: - supports-color optional: true + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + human-signals@5.0.0: {} humanize-ms@1.2.1: @@ -2655,6 +4206,10 @@ snapshots: ieee754@1.2.1: {} + immediate@3.0.6: {} + + import-meta-resolve@4.1.0: {} + imurmurhash@0.1.4: optional: true @@ -2677,7 +4232,6 @@ snapshots: dependencies: jsbn: 1.1.0 sprintf-js: 1.1.3 - optional: true is-binary-path@2.1.0: dependencies: @@ -2694,16 +4248,32 @@ snapshots: is-lambda@1.0.1: optional: true + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} + + is-stream@2.0.1: {} + is-stream@3.0.0: {} is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} + isexe@3.1.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -2743,42 +4313,83 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@9.0.0: {} - js-yaml@4.1.0: dependencies: argparse: 2.0.1 - jsbn@1.1.0: - optional: true + jsbn@1.1.0: {} + + json-buffer@3.0.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + ky@0.33.3: {} - local-pkg@0.5.0: + lazystream@1.0.1: dependencies: - mlly: 1.7.1 - pkg-types: 1.1.3 + readable-stream: 2.3.8 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + locate-app@2.4.32: + dependencies: + '@promptbook/utils': 0.67.5 + type-fest: 2.13.0 + userhome: 1.0.0 locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash.clonedeep@4.5.0: {} + + lodash.zip@4.2.0: {} + + lodash@4.17.21: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 + loglevel-plugin-prefix@0.8.4: {} + + loglevel@1.9.1: {} loupe@3.1.1: dependencies: get-func-name: 2.0.2 + lowercase-keys@3.0.0: {} + + lru-cache@10.4.3: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 optional: true + lru-cache@7.18.3: {} + + lz-string@1.5.0: {} + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -2819,6 +4430,8 @@ snapshots: mimic-response@3.1.0: {} + mimic-response@4.0.0: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -2828,6 +4441,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + minimist@1.2.8: {} minipass-collect@1.0.2: @@ -2865,22 +4482,19 @@ snapshots: minipass@5.0.0: {} + minipass@7.1.2: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 + mitt@3.0.1: {} + mkdirp-classic@0.5.3: {} mkdirp@1.0.4: {} - mlly@1.7.1: - dependencies: - acorn: 8.12.1 - pathe: 1.1.2 - pkg-types: 1.1.3 - ufo: 1.5.4 - mocha@10.7.3: dependencies: ansi-colors: 4.1.3 @@ -2904,10 +4518,36 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mrmime@2.0.0: {} + ms@2.1.2: {} ms@2.1.3: {} + msw@2.3.5(typescript@5.5.4): + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 3.1.22 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.25.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.5.4 + + mute-stream@1.0.0: {} + nanoid@3.3.7: {} napi-build-utils@1.0.2: {} @@ -2915,12 +4555,28 @@ snapshots: negotiator@0.6.3: optional: true + netmask@2.0.2: {} + node-abi@3.65.0: dependencies: semver: 7.6.3 node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp@8.4.1: dependencies: env-paths: 2.2.1 @@ -2945,6 +4601,8 @@ snapshots: normalize-path@3.0.0: {} + normalize-url@8.0.1: {} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -2965,14 +4623,14 @@ snapshots: dependencies: mimic-fn: 4.0.0 + outvariant@1.4.3: {} + + p-cancelable@3.0.0: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.1.1 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -2982,6 +4640,28 @@ snapshots: aggregate-error: 3.1.0 optional: true + pac-proxy-agent@7.0.2: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.4 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-json-from-dist@1.0.0: {} + + pako@1.0.11: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: @@ -2991,21 +4671,30 @@ snapshots: path-key@4.0.0: {} - pathe@1.1.2: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@6.2.2: {} - pathval@1.1.1: {} + pathe@1.1.2: {} pathval@2.0.0: {} + pend@1.2.0: {} + picocolors@1.0.1: {} picomatch@2.3.1: {} - pkg-types@1.1.3: + playwright-core@1.46.1: {} + + playwright@1.46.1: dependencies: - confbox: 0.1.7 - mlly: 1.7.1 - pathe: 1.1.2 + playwright-core: 1.46.1 + optionalDependencies: + fsevents: 2.3.2 postcss@8.4.41: dependencies: @@ -3032,12 +4721,24 @@ snapshots: prettier@3.3.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + progress@2.0.3: {} + promise-inflight@1.0.1: optional: true @@ -3047,11 +4748,52 @@ snapshots: retry: 0.12.0 optional: true + proxy-agent@6.3.1: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.2 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.4 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + psl@1.9.0: {} + pump@3.0.0: dependencies: end-of-stream: 1.4.4 once: 1.4.0 + punycode@2.3.1: {} + + puppeteer-core@21.11.0(encoding@0.1.13): + dependencies: + '@puppeteer/browsers': 1.9.1 + chromium-bidi: 0.5.8(devtools-protocol@0.0.1232444) + cross-fetch: 4.0.0(encoding@0.1.13) + debug: 4.3.4 + devtools-protocol: 0.0.1232444 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + query-selector-shadow-dom@1.0.1: {} + + querystringify@2.2.0: {} + + queue-tick@1.0.1: {} + + quick-lru@5.1.1: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -3063,25 +4805,65 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-is@17.0.2: {} + react-is@18.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 + regenerator-runtime@0.14.1: {} + require-directory@2.1.1: {} + requires-port@1.0.0: {} + + resolve-alpn@1.2.1: {} + resolve-pkg-maps@1.0.0: {} + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + resq@1.11.0: + dependencies: + fast-deep-equal: 2.0.1 + retry@0.12.0: optional: true + rgb2hex@0.2.5: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -3109,6 +4891,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.20.0 fsevents: 2.3.3 + safaridriver@0.1.2: {} + + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: @@ -3116,6 +4902,10 @@ snapshots: semver@7.6.3: {} + serialize-error@11.0.3: + dependencies: + type-fest: 2.19.0 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -3123,6 +4913,8 @@ snapshots: set-blocking@2.0.0: optional: true + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3144,10 +4936,15 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + slash@3.0.0: {} - smart-buffer@4.2.0: - optional: true + smart-buffer@4.2.0: {} socks-proxy-agent@6.2.1: dependencies: @@ -3158,17 +4955,30 @@ snapshots: - supports-color optional: true + socks-proxy-agent@8.0.4: + dependencies: + agent-base: 7.1.1 + debug: 4.3.6(supports-color@8.1.1) + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + socks@2.8.3: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 - optional: true source-map-js@1.2.0: {} - sprintf-js@1.1.3: + source-map@0.6.1: optional: true + spacetrim@0.11.39: {} + + split2@4.2.0: {} + + sprintf-js@1.1.3: {} + sqlite3@5.1.7: dependencies: bindings: 1.5.0 @@ -3194,14 +5004,36 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.7.0: {} + streamx@2.19.0: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.1.1 + optionalDependencies: + bare-events: 2.4.2 + + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -3210,15 +5042,17 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + strip-final-newline@3.0.0: {} strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} - strip-literal@2.1.0: - dependencies: - js-tokens: 9.0.0 + strnum@1.0.5: {} supports-color@5.5.0: dependencies: @@ -3239,6 +5073,20 @@ snapshots: pump: 3.0.0 tar-stream: 2.2.0 + tar-fs@3.0.4: + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.7 + + tar-fs@3.0.6: + dependencies: + pump: 3.0.0 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 2.3.1 + bare-path: 2.1.3 + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -3247,6 +5095,12 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.7: + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.19.0 + tar@6.2.1: dependencies: chownr: 2.0.0 @@ -3256,23 +5110,36 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - tinybench@2.9.0: {} + text-decoder@1.1.1: + dependencies: + b4a: 1.6.6 + + through@2.3.8: {} - tinypool@0.8.4: {} + tinybench@2.9.0: {} tinypool@1.0.0: {} tinyrainbow@1.2.0: {} - tinyspy@2.2.1: {} - tinyspy@3.0.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4): + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + ts-node@10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -3289,6 +5156,10 @@ snapshots: typescript: 5.5.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.7.18 + + tslib@2.7.0: {} tsx@4.17.0: dependencies: @@ -3301,11 +5172,20 @@ snapshots: dependencies: safe-buffer: 5.2.1 - type-detect@4.1.0: {} + type-fest@0.21.3: {} + + type-fest@2.13.0: {} + + type-fest@2.19.0: {} + + type-fest@4.25.0: {} typescript@5.5.4: {} - ufo@1.5.4: {} + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 undici-types@5.26.5: {} @@ -3321,16 +5201,31 @@ snapshots: imurmurhash: 0.1.4 optional: true + universalify@0.2.0: {} + + universalify@2.0.1: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + urlpattern-polyfill@10.0.0: {} + + userhome@1.0.0: {} + util-deprecate@1.0.2: {} + uuid@10.0.0: {} + v8-compile-cache-lib@3.0.1: {} - vite-node@1.6.0(@types/node@20.14.15): + vite-node@2.0.5(@types/node@20.14.15): dependencies: cac: 6.7.14 debug: 4.3.6(supports-color@8.1.1) pathe: 1.1.2 - picocolors: 1.0.1 + tinyrainbow: 1.2.0 vite: 5.4.1(@types/node@20.14.15) transitivePeerDependencies: - '@types/node' @@ -3361,6 +5256,20 @@ snapshots: - supports-color - terser + vite-plugin-top-level-await@1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@22.3.0)): + dependencies: + '@rollup/plugin-virtual': 3.0.2(rollup@4.20.0) + '@swc/core': 1.7.18 + uuid: 10.0.0 + vite: 5.4.1(@types/node@22.3.0) + transitivePeerDependencies: + - '@swc/helpers' + - rollup + + vite-plugin-wasm@3.3.0(vite@5.4.1(@types/node@22.3.0)): + dependencies: + vite: 5.4.1(@types/node@22.3.0) + vite@5.4.1(@types/node@20.14.15): dependencies: esbuild: 0.21.5 @@ -3379,30 +5288,30 @@ snapshots: '@types/node': 22.3.0 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.15): + vitest@2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5): dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.3 - chai: 4.5.0 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 debug: 4.3.6(supports-color@8.1.1) execa: 8.0.1 - local-pkg: 0.5.0 magic-string: 0.30.11 pathe: 1.1.2 - picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.1.0 tinybench: 2.9.0 - tinypool: 0.8.4 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 vite: 5.4.1(@types/node@20.14.15) - vite-node: 1.6.0(@types/node@20.14.15) + vite-node: 2.0.5(@types/node@20.14.15) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.14.15 + '@vitest/browser': 2.0.5(typescript@5.5.4)(vitest@2.0.5) transitivePeerDependencies: - less - lightningcss @@ -3413,7 +5322,7 @@ snapshots: - supports-color - terser - vitest@2.0.5(@types/node@22.3.0): + vitest@2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5): dependencies: '@ampproject/remapping': 2.3.0 '@vitest/expect': 2.0.5 @@ -3436,6 +5345,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.3.0 + '@vitest/browser': 2.0.5(typescript@5.5.4)(vitest@2.0.5) transitivePeerDependencies: - less - lightningcss @@ -3446,10 +5356,82 @@ snapshots: - supports-color - terser + wait-port@1.1.0: + dependencies: + chalk: 4.1.2 + commander: 9.5.0 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + web-streams-polyfill@3.3.3: {} + + webdriver@8.40.3: + dependencies: + '@types/node': 22.3.0 + '@types/ws': 8.5.12 + '@wdio/config': 8.40.3 + '@wdio/logger': 8.38.0 + '@wdio/protocols': 8.40.3 + '@wdio/types': 8.40.3 + '@wdio/utils': 8.40.3 + deepmerge-ts: 5.1.0 + got: 12.6.1 + ky: 0.33.3 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + webdriverio@8.40.3(encoding@0.1.13): + dependencies: + '@types/node': 22.3.0 + '@wdio/config': 8.40.3 + '@wdio/logger': 8.38.0 + '@wdio/protocols': 8.40.3 + '@wdio/repl': 8.40.3 + '@wdio/types': 8.40.3 + '@wdio/utils': 8.40.3 + archiver: 7.0.1 + aria-query: 5.3.0 + css-shorthand-properties: 1.1.1 + css-value: 0.0.1 + devtools-protocol: 0.0.1340018 + grapheme-splitter: 1.0.4 + import-meta-resolve: 4.1.0 + is-plain-obj: 4.1.0 + jszip: 3.10.1 + lodash.clonedeep: 4.5.0 + lodash.zip: 4.2.0 + minimatch: 9.0.5 + puppeteer-core: 21.11.0(encoding@0.1.13) + query-selector-shadow-dom: 1.0.1 + resq: 1.11.0 + rgb2hex: 0.2.5 + serialize-error: 11.0.3 + webdriver: 8.40.3 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 + which@4.0.0: + dependencies: + isexe: 3.1.1 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -3462,20 +5444,38 @@ snapshots: workerpool@6.5.1: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} + ws@8.16.0: {} + + ws@8.18.0: {} + y18n@5.0.8: {} yallist@4.0.0: {} yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs-unparser@2.0.0: dependencies: camelcase: 6.3.0 @@ -3493,8 +5493,29 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yn@3.1.1: {} yocto-queue@0.1.0: {} - yocto-queue@1.1.1: {} + yoctocolors-cjs@2.1.2: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.5.2 diff --git a/test/src/wa-sqlite.test.ts b/test/src/wa-sqlite.test.ts new file mode 100644 index 0000000..5c16bf0 --- /dev/null +++ b/test/src/wa-sqlite.test.ts @@ -0,0 +1,14 @@ +import { ConnectionPoolImpl, waSqlitePool } from '../../lib/index.js'; +import { describeDriverTests } from './tests/driver-tests.js'; +import { describeImplTests } from './tests/impl-tests.js'; + +describeDriverTests( + 'wa-sqlite', + { getColumns: true, rawResults: false, allowsMissingParameters: true }, + (path: string) => waSqlitePool(':memory:') +); + +describeImplTests( + 'wa-sqlite', + (path) => new ConnectionPoolImpl(waSqlitePool(':memory:')) +); From 7b1076a594acbac55546c15663922c5890f9cd67 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 27 Aug 2024 16:32:23 +0200 Subject: [PATCH 02/25] Running tests for wa-sqlite. --- packages/driver-tests/src/driver-tests.ts | 3 +-- packages/driver-tests/src/test.ts | 13 +++++++++++-- packages/wa-sqlite-driver/test/src/driver.test.ts | 8 ++++++++ packages/wa-sqlite-driver/test/tsconfig.json | 9 +++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/wa-sqlite-driver/test/src/driver.test.ts create mode 100644 packages/wa-sqlite-driver/test/tsconfig.json diff --git a/packages/driver-tests/src/driver-tests.ts b/packages/driver-tests/src/driver-tests.ts index 6c8452f..a217b06 100644 --- a/packages/driver-tests/src/driver-tests.ts +++ b/packages/driver-tests/src/driver-tests.ts @@ -1,7 +1,6 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; -import { beforeEach, describe, test } from './test.js'; -import { expect } from 'expect'; +import { beforeEach, describe, test, expect } from './test.js'; import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; export interface DriverFeatures { diff --git a/packages/driver-tests/src/test.ts b/packages/driver-tests/src/test.ts index 9845255..5bef452 100644 --- a/packages/driver-tests/src/test.ts +++ b/packages/driver-tests/src/test.ts @@ -1,5 +1,9 @@ // A lib to allow testing with vitest or mocha -import type { test as testType, describe as describeType } from 'vitest'; +import type { + test as testType, + describe as describeType, + expect as expectType +} from 'vitest'; export interface TestContext { fullName: string; @@ -8,15 +12,19 @@ export interface TestContext { export const isVitest = true || process.env.VITEST == 'true'; export const isMocha = !isVitest; -let testImpl, describeImpl, beforeEachImpl; +let testImpl, describeImpl, beforeEachImpl, expectImpl; if (isMocha) { const { test, describe, beforeEach } = await import('./setup-mocha.js'); + const { expect } = await import('expect'); + expectImpl = expect; testImpl = test; describeImpl = describe; beforeEachImpl = beforeEach; } else { const { test, describe, beforeEach } = await import('./setup-vitest.js'); + const { expect } = await import('vitest'); + expectImpl = expect; testImpl = test; describeImpl = describe; beforeEachImpl = beforeEach; @@ -28,3 +36,4 @@ export function beforeEach(callback: (context: TestContext) => any) { export const test = testImpl as typeof testType; export const describe = describeImpl as typeof describeType; +export const expect = expectImpl as typeof expectType; diff --git a/packages/wa-sqlite-driver/test/src/driver.test.ts b/packages/wa-sqlite-driver/test/src/driver.test.ts new file mode 100644 index 0000000..313ead9 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/driver.test.ts @@ -0,0 +1,8 @@ +import { describeDriverTests } from '@sqlite-js/driver-tests'; +import { waSqlitePool } from '../../lib/index.js'; + +describeDriverTests( + 'wa-sqlite', + { getColumns: true, rawResults: true, allowsMissingParameters: false }, + (path) => waSqlitePool(':memory:') +); diff --git a/packages/wa-sqlite-driver/test/tsconfig.json b/packages/wa-sqlite-driver/test/tsconfig.json new file mode 100644 index 0000000..42305e6 --- /dev/null +++ b/packages/wa-sqlite-driver/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "noEmit": true + }, + "include": ["src"], + "references": [{ "path": "../" }] +} From 98b78fb0f93a5227b6625008a65e39c0aa21697c Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 27 Aug 2024 16:56:47 +0200 Subject: [PATCH 03/25] Fix tests to work in both NodeJS and browser. --- packages/api/test/src/impl-tests.ts | 14 +++++++------- .../test/src/better-sqlite3-async.test.ts | 6 +++++- .../test/src/better-sqlite3.test.ts | 6 +++++- packages/better-sqlite3-driver/test/src/util.ts | 12 ++++++++++++ packages/driver-tests/src/driver-tests.ts | 13 ++----------- packages/driver-tests/src/test.ts | 2 +- .../test/src/node-sqlite-async.test.ts | 6 +++++- .../driver-tests/test/src/node-sqlite-sync.test.ts | 6 +++++- packages/driver-tests/test/src/util.ts | 12 ++++++++++++ packages/wa-sqlite-driver/test/src/driver.test.ts | 4 +++- packages/wa-sqlite-driver/vitest.config.ts | 3 +++ 11 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 packages/better-sqlite3-driver/test/src/util.ts create mode 100644 packages/driver-tests/test/src/util.ts diff --git a/packages/api/test/src/impl-tests.ts b/packages/api/test/src/impl-tests.ts index 58f8716..d09c847 100644 --- a/packages/api/test/src/impl-tests.ts +++ b/packages/api/test/src/impl-tests.ts @@ -12,13 +12,13 @@ export function describeImplTests( let dbPath: string; const open = async () => { - // const dir = path.dirname(dbPath); - // try { - // await fs.mkdir(dir); - // } catch (e) {} - // try { - // await fs.rm(dbPath); - // } catch (e) {} + const dir = path.dirname(dbPath); + try { + await fs.mkdir(dir); + } catch (e) {} + try { + await fs.rm(dbPath); + } catch (e) {} return factory(dbPath); }; diff --git a/packages/better-sqlite3-driver/test/src/better-sqlite3-async.test.ts b/packages/better-sqlite3-driver/test/src/better-sqlite3-async.test.ts index d08bab3..9b0bd32 100644 --- a/packages/better-sqlite3-driver/test/src/better-sqlite3-async.test.ts +++ b/packages/better-sqlite3-driver/test/src/better-sqlite3-async.test.ts @@ -1,8 +1,12 @@ import { describeDriverTests } from '@sqlite-js/driver-tests'; import { BetterSqliteDriver } from '../../lib/index.js'; +import { deleteDb } from './util.js'; describeDriverTests( 'better-sqlite3-async-pool', { getColumns: true, rawResults: true, allowsMissingParameters: false }, - (path) => BetterSqliteDriver.open(path) + async (path) => { + await deleteDb(path); + return BetterSqliteDriver.open(path); + } ); diff --git a/packages/better-sqlite3-driver/test/src/better-sqlite3.test.ts b/packages/better-sqlite3-driver/test/src/better-sqlite3.test.ts index d9d5e91..4d430e3 100644 --- a/packages/better-sqlite3-driver/test/src/better-sqlite3.test.ts +++ b/packages/better-sqlite3-driver/test/src/better-sqlite3.test.ts @@ -1,8 +1,12 @@ import { BetterSqliteDriver } from '../../lib/index.js'; import { describeDriverTests } from '@sqlite-js/driver-tests'; +import { deleteDb } from './util.js'; describeDriverTests( 'better-sqlite3', { getColumns: true, rawResults: true, allowsMissingParameters: false }, - (path) => BetterSqliteDriver.openInProcess(path) + async (path) => { + await deleteDb(path); + return BetterSqliteDriver.openInProcess(path); + } ); diff --git a/packages/better-sqlite3-driver/test/src/util.ts b/packages/better-sqlite3-driver/test/src/util.ts new file mode 100644 index 0000000..346ea10 --- /dev/null +++ b/packages/better-sqlite3-driver/test/src/util.ts @@ -0,0 +1,12 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +export async function deleteDb(dbPath: string) { + const dir = path.dirname(dbPath); + try { + await fs.mkdir(dir); + } catch (e) {} + try { + await fs.rm(dbPath); + } catch (e) {} +} diff --git a/packages/driver-tests/src/driver-tests.ts b/packages/driver-tests/src/driver-tests.ts index a217b06..88ef9c0 100644 --- a/packages/driver-tests/src/driver-tests.ts +++ b/packages/driver-tests/src/driver-tests.ts @@ -1,5 +1,3 @@ -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; import { beforeEach, describe, test, expect } from './test.js'; import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; @@ -12,20 +10,13 @@ export interface DriverFeatures { export function describeDriverTests( name: string, features: DriverFeatures, - factory: (path: string) => SqliteDriverConnectionPool + factory: (path: string) => Promise ) { describe(`${name} - driver tests`, () => { let dbPath: string; const open = async () => { - // const dir = path.dirname(dbPath); - // try { - // await fs.mkdir(dir); - // } catch (e) {} - // try { - // await fs.rm(dbPath); - // } catch (e) {} - const db = factory(dbPath); + const db = await factory(dbPath); return db; }; diff --git a/packages/driver-tests/src/test.ts b/packages/driver-tests/src/test.ts index 5bef452..8ccb482 100644 --- a/packages/driver-tests/src/test.ts +++ b/packages/driver-tests/src/test.ts @@ -9,7 +9,7 @@ export interface TestContext { fullName: string; } -export const isVitest = true || process.env.VITEST == 'true'; +export const isVitest = process.env.VITEST == 'true'; export const isMocha = !isVitest; let testImpl, describeImpl, beforeEachImpl, expectImpl; diff --git a/packages/driver-tests/test/src/node-sqlite-async.test.ts b/packages/driver-tests/test/src/node-sqlite-async.test.ts index 448a648..8b5e22d 100644 --- a/packages/driver-tests/test/src/node-sqlite-async.test.ts +++ b/packages/driver-tests/test/src/node-sqlite-async.test.ts @@ -1,8 +1,12 @@ import { NodeSqliteDriver } from '@sqlite-js/driver/node'; import { describeDriverTests } from '../../lib/index.js'; +import { deleteDb } from './util.js'; describeDriverTests( 'node:sqlite worker', { getColumns: false, rawResults: false, allowsMissingParameters: true }, - (path) => NodeSqliteDriver.open(path) + async (path) => { + await deleteDb(path); + return NodeSqliteDriver.open(path); + } ); diff --git a/packages/driver-tests/test/src/node-sqlite-sync.test.ts b/packages/driver-tests/test/src/node-sqlite-sync.test.ts index f305b2a..3c5ce4d 100644 --- a/packages/driver-tests/test/src/node-sqlite-sync.test.ts +++ b/packages/driver-tests/test/src/node-sqlite-sync.test.ts @@ -2,12 +2,16 @@ import { NodeSqliteDriver } from '@sqlite-js/driver/node'; import { describeDriverTests } from '../../lib/index.js'; import { isMocha, test } from '../../lib/test.js'; +import { deleteDb } from './util.js'; if (isMocha) { describeDriverTests( 'node:sqlite direct', { getColumns: false, rawResults: false, allowsMissingParameters: true }, - (path) => NodeSqliteDriver.openInProcess(path) + async (path) => { + await deleteDb(path); + return NodeSqliteDriver.openInProcess(path); + } ); } else { test.skip('only running in mocha'); diff --git a/packages/driver-tests/test/src/util.ts b/packages/driver-tests/test/src/util.ts new file mode 100644 index 0000000..346ea10 --- /dev/null +++ b/packages/driver-tests/test/src/util.ts @@ -0,0 +1,12 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +export async function deleteDb(dbPath: string) { + const dir = path.dirname(dbPath); + try { + await fs.mkdir(dir); + } catch (e) {} + try { + await fs.rm(dbPath); + } catch (e) {} +} diff --git a/packages/wa-sqlite-driver/test/src/driver.test.ts b/packages/wa-sqlite-driver/test/src/driver.test.ts index 313ead9..3c39879 100644 --- a/packages/wa-sqlite-driver/test/src/driver.test.ts +++ b/packages/wa-sqlite-driver/test/src/driver.test.ts @@ -4,5 +4,7 @@ import { waSqlitePool } from '../../lib/index.js'; describeDriverTests( 'wa-sqlite', { getColumns: true, rawResults: true, allowsMissingParameters: false }, - (path) => waSqlitePool(':memory:') + async (path) => { + return waSqlitePool(':memory:'); + } ); diff --git a/packages/wa-sqlite-driver/vitest.config.ts b/packages/wa-sqlite-driver/vitest.config.ts index 06797bd..ae8ba7d 100644 --- a/packages/wa-sqlite-driver/vitest.config.ts +++ b/packages/wa-sqlite-driver/vitest.config.ts @@ -11,6 +11,9 @@ export default defineConfig({ exclude: ['@journeyapps/wa-sqlite'], include: [] }, + define: { + 'process.env.VITEST': JSON.stringify('true') + }, test: { // environment: 'node', // include: ['test/src/**/*.test.ts'], From 53a847d7832cf4233ddb3e600f02f65cc6d65b67 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 27 Aug 2024 17:06:08 +0200 Subject: [PATCH 04/25] Fix uncaught errors. --- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index 191688a..bc20e6e 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -35,8 +35,8 @@ export function waSqlitePool(path: string): SqliteDriverConnectionPool { const m = new mutex.Mutex(); class StatementImpl implements SqliteDriverStatement { - private preparePromise: Promise; - private bindPromise?: Promise; + private preparePromise: Promise<{ error: SqliteError | null }>; + private bindPromise?: Promise<{ error: SqliteError | null }>; private columns: string[] = []; private stringRef?: number; @@ -52,7 +52,7 @@ class StatementImpl implements SqliteDriverStatement { } async prepare() { - await m.runExclusive(() => this._prepare()); + return await m.runExclusive(() => this._prepare()); } async _prepare() { @@ -66,22 +66,36 @@ class StatementImpl implements SqliteDriverStatement { this.statementRef = r?.stmt; this.columns = sqlite3.column_names(this.statementRef!); + return { error: null }; } catch (e: any) { - throw new SqliteError({ - code: 'SQLITE_ERROR', - message: e.message - }); + return { + error: new SqliteError({ + code: 'SQLITE_ERROR', + message: e.message + }) + }; + } + } + + private async _waitForPrepare() { + const { error } = await (this.bindPromise ?? this.preparePromise); + if (error) { + throw error; } } async getColumns(): Promise { - await this.preparePromise; + await this._waitForPrepare(); return sqlite3.column_names(this.statementRef!); } bind(parameters: SqliteParameterBinding): void { - this.bindPromise = this.preparePromise.then(async () => { + this.bindPromise = this.preparePromise.then(async (result) => { + if (result.error) { + return result; + } await m.runExclusive(() => this.bindImpl(parameters)); + return { error: null }; }); } @@ -125,13 +139,12 @@ class StatementImpl implements SqliteDriverStatement { } async step(n?: number, options?: StepOptions): Promise { - await this.preparePromise; + await this._waitForPrepare(); + return await m.runExclusive(() => this._step(n, options)); } async _step(n?: number, options?: StepOptions): Promise { - await this.preparePromise; - try { if (this.done) { return { done: true }; @@ -185,7 +198,10 @@ class StatementImpl implements SqliteDriverStatement { } async _finalize() { + // Wait for these to complete, but ignore any errors. + // TODO: also wait for run/step to complete await this.preparePromise; + await this.bindPromise; if (this.statementRef) { sqlite3.finalize(this.statementRef); From a183c899a29221ceb5bfb18cc8aa55d5eb57a372 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 27 Aug 2024 17:11:23 +0200 Subject: [PATCH 05/25] Remove test file from merge conflict. --- test/src/wa-sqlite.test.ts | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 test/src/wa-sqlite.test.ts diff --git a/test/src/wa-sqlite.test.ts b/test/src/wa-sqlite.test.ts deleted file mode 100644 index 5c16bf0..0000000 --- a/test/src/wa-sqlite.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ConnectionPoolImpl, waSqlitePool } from '../../lib/index.js'; -import { describeDriverTests } from './tests/driver-tests.js'; -import { describeImplTests } from './tests/impl-tests.js'; - -describeDriverTests( - 'wa-sqlite', - { getColumns: true, rawResults: false, allowsMissingParameters: true }, - (path: string) => waSqlitePool(':memory:') -); - -describeImplTests( - 'wa-sqlite', - (path) => new ConnectionPoolImpl(waSqlitePool(':memory:')) -); From 43cfd09fa3883cb616de2cc113efb88e58265f07 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Mon, 20 Oct 2025 20:05:44 -0600 Subject: [PATCH 06/25] Partial wa-sqlite worker implementation. --- .../driver/src/util/LazyConnectionPool.ts | 4 + packages/wa-sqlite-driver/package.json | 2 +- packages/wa-sqlite-driver/src/index.ts | 2 +- packages/wa-sqlite-driver/src/pool.ts | 22 ++ .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 43 ++- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 18 ++ .../src/worker_threads/WorkerDriverAdapter.ts | 147 ++++++++++ .../src/worker_threads/async-commands.ts | 128 +++++++++ .../src/worker_threads/deferred.ts | 12 + .../src/worker_threads/index.ts | 3 + .../src/worker_threads/setup.ts | 106 +++++++ .../src/worker_threads/worker-driver.ts | 259 ++++++++++++++++++ .../wa-sqlite-driver/test/src/driver.test.ts | 5 +- packages/wa-sqlite-driver/tsconfig.json | 1 + packages/wa-sqlite-driver/vitest.config.ts | 1 + pnpm-lock.yaml | 10 +- 16 files changed, 727 insertions(+), 36 deletions(-) create mode 100644 packages/wa-sqlite-driver/src/pool.ts create mode 100644 packages/wa-sqlite-driver/src/wa-sqlite-worker.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/async-commands.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/deferred.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/index.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/setup.ts create mode 100644 packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts diff --git a/packages/driver/src/util/LazyConnectionPool.ts b/packages/driver/src/util/LazyConnectionPool.ts index 7031374..fa4a0d2 100644 --- a/packages/driver/src/util/LazyConnectionPool.ts +++ b/packages/driver/src/util/LazyConnectionPool.ts @@ -33,8 +33,12 @@ export class LazyConnectionPool implements SqliteDriverConnectionPool { } async close(): Promise { + try { await this.initPromise; await this.connection!.close(); + } catch (e) { + console.error(e); + } } onUpdate( diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index 9a85276..e1934b5 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -15,7 +15,7 @@ "author": "", "license": "MIT", "dependencies": { - "@journeyapps/wa-sqlite": "^0.3.0", + "@journeyapps/wa-sqlite": "^1.3.2", "@sqlite-js/driver": "workspace:^", "async-mutex": "^0.5.0" }, diff --git a/packages/wa-sqlite-driver/src/index.ts b/packages/wa-sqlite-driver/src/index.ts index 9c628f2..a619667 100644 --- a/packages/wa-sqlite-driver/src/index.ts +++ b/packages/wa-sqlite-driver/src/index.ts @@ -1 +1 @@ -export * from './wa-sqlite-driver.js'; +export * from './pool.js'; diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts new file mode 100644 index 0000000..249045c --- /dev/null +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -0,0 +1,22 @@ +import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; +import { LazyConnectionPool } from '@sqlite-js/driver/util'; +import { WorkerDriverConnection } from './worker_threads'; +// import { WaSqliteConnection } from './wa-sqlite-driver'; + +// export function waSqlitePool(path: string): SqliteDriverConnectionPool { +// return new LazyConnectionPool(async () => { +// return await WaSqliteConnection.open(path); +// }); +// } + +export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { + return new LazyConnectionPool(async () => { + return new WorkerDriverConnection( + new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { + type: 'module' + }), + { path } + ); + // return await WaSqliteConnection.open(path); + }); +} diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index bc20e6e..d5c4df0 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -16,21 +16,11 @@ import { import { LazyConnectionPool } from '@sqlite-js/driver/util'; import { SqliteError } from '@sqlite-js/driver'; import * as mutex from 'async-mutex'; +import { WorkerDriverConnection } from './worker_threads'; // Initialize SQLite. -const module = await SQLiteESMFactory(); -const sqlite3 = SQLite.Factory(module); - -export function waSqlitePool(path: string): SqliteDriverConnectionPool { - return new LazyConnectionPool(async () => { - return await WaSqliteConnection.open(path); - }); -} - -// // Register a custom file system. -// const vfs = await IDBBatchAtomicVFS.create('hello', module); -// // @ts-ignore -// sqlite3.vfs_register(vfs, true); +export const module = await SQLiteESMFactory(); +export const sqlite3 = SQLite.Factory(module); const m = new mutex.Mutex(); @@ -39,7 +29,6 @@ class StatementImpl implements SqliteDriverStatement { private bindPromise?: Promise<{ error: SqliteError | null }>; private columns: string[] = []; - private stringRef?: number; private statementRef?: number; private done = false; @@ -55,17 +44,21 @@ class StatementImpl implements SqliteDriverStatement { return await m.runExclusive(() => this._prepare()); } + private async getStatement() { + const statementsIter = sqlite3.statements(this.db, this.source, { + unscoped: true + }); + for await (let statement of statementsIter) { + return statement; + } + throw new Error(`No SQL statements in: ${this.source}`); + } + async _prepare() { try { - this.stringRef = sqlite3.str_new(this.db, this.source); - const strValue = sqlite3.str_value(this.stringRef); - const r = await sqlite3.prepare_v2(this.db, strValue); - if (r == null) { - throw new Error('could not prepare'); - } - - this.statementRef = r?.stmt; - this.columns = sqlite3.column_names(this.statementRef!); + const statement = await this.getStatement(); + this.statementRef = statement; + this.columns = sqlite3.column_names(statement); return { error: null }; } catch (e: any) { return { @@ -207,10 +200,6 @@ class StatementImpl implements SqliteDriverStatement { sqlite3.finalize(this.statementRef); this.statementRef = undefined; } - if (this.stringRef) { - sqlite3.str_finish(this.stringRef); - this.stringRef = undefined; - } } finalize(): void { diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts new file mode 100644 index 0000000..fb95471 --- /dev/null +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -0,0 +1,18 @@ +import { sqlite3, module, WaSqliteConnection } from './wa-sqlite-driver'; +import { setupDriverWorker } from './worker_threads'; +import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; +import { OPFSCoopSyncVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js'; + +// Register a custom file system. +// @ts-ignore +const vfs = await OPFSCoopSyncVFS.create('test.db', module, { + lockPolicy: 'exclusive' +}); +// @ts-ignore +sqlite3.vfs_register(vfs as any, true); + +setupDriverWorker({ + async openConnection(options) { + return await WaSqliteConnection.open(options.path); + } +}); diff --git a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts new file mode 100644 index 0000000..09eedf5 --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts @@ -0,0 +1,147 @@ +import { + SqliteChanges, + SqliteDriverConnection, + SqliteDriverStatement, + SqliteStepResult, + UpdateListener +} from '@sqlite-js/driver'; +import { mapError } from '@sqlite-js/driver/util'; +import { + InferBatchResult, + SqliteBind, + SqliteCommand, + SqliteCommandResponse, + SqliteCommandType, + SqliteFinalize, + SqliteParse, + SqliteParseResult, + SqlitePrepare, + SqliteReset, + SqliteRun, + SqliteStep, + WorkerDriver +} from './async-commands.js'; + +export class WorkerConnectionAdapter implements WorkerDriver { + constructor(public connnection: SqliteDriverConnection) {} + + statements = new Map(); + + async close() { + await this.connnection.close(); + } + + private requireStatement(id: number) { + const statement = this.statements.get(id); + if (statement == null) { + throw new Error(`statement not found: ${id}`); + } + return statement; + } + + private _prepare(command: SqlitePrepare): void { + const { id, sql } = command; + + const existing = this.statements.get(id); + if (existing != null) { + throw new Error( + `Replacing statement ${id} without finalizing the previous one` + ); + } + + const statement = this.connnection.prepare(sql, { + bigint: command.bigint, + persist: command.persist, + rawResults: command.rawResults + }); + this.statements.set(id, statement); + } + + private async _parse(command: SqliteParse): Promise { + const { id } = command; + const statement = this.requireStatement(id); + return { columns: await statement.getColumns() }; + } + + private _bind(command: SqliteBind): void { + const { id, parameters } = command; + const statement = this.requireStatement(id); + statement.bind(parameters); + } + + private _step(command: SqliteStep): Promise { + const { id, n, requireTransaction } = command; + const statement = this.requireStatement(id); + return statement.step(n, { requireTransaction }); + } + + private _run(command: SqliteRun): Promise { + const { id } = command; + const statement = this.requireStatement(id); + return statement.run(command); + } + + private _reset(command: SqliteReset): void { + const { id } = command; + const statement = this.requireStatement(id); + statement.reset(command); + } + + private _finalize(command: SqliteFinalize): void { + const { id } = command; + const statement = this.requireStatement(id); + statement.finalize(); + this.statements.delete(id); + } + + private async _executeCommand(command: SqliteCommand): Promise { + switch (command.type) { + case SqliteCommandType.prepare: + return this._prepare(command); + case SqliteCommandType.bind: + return this._bind(command); + case SqliteCommandType.step: + return this._step(command); + case SqliteCommandType.run: + return this._run(command); + case SqliteCommandType.reset: + return this._reset(command); + case SqliteCommandType.finalize: + return this._finalize(command); + case SqliteCommandType.parse: + return this._parse(command); + case SqliteCommandType.changes: + return this.connnection.getLastChanges(); + default: + throw new Error(`Unknown command: ${command.type}`); + } + } + + async execute( + commands: T + ): Promise> { + let results: SqliteCommandResponse[] = []; + + for (let command of commands) { + try { + const result = await this._executeCommand(command); + results.push({ value: result }); + } catch (e: any) { + const err = mapError(e); + results.push({ + error: { message: err.message, stack: err.stack, code: err.code } + }); + } + } + return results as InferBatchResult; + } + + onUpdate( + listener: UpdateListener, + options?: + | { tables?: string[] | undefined; batchLimit?: number | undefined } + | undefined + ): () => void { + throw new Error('Not implemented yet'); + } +} diff --git a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts new file mode 100644 index 0000000..d33641c --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts @@ -0,0 +1,128 @@ +import { + SqliteParameterBinding, + SqliteChanges, + SqliteStepResult +} from '@sqlite-js/driver'; +import { SerializedDriverError } from '@sqlite-js/driver'; + +export enum SqliteCommandType { + prepare = 1, + bind = 2, + step = 3, + reset = 4, + finalize = 5, + sync = 6, + parse = 7, + run = 8, + changes = 9 +} + +export type SqliteDriverError = SerializedDriverError; + +export type SqliteCommandResponse = SqliteErrorResponse | SqliteValueResponse; + +export interface SqliteErrorResponse { + error: SqliteDriverError; +} + +export interface SqliteValueResponse { + value: T; +} + +export interface SqliteBaseCommand { + type: SqliteCommandType; +} + +export interface SqlitePrepare extends SqliteBaseCommand { + type: SqliteCommandType.prepare; + id: number; + sql: string; + bigint?: boolean; + persist?: boolean; + rawResults?: boolean; +} + +export interface SqliteParseResult { + columns: string[]; +} + +export interface SqliteBind extends SqliteBaseCommand { + type: SqliteCommandType.bind; + id: number; + parameters: SqliteParameterBinding; +} + +export interface SqliteParse extends SqliteBaseCommand { + type: SqliteCommandType.parse; + id: number; +} + +export interface SqliteStep extends SqliteBaseCommand { + type: SqliteCommandType.step; + id: number; + n?: number; + requireTransaction?: boolean; +} + +export interface SqliteRun extends SqliteBaseCommand { + type: SqliteCommandType.run; + id: number; + requireTransaction?: boolean; +} + +export interface SqliteReset extends SqliteBaseCommand { + type: SqliteCommandType.reset; + id: number; + clearBindings?: boolean; +} + +export interface SqliteFinalize extends SqliteBaseCommand { + type: SqliteCommandType.finalize; + id: number; +} + +export interface SqliteSync { + type: SqliteCommandType.sync; +} + +export interface SqliteGetChanges { + type: SqliteCommandType.changes; +} + +export type SqliteCommand = + | SqlitePrepare + | SqliteBind + | SqliteStep + | SqliteRun + | SqliteReset + | SqliteFinalize + | SqliteSync + | SqliteParse + | SqliteGetChanges; + +export type InferCommandResult = T extends SqliteRun + ? SqliteChanges + : T extends SqliteStep + ? SqliteStepResult + : T extends SqliteParse + ? SqliteParseResult + : T extends SqliteGetChanges + ? SqliteChanges + : void; + +export type InferBatchResult = { + [i in keyof T]: + | SqliteErrorResponse + | SqliteValueResponse>; +}; + +export function isErrorResponse( + response: SqliteCommandResponse +): response is SqliteErrorResponse { + return (response as SqliteErrorResponse).error != null; +} + +export interface WorkerDriver { + execute(commands: SqliteCommand[]): Promise; + close(): Promise; +} diff --git a/packages/wa-sqlite-driver/src/worker_threads/deferred.ts b/packages/wa-sqlite-driver/src/worker_threads/deferred.ts new file mode 100644 index 0000000..2508399 --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/deferred.ts @@ -0,0 +1,12 @@ +export class Deferred { + promise: Promise; + resolve: (result: T) => void = undefined as any; + reject: (error: any) => void = undefined as any; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/packages/wa-sqlite-driver/src/worker_threads/index.ts b/packages/wa-sqlite-driver/src/worker_threads/index.ts new file mode 100644 index 0000000..7d1c108 --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/index.ts @@ -0,0 +1,3 @@ +export * from './async-commands.js'; +export * from './worker-driver.js'; +export * from './setup.js'; diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts new file mode 100644 index 0000000..c77fee2 --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -0,0 +1,106 @@ +import { Deferred } from './deferred.js'; +import { isErrorResponse, WorkerDriver } from './async-commands.js'; + +import type { WorkerDriverConnectionOptions } from './worker-driver.js'; +import { SqliteDriverConnection } from '@sqlite-js/driver'; +import { WorkerConnectionAdapter } from './WorkerDriverAdapter.js'; + +export type { WorkerDriverConnectionOptions }; + +export interface WorkerDriverConfig { + openConnection: ( + options: WorkerDriverConnectionOptions + ) => Promise; +} + +export function setupDriverWorker(config: WorkerDriverConfig) { + return setupDriverPort(config); +} + +export function setupDriverPort(config: WorkerDriverConfig) { + let db: WorkerDriver | null = null; + let opened = new Deferred(); + const port = { + postMessage: self.postMessage.bind(self) + }; + + const listener = async (value: any) => { + const [message, id, args] = value.data; + + console.log('received', message, id, args); + + if (message == 'open') { + try { + const connection = await config.openConnection( + args as WorkerDriverConnectionOptions + ); + db = new WorkerConnectionAdapter(connection); + port.postMessage({ id }); + opened.resolve(); + } catch (e: any) { + opened.reject(e); + port.postMessage({ id, value: { error: { message: e.message } } }); + } + } else if (message == 'close') { + try { + await opened.promise; + await db?.close(); + port.postMessage({ id }); + } catch (e: any) { + port.postMessage({ id, value: { error: { message: e.message } } }); + } + } else if (message == 'execute') { + await opened.promise; + const commands = args; + + const results = (await db!.execute(commands)).map((r) => { + if (isErrorResponse(r)) { + const error = r.error; + return { + error: { + code: error.code, + message: error.message, + stack: error.stack + } + }; + } else { + return r; + } + }); + port.postMessage({ + id, + value: results + }); + } else { + throw new Error(`Unknown message: ${message}`); + } + }; + + self.onmessage = listener; + + port.postMessage({ id: 0, value: 'ready' }); + + return () => { + // port.removeEventListener('message', listener); + }; +} + +export async function retriedOpen( + open: () => SqliteDriverConnection, + timeout: number +) { + const endTime = performance.now() + timeout; + let delay = 1; + while (true) { + try { + return open(); + } catch (e) { + console.error(e); + if (performance.now() >= endTime) { + throw e; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + delay = Math.min(delay * 1.5, 60); + } + } +} diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts new file mode 100644 index 0000000..3862455 --- /dev/null +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -0,0 +1,259 @@ +import { + PrepareOptions, + ResetOptions, + SqliteDriverConnection, + SqliteDriverStatement, + SqliteParameterBinding, + SqliteChanges, + SqliteStepResult, + StepOptions, + UpdateListener +} from '@sqlite-js/driver'; + +import { Deferred } from './deferred.js'; +import { SqliteError } from '@sqlite-js/driver'; +import { + InferBatchResult, + InferCommandResult, + isErrorResponse, + SqliteCommand, + SqliteCommandType, + SqliteDriverError +} from './async-commands.js'; + +export interface WorkerDriverConnectionOptions { + path: string; + connectionName?: string; + readonly?: boolean; + workerOptions?: WorkerOptions; +} + +/** + * Driver connection using worker_threads. + */ +export class WorkerDriverConnection implements SqliteDriverConnection { + worker: Worker; + private callbacks = new Map void>(); + private nextCallbackId = 1; + private ready: Promise; + private closing = false; + private nextId = 1; + + buffer: CommandQueueItem[] = []; + + constructor(worker: Worker, options: WorkerDriverConnectionOptions) { + this.worker = worker; + worker.addEventListener('error', (err) => { + console.error('worker error', err.message, err); + }); + this.ready = new Promise((resolve) => { + worker.addEventListener('message', (event) => { + const { id, value } = event.data; + console.log('gotta message', id, value); + if (id == 0) { + resolve(); + return; + } + const callback = this.callbacks.get(id); + if (callback == null) { + throw new Error(`No callback with id ${id}`); + } + this.callbacks.delete(id); + callback(value); + }); + }); + this.post('open', options); + this.worker = worker; + } + + prepare(sql: string, options?: PrepareOptions): WorkerDriverStatement { + const id = this.nextId++; + this.buffer.push({ + cmd: { + type: SqliteCommandType.prepare, + id, + bigint: options?.bigint, + persist: options?.persist, + rawResults: options?.rawResults, + sql + } + }); + return new WorkerDriverStatement(this, id); + } + + async getLastChanges(): Promise { + return await this._push({ + type: SqliteCommandType.changes + }); + } + + async sync(): Promise { + await this._push({ + type: SqliteCommandType.sync + }); + } + + _push(cmd: T): Promise> { + const d = new Deferred(); + this.buffer.push({ cmd, resolve: d.resolve, reject: d.reject }); + this._maybeFlush(); + return d.promise as Promise>; + } + + _send(cmd: SqliteCommand): void { + this.buffer.push({ cmd }); + } + + private registerCallback(callback: (value: any) => void) { + const id = this.nextCallbackId++; + this.callbacks.set(id, callback); + return id; + } + + private async post(command: string, args: any): Promise { + await this.ready; + let id: number; + const p = new Promise((resolve) => { + id = this.registerCallback(resolve); + }); + console.log('posting', command, id, args); + this.worker.postMessage([command, id!, args]); + const result = await p; + const error = (result as any)?.error; + if (error != null) { + return { + error: new SqliteError(error) + } as any; + } + return p; + } + + async close() { + if (this.closing) { + return; + } + this.closing = true; + await this._flush(); + const r: any = await this.post('close', {}); + if (r?.error) { + throw r.error; + } + await this.worker.terminate(); + } + + private inProgress = 0; + + async _flush() { + const commands = this.buffer; + if (commands.length == 0) { + return; + } + this.buffer = []; + const r = await this._execute(commands.map((c) => c.cmd)); + for (let i = 0; i < commands.length; i++) { + const c = commands[i]; + const rr = r[i]; + if (isErrorResponse(rr)) { + c.reject?.(rr.error); + } else if (c.resolve) { + c.resolve!(rr.value); + } + } + } + + async _maybeFlush() { + if (this.inProgress <= 2) { + this.inProgress += 1; + try { + while (this.buffer.length > 0) { + await this._flush(); + } + } finally { + this.inProgress -= 1; + } + } + } + + async _execute( + commands: T + ): Promise> { + return await this.post('execute', commands); + } + + onUpdate( + listener: UpdateListener, + options?: + | { tables?: string[] | undefined; batchLimit?: number | undefined } + | undefined + ): () => void { + throw new Error('Not implemented'); + } +} + +class WorkerDriverStatement implements SqliteDriverStatement { + [Symbol.dispose]: () => void = undefined as any; + + constructor( + private driver: WorkerDriverConnection, + private id: number + ) { + if (typeof Symbol.dispose != 'undefined') { + this[Symbol.dispose] = () => this.finalize(); + } + } + + async getColumns(): Promise { + return this.driver + ._push({ + type: SqliteCommandType.parse, + id: this.id + }) + .then((r) => r.columns); + } + + bind(parameters: SqliteParameterBinding): void { + this.driver._send({ + type: SqliteCommandType.bind, + id: this.id, + parameters: parameters + }); + } + + async step(n?: number, options?: StepOptions): Promise { + return this.driver._push({ + type: SqliteCommandType.step, + id: this.id, + n: n, + requireTransaction: options?.requireTransaction + }); + } + + async run(options?: StepOptions): Promise { + return this.driver._push({ + type: SqliteCommandType.run, + id: this.id, + requireTransaction: options?.requireTransaction + }); + } + + finalize(): void { + this.driver._send({ + type: SqliteCommandType.finalize, + id: this.id + }); + } + + reset(options?: ResetOptions): void { + this.driver._send({ + type: SqliteCommandType.reset, + id: this.id, + clearBindings: options?.clearBindings + }); + } +} + +interface CommandQueueItem { + cmd: SqliteCommand; + resolve?: (r: any) => void; + reject?: (e: SqliteDriverError) => void; +} diff --git a/packages/wa-sqlite-driver/test/src/driver.test.ts b/packages/wa-sqlite-driver/test/src/driver.test.ts index 3c39879..7030eb4 100644 --- a/packages/wa-sqlite-driver/test/src/driver.test.ts +++ b/packages/wa-sqlite-driver/test/src/driver.test.ts @@ -1,10 +1,11 @@ import { describeDriverTests } from '@sqlite-js/driver-tests'; -import { waSqlitePool } from '../../lib/index.js'; +import { waSqliteWorkerPool } from '../../lib/index.js'; describeDriverTests( 'wa-sqlite', { getColumns: true, rawResults: true, allowsMissingParameters: false }, async (path) => { - return waSqlitePool(':memory:'); + console.log('open', path); + return waSqliteWorkerPool(path); } ); diff --git a/packages/wa-sqlite-driver/tsconfig.json b/packages/wa-sqlite-driver/tsconfig.json index f8e62d7..57c042b 100644 --- a/packages/wa-sqlite-driver/tsconfig.json +++ b/packages/wa-sqlite-driver/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "lib": ["ES2022", "DOM"], "outDir": "lib", "rootDir": "src", "esModuleInterop": true, diff --git a/packages/wa-sqlite-driver/vitest.config.ts b/packages/wa-sqlite-driver/vitest.config.ts index ae8ba7d..4c88f04 100644 --- a/packages/wa-sqlite-driver/vitest.config.ts +++ b/packages/wa-sqlite-driver/vitest.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ 'process.env.VITEST': JSON.stringify('true') }, test: { + maxConcurrency: 1, // environment: 'node', // include: ['test/src/**/*.test.ts'], browser: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 986a2e0..50ac26f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,8 +145,8 @@ importers: packages/wa-sqlite-driver: dependencies: '@journeyapps/wa-sqlite': - specifier: ^0.3.0 - version: 0.3.0 + specifier: ^1.3.2 + version: 1.3.2 '@sqlite-js/driver': specifier: workspace:^ version: link:../driver @@ -534,8 +534,8 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@journeyapps/wa-sqlite@0.3.0': - resolution: {integrity: sha512-LQMjcMh92myqzq9kpKFJJ+t1zY7owHTq8TvVYG83luCKzaZepNk86jNB/56fb/vCEy1PQBRc/cI7BTt10SfItA==} + '@journeyapps/wa-sqlite@1.3.2': + resolution: {integrity: sha512-zZ0KF01an940DV2jJHHzeTvGlXZ8il/sWnI6a2hH3eV/LHtb5H0c6TB7WursE3c8BvNLifXre3R0mLM5OyNhgw==} '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} @@ -3015,7 +3015,7 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@journeyapps/wa-sqlite@0.3.0': {} + '@journeyapps/wa-sqlite@1.3.2': {} '@jridgewell/gen-mapping@0.3.5': dependencies: From 35cb0aeb21248710fa2084eccf4b9c4ee3d6dc0c Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 21 Oct 2025 17:45:20 -0600 Subject: [PATCH 07/25] More robust error handling. --- packages/wa-sqlite-driver/src/pool.ts | 4 +- .../src/worker_threads/setup.ts | 57 ++++++++++++------- .../src/worker_threads/worker-driver.ts | 13 ++++- 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index 249045c..70c38d0 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -11,12 +11,14 @@ import { WorkerDriverConnection } from './worker_threads'; export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { return new LazyConnectionPool(async () => { - return new WorkerDriverConnection( + const connection = new WorkerDriverConnection( new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { type: 'module' }), { path } ); + await connection.open(); + return connection; // return await WaSqliteConnection.open(path); }); } diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts index c77fee2..af3f106 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/setup.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -19,7 +19,7 @@ export function setupDriverWorker(config: WorkerDriverConfig) { export function setupDriverPort(config: WorkerDriverConfig) { let db: WorkerDriver | null = null; - let opened = new Deferred(); + let opened: Promise | undefined = undefined; const port = { postMessage: self.postMessage.bind(self) }; @@ -30,47 +30,60 @@ export function setupDriverPort(config: WorkerDriverConfig) { console.log('received', message, id, args); if (message == 'open') { + const open = new Deferred(); + opened = open.promise; try { const connection = await config.openConnection( args as WorkerDriverConnectionOptions ); db = new WorkerConnectionAdapter(connection); port.postMessage({ id }); - opened.resolve(); + open.resolve(); } catch (e: any) { - opened.reject(e); + open.reject(e); port.postMessage({ id, value: { error: { message: e.message } } }); } } else if (message == 'close') { try { - await opened.promise; + await opened; await db?.close(); port.postMessage({ id }); } catch (e: any) { port.postMessage({ id, value: { error: { message: e.message } } }); } } else if (message == 'execute') { - await opened.promise; - const commands = args; + try { + await opened; + const commands = args; - const results = (await db!.execute(commands)).map((r) => { - if (isErrorResponse(r)) { - const error = r.error; - return { + const results = (await db!.execute(commands)).map((r) => { + if (isErrorResponse(r)) { + const error = r.error; + return { + error: { + code: error.code, + message: error.message, + stack: error.stack + } + }; + } else { + return r; + } + }); + port.postMessage({ + id, + value: results + }); + } catch (e) { + port.postMessage({ + id, + value: args.map((c) => ({ error: { - code: error.code, - message: error.message, - stack: error.stack + message: e.message } - }; - } else { - return r; - } - }); - port.postMessage({ - id, - value: results - }); + })) + }); + } } else { throw new Error(`Unknown message: ${message}`); } diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index 3862455..ad1f6df 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -38,11 +38,13 @@ export class WorkerDriverConnection implements SqliteDriverConnection { private ready: Promise; private closing = false; private nextId = 1; + private options: WorkerDriverConnectionOptions; buffer: CommandQueueItem[] = []; constructor(worker: Worker, options: WorkerDriverConnectionOptions) { this.worker = worker; + this.options = options; worker.addEventListener('error', (err) => { console.error('worker error', err.message, err); }); @@ -62,10 +64,13 @@ export class WorkerDriverConnection implements SqliteDriverConnection { callback(value); }); }); - this.post('open', options); this.worker = worker; } + open() { + return this.post('open', this.options); + } + prepare(sql: string, options?: PrepareOptions): WorkerDriverStatement { const id = this.nextId++; this.buffer.push({ @@ -125,7 +130,7 @@ export class WorkerDriverConnection implements SqliteDriverConnection { error: new SqliteError(error) } as any; } - return p; + return result; } async close() { @@ -153,7 +158,9 @@ export class WorkerDriverConnection implements SqliteDriverConnection { for (let i = 0; i < commands.length; i++) { const c = commands[i]; const rr = r[i]; - if (isErrorResponse(rr)) { + if (rr == null) { + c.reject?.({ message: 'no result received', code: '' }); + } else if (isErrorResponse(rr)) { c.reject?.(rr.error); } else if (c.resolve) { c.resolve!(rr.value); From 12a3ff9d393c4f832cb590881a53362ad8fe3050 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Tue, 21 Oct 2025 17:56:51 -0600 Subject: [PATCH 08/25] Fix opening db issues. --- packages/driver-tests/src/driver-tests.ts | 8 +++++++- packages/wa-sqlite-driver/src/worker_threads/setup.ts | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/driver-tests/src/driver-tests.ts b/packages/driver-tests/src/driver-tests.ts index 88ef9c0..db0aa7a 100644 --- a/packages/driver-tests/src/driver-tests.ts +++ b/packages/driver-tests/src/driver-tests.ts @@ -21,10 +21,16 @@ export function describeDriverTests( }; beforeEach((context) => { - const testNameSanitized = context.fullName.replaceAll( + let testNameSanitized = context.fullName.replaceAll( /[\s\/\\>\.\-\:]+/g, '_' ); + + if (testNameSanitized.length > 10) { + testNameSanitized = + testNameSanitized.substring(testNameSanitized.length - 7) + + String(Math.random()).substring(1, 4); + } dbPath = `test-db/${testNameSanitized}.db`; }); diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts index af3f106..d432a40 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/setup.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -41,7 +41,10 @@ export function setupDriverPort(config: WorkerDriverConfig) { open.resolve(); } catch (e: any) { open.reject(e); - port.postMessage({ id, value: { error: { message: e.message } } }); + port.postMessage({ + id, + value: { error: { message: e.message, code: e.code } } + }); } } else if (message == 'close') { try { From aa41480498f70cd38cceea399f58186f296a76d9 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Wed, 22 Oct 2025 17:42:33 -0600 Subject: [PATCH 09/25] Typed OPFSCoopSyncVFS2. --- benchmarks/package.json | 2 +- package.json | 2 +- packages/api/package.json | 2 +- packages/better-sqlite3-driver/package.json | 2 +- packages/driver-tests/package.json | 2 +- packages/driver/package.json | 2 +- packages/wa-sqlite-driver/package.json | 2 +- .../wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 577 ++++++++++++++++++ .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 7 +- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 8 +- packages/wa-sqlite-driver/tsconfig.json | 2 +- pnpm-lock.yaml | 58 +- 12 files changed, 618 insertions(+), 48 deletions(-) create mode 100644 packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts diff --git a/benchmarks/package.json b/benchmarks/package.json index 474dd11..59d168b 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "@types/node": "^20.14.2", - "typescript": "^5.4.5" + "typescript": "^5.9.3" } } diff --git a/package.json b/package.json index f316ed3..bcd63e5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prettier": "^3.2.5", "ts-node": "^10.9.2", "tsx": "^4.16.2", - "typescript": "^5.4.5", + "typescript": "^5.9.3", "vitest": "^2.0.5" }, "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" diff --git a/packages/api/package.json b/packages/api/package.json index 5ff4ba7..5547045 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@types/node": "^22.3.0", - "typescript": "^5.5.4", + "typescript": "^5.9.3", "vitest": "^2.0.5", "@sqlite-js/better-sqlite3-driver": "workspace:^", "@sqlite-js/driver-tests": "workspace:^" diff --git a/packages/better-sqlite3-driver/package.json b/packages/better-sqlite3-driver/package.json index 2e00018..981d3c5 100644 --- a/packages/better-sqlite3-driver/package.json +++ b/packages/better-sqlite3-driver/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@types/node": "^22.3.0", - "typescript": "^5.5.4", + "typescript": "^5.9.3", "vitest": "^2.0.5", "@sqlite-js/driver-tests": "workspace:^" } diff --git a/packages/driver-tests/package.json b/packages/driver-tests/package.json index e5dedd9..57fd965 100644 --- a/packages/driver-tests/package.json +++ b/packages/driver-tests/package.json @@ -23,6 +23,6 @@ }, "devDependencies": { "@types/node": "^22.3.0", - "typescript": "^5.5.4" + "typescript": "^5.9.3" } } diff --git a/packages/driver/package.json b/packages/driver/package.json index ca43081..7761c2b 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -21,6 +21,6 @@ "dependencies": {}, "devDependencies": { "@types/node": "^22.3.0", - "typescript": "^5.5.4" + "typescript": "^5.9.3" } } diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index e1934b5..3adb50f 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -24,7 +24,7 @@ "vite-plugin-wasm": "^3.3.0", "@sqlite-js/driver-tests": "workspace:^", "@types/node": "^22.3.0", - "typescript": "^5.5.4", + "typescript": "^5.9.3", "@vitest/browser": "^2.0.5", "vitest": "^2.0.5", "playwright": "^1.45.3", diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts new file mode 100644 index 0000000..398e57b --- /dev/null +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -0,0 +1,577 @@ +// Copyright 2024 Roy T. Hashimoto. All Rights Reserved. +import { FacadeVFS } from '@journeyapps/wa-sqlite/src/FacadeVFS.js'; +import * as VFS from '@journeyapps/wa-sqlite/src/VFS.js'; + +const DEFAULT_TEMPORARY_FILES = 10; +const LOCK_NOTIFY_INTERVAL = 1000; + +const DB_RELATED_FILE_SUFFIXES = ['', '-journal', '-wal']; + +const finalizationRegistry = new FinalizationRegistry<() => void>((releaser) => + releaser() +); + +class File { + path: string; + flags: number; + accessHandle: FileSystemSyncAccessHandle; + + persistentFile?: PersistentFile; + + constructor(path, flags) { + this.path = path; + this.flags = flags; + } +} + +interface FileSystemSyncAccessHandle extends FileSystemHandle { + truncate(n: number): void; + read(buffer: Uint8Array, options: { at: number }); + flush(): void; + write(buffer: Uint8Array, options: { at: number }); + getSize(): number; + close(): void; +} + +class PersistentFile { + fileHandle: FileSystemFileHandle; + accessHandle: null | FileSystemSyncAccessHandle = null; + + // The following properties are for main database files. + + isLockBusy = false; + isFileLocked = false; + isRequestInProgress = false; + handleLockReleaser: null | (() => void) = null; + + handleRequestChannel: BroadcastChannel; + isHandleRequested = false; + + constructor(fileHandle) { + this.fileHandle = fileHandle; + } +} + +export class OPFSCoopSyncVFS2 extends FacadeVFS { + mapIdToFile = new Map(); + + lastError = null; + log = null; //function(...args) { console.log(`[${contextName}]`, ...args) }; + + persistentFiles = new Map(); + boundAccessHandles = new Map(); + unboundAccessHandles = new Set(); + accessiblePaths = new Set(); + releaser: null | (() => void) = null; + + _module: { retryOps: Promise[] }; + + static async create(name, module) { + const vfs = new OPFSCoopSyncVFS2(name, module); + await Promise.all([ + (vfs as any).isReady(), + vfs.#initialize(DEFAULT_TEMPORARY_FILES) + ]); + return vfs; + } + + constructor(name, module) { + super(name, module); + } + + async #initialize(nTemporaryFiles) { + // Delete temporary directories no longer in use. + const root = await navigator.storage.getDirectory(); + // @ts-ignore + for await (const entry of root.values()) { + if (entry.kind === 'directory' && entry.name.startsWith('.ahp-')) { + // A lock with the same name as the directory protects it from + // being deleted. + await navigator.locks.request( + entry.name, + { ifAvailable: true }, + async (lock) => { + if (lock) { + this.log?.(`Deleting temporary directory ${entry.name}`); + await root.removeEntry(entry.name, { recursive: true }); + } else { + this.log?.(`Temporary directory ${entry.name} is in use`); + } + } + ); + } + } + + // Create our temporary directory. + const tmpDirName = `.ahp-${Math.random().toString(36).slice(2)}`; + this.releaser = await new Promise((resolve) => { + navigator.locks.request(tmpDirName, () => { + return new Promise((release) => { + resolve(release); + }); + }); + }); + finalizationRegistry.register(this, this.releaser); + const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true }); + + // Populate temporary directory. + for (let i = 0; i < nTemporaryFiles; i++) { + const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true }); + const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle(); + this.unboundAccessHandles.add(tmpAccessHandle); + } + } + + jOpen( + zName: string | undefined, + fileId: number, + flags: number, + pOutFlags: DataView + ): number { + try { + const url = new URL( + zName || Math.random().toString(36).slice(2), + 'file://' + ); + const path = url.pathname; + + if (flags & VFS.SQLITE_OPEN_MAIN_DB) { + const persistentFile = this.persistentFiles.get(path); + if (persistentFile?.isRequestInProgress) { + // Should not reach here unless SQLite itself retries an open. + // Otherwise, asynchronous operations started on a previous + // open try should have completed. + return VFS.SQLITE_BUSY; + } else if (!persistentFile) { + // This is the usual starting point for opening a database. + // Register a Promise that resolves when the database and related + // files are ready to be used. + this.log?.(`creating persistent file for ${path}`); + const create = !!(flags & VFS.SQLITE_OPEN_CREATE); + this._module.retryOps.push( + (async () => { + try { + // Get the path directory handle. + let dirHandle = await navigator.storage.getDirectory(); + const directories = path.split('/').filter((d) => d); + const filename = directories.pop(); + for (const directory of directories) { + dirHandle = await dirHandle.getDirectoryHandle(directory, { + create + }); + } + + // Get file handles for the database and related files, + // and create persistent file instances. + for (const suffix of DB_RELATED_FILE_SUFFIXES) { + const fileHandle = await dirHandle.getFileHandle( + filename + suffix, + { create } + ); + await this.#createPersistentFile(fileHandle); + } + + // Get access handles for the files. + const file = new File(path, flags); + file.persistentFile = this.persistentFiles.get(path); + await this.#requestAccessHandle(file); + } catch (e) { + // Use an invalid persistent file to signal this error + // for the retried open. + const persistentFile = new PersistentFile(null); + this.persistentFiles.set(path, persistentFile); + console.error(e); + } + })() + ); + return VFS.SQLITE_BUSY; + } else if (!persistentFile.fileHandle) { + // The asynchronous open operation failed. + this.persistentFiles.delete(path); + return VFS.SQLITE_CANTOPEN; + } else if (!persistentFile.accessHandle) { + // This branch is reached if the database was previously opened + // and closed. + this._module.retryOps.push( + (async () => { + const file = new File(path, flags); + file.persistentFile = this.persistentFiles.get(path); + await this.#requestAccessHandle(file); + })() + ); + return VFS.SQLITE_BUSY; + } + } + + if ( + !this.accessiblePaths.has(path) && + !(flags & VFS.SQLITE_OPEN_CREATE) + ) { + throw new Error(`File ${path} not found`); + } + + const file = new File(path, flags); + this.mapIdToFile.set(fileId, file); + + if (this.persistentFiles.has(path)) { + file.persistentFile = this.persistentFiles.get(path); + } else if (this.boundAccessHandles.has(path)) { + // This temporary file was previously created and closed. Reopen + // the same access handle. + file.accessHandle = this.boundAccessHandles.get(path); + } else if (this.unboundAccessHandles.size) { + // Associate an unbound access handle to this file. + file.accessHandle = this.unboundAccessHandles.values().next().value; + file.accessHandle.truncate(0); + this.unboundAccessHandles.delete(file.accessHandle); + this.boundAccessHandles.set(path, file.accessHandle); + } + this.accessiblePaths.add(path); + + pOutFlags.setInt32(0, flags, true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_CANTOPEN; + } + } + + jDelete(zName: string, syncDir: number): number { + try { + const url = new URL(zName, 'file://'); + const path = url.pathname; + if (this.persistentFiles.has(path)) { + const persistentFile = this.persistentFiles.get(path); + persistentFile.accessHandle.truncate(0); + } else { + this.boundAccessHandles.get(path)?.truncate(0); + } + this.accessiblePaths.delete(path); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_DELETE; + } + } + + jAccess(zName: string, flags: number, pResOut: DataView): number { + try { + const url = new URL(zName, 'file://'); + const path = url.pathname; + pResOut.setInt32(0, this.accessiblePaths.has(path) ? 1 : 0, true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_ACCESS; + } + } + + jClose(fileId: number): number { + try { + const file = this.mapIdToFile.get(fileId); + this.mapIdToFile.delete(fileId); + + if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { + if (file.persistentFile?.handleLockReleaser) { + this.#releaseAccessHandle(file); + } + } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { + file.accessHandle.truncate(0); + this.accessiblePaths.delete(file.path); + if (!this.persistentFiles.has(file.path)) { + this.boundAccessHandles.delete(file.path); + this.unboundAccessHandles.add(file.accessHandle); + } + } + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_CLOSE; + } + } + + jRead(fileId: number, pData: Uint8Array, iOffset: number): number { + try { + const file = this.mapIdToFile.get(fileId); + + // On Chrome (at least), passing pData to accessHandle.read() is + // an error because pData is a Proxy of a Uint8Array. Calling + // subarray() produces a real Uint8Array and that works. + const accessHandle = + file.accessHandle || file.persistentFile.accessHandle; + const bytesRead = accessHandle.read(pData.subarray(), { at: iOffset }); + + // Opening a database file performs one read without a xLock call. + if ( + file.flags & VFS.SQLITE_OPEN_MAIN_DB && + !file.persistentFile.isFileLocked + ) { + this.#releaseAccessHandle(file); + } + + if (bytesRead < pData.byteLength) { + pData.fill(0, bytesRead); + return VFS.SQLITE_IOERR_SHORT_READ; + } + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_READ; + } + } + + jWrite(fileId: number, pData: Uint8Array, iOffset: number): number { + try { + const file = this.mapIdToFile.get(fileId); + + // On Chrome (at least), passing pData to accessHandle.write() is + // an error because pData is a Proxy of a Uint8Array. Calling + // subarray() produces a real Uint8Array and that works. + const accessHandle = + file.accessHandle || file.persistentFile.accessHandle; + const nBytes = accessHandle.write(pData.subarray(), { at: iOffset }); + if (nBytes !== pData.byteLength) throw new Error('short write'); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_WRITE; + } + } + + jTruncate(fileId: number, iSize: number): number { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = + file.accessHandle || file.persistentFile.accessHandle; + accessHandle.truncate(iSize); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_TRUNCATE; + } + } + + jSync(fileId: number, flags: number): number { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = + file.accessHandle || file.persistentFile.accessHandle; + accessHandle.flush(); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSYNC; + } + } + + jFileSize(fileId: number, pSize64: DataView): number { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = + file.accessHandle || file.persistentFile.accessHandle; + const size = accessHandle.getSize(); + pSize64.setBigInt64(0, BigInt(size), true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSTAT; + } + } + + jLock(fileId: number, lockType: number): number { + const file = this.mapIdToFile.get(fileId); + if (file.persistentFile.isRequestInProgress) { + file.persistentFile.isLockBusy = true; + return VFS.SQLITE_BUSY; + } + + file.persistentFile.isFileLocked = true; + if (!file.persistentFile.handleLockReleaser) { + // Start listening for notifications from other connections. + // This is before we actually get access handles, but waiting to + // listen until then allows a race condition where notifications + // are missed. + file.persistentFile.handleRequestChannel.onmessage = () => { + this.log?.(`received notification for ${file.path}`); + if (file.persistentFile.isFileLocked) { + // We're still using the access handle, so mark it to be + // released when we're done. + file.persistentFile.isHandleRequested = true; + } else { + // Release the access handles immediately. + this.#releaseAccessHandle(file); + } + file.persistentFile.handleRequestChannel.onmessage = null; + }; + + this.#requestAccessHandle(file); + this.log?.('returning SQLITE_BUSY'); + file.persistentFile.isLockBusy = true; + return VFS.SQLITE_BUSY; + } + file.persistentFile.isLockBusy = false; + return VFS.SQLITE_OK; + } + + jUnlock(fileId: number, lockType: number): number { + const file = this.mapIdToFile.get(fileId); + if (lockType === VFS.SQLITE_LOCK_NONE) { + // Don't change any state if this unlock is because xLock returned + // SQLITE_BUSY. + if (!file.persistentFile.isLockBusy) { + if (file.persistentFile.isHandleRequested) { + // Another connection wants the access handle. + this.#releaseAccessHandle(file); + file.persistentFile.isHandleRequested = false; + } + file.persistentFile.isFileLocked = false; + } + } + return VFS.SQLITE_OK; + } + + jFileControl( + fileId: number, + op: number, + pArg: DataView + ): number | Promise { + try { + const file = this.mapIdToFile.get(fileId); + switch (op) { + case VFS.SQLITE_FCNTL_PRAGMA: + const key = extractString(pArg, 4); + const value = extractString(pArg, 8); + this.log?.('xFileControl', file.path, 'PRAGMA', key, value); + switch (key.toLowerCase()) { + case 'journal_mode': + if ( + value && + !['off', 'memory', 'delete', 'wal'].includes( + value.toLowerCase() + ) + ) { + throw new Error( + 'journal_mode must be "off", "memory", "delete", or "wal"' + ); + } + break; + } + break; + } + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR; + } + return VFS.SQLITE_NOTFOUND; + } + + jGetLastError(zBuf: Uint8Array): number { + if (this.lastError) { + console.error(this.lastError); + const outputArray = zBuf.subarray(0, zBuf.byteLength - 1); + const { written } = new TextEncoder().encodeInto( + this.lastError.message, + outputArray + ); + zBuf[written] = 0; + } + return VFS.SQLITE_OK; + } + + async #createPersistentFile( + fileHandle: FileSystemFileHandle + ): Promise { + const persistentFile = new PersistentFile(fileHandle); + const root = await navigator.storage.getDirectory(); + const relativePath = await root.resolve(fileHandle); + const path = `/${relativePath.join('/')}`; + persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`); + this.persistentFiles.set(path, persistentFile); + + const f = await fileHandle.getFile(); + if (f.size) { + this.accessiblePaths.add(path); + } + return persistentFile; + } + + #requestAccessHandle(file: File): Promise { + console.assert(!file.persistentFile.handleLockReleaser); + if (!file.persistentFile.isRequestInProgress) { + file.persistentFile.isRequestInProgress = true; + this._module.retryOps.push( + (async () => { + // Acquire the Web Lock. + file.persistentFile.handleLockReleaser = await this.#acquireLock( + file.persistentFile + ); + + // Get access handles for the database and releated files in parallel. + this.log?.(`creating access handles for ${file.path}`); + await Promise.all( + DB_RELATED_FILE_SUFFIXES.map(async (suffix) => { + const persistentFile = this.persistentFiles.get( + file.path + suffix + ); + if (persistentFile) { + persistentFile.accessHandle = await ( + persistentFile.fileHandle as any + ).createSyncAccessHandle(); + } + }) + ); + file.persistentFile.isRequestInProgress = false; + })() + ); + return this._module.retryOps.at(-1); + } + return Promise.resolve(); + } + + async #releaseAccessHandle(file: File): Promise { + DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => { + const persistentFile = this.persistentFiles.get(file.path + suffix); + if (persistentFile) { + persistentFile.accessHandle?.close(); + persistentFile.accessHandle = null; + } + }); + this.log?.(`access handles closed for ${file.path}`); + + file.persistentFile.handleLockReleaser?.(); + file.persistentFile.handleLockReleaser = null; + this.log?.(`lock released for ${file.path}`); + } + + #acquireLock(persistentFile: PersistentFile): Promise<() => void> { + return new Promise<() => void>((resolve) => { + // Tell other connections we want the access handle. + const lockName = persistentFile.handleRequestChannel.name; + const notify = () => { + this.log?.(`notifying for ${lockName}`); + persistentFile.handleRequestChannel.postMessage(null); + }; + const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL); + setTimeout(notify); + + this.log?.(`lock requested: ${lockName}`); + navigator.locks.request(lockName, (lock) => { + // We have the lock. Stop asking other connections for it. + this.log?.(`lock acquired: ${lockName}`, lock); + clearInterval(notifyId); + return new Promise<() => void>((res) => { + resolve(res as () => void); + }); + }); + }); + } +} + +function extractString(dataView: DataView, offset: number): string | null { + const p = dataView.getUint32(offset, true); + if (p) { + const chars = new Uint8Array(dataView.buffer, p); + return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0))); + } + return null; +} diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index d5c4df0..94bd453 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -1,22 +1,19 @@ -import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; import * as SQLite from '@journeyapps/wa-sqlite'; +import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; import { PrepareOptions, ResetOptions, SqliteChanges, SqliteDriverConnection, - SqliteDriverConnectionPool, SqliteDriverStatement, + SqliteError, SqliteParameterBinding, SqliteRow, SqliteStepResult, StepOptions, UpdateListener } from '@sqlite-js/driver'; -import { LazyConnectionPool } from '@sqlite-js/driver/util'; -import { SqliteError } from '@sqlite-js/driver'; import * as mutex from 'async-mutex'; -import { WorkerDriverConnection } from './worker_threads'; // Initialize SQLite. export const module = await SQLiteESMFactory(); diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index fb95471..6a27fb8 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -1,13 +1,9 @@ +import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; import { sqlite3, module, WaSqliteConnection } from './wa-sqlite-driver'; import { setupDriverWorker } from './worker_threads'; -import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; -import { OPFSCoopSyncVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js'; // Register a custom file system. -// @ts-ignore -const vfs = await OPFSCoopSyncVFS.create('test.db', module, { - lockPolicy: 'exclusive' -}); +const vfs = await OPFSCoopSyncVFS2.create('test.db', module); // @ts-ignore sqlite3.vfs_register(vfs as any, true); diff --git a/packages/wa-sqlite-driver/tsconfig.json b/packages/wa-sqlite-driver/tsconfig.json index 57c042b..9d43e7e 100644 --- a/packages/wa-sqlite-driver/tsconfig.json +++ b/packages/wa-sqlite-driver/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["ES2022", "DOM"], + "lib": ["ES2024", "DOM"], "outDir": "lib", "rootDir": "src", "esModuleInterop": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50ac26f..dd6af96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,13 +28,13 @@ importers: version: 3.3.3 ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.5.4) + version: 10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.9.3) tsx: specifier: ^4.16.2 version: 4.17.0 typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^2.0.5 version: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) @@ -67,8 +67,8 @@ importers: specifier: ^20.14.2 version: 20.14.15 typescript: - specifier: ^5.4.5 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 packages/api: dependencies: @@ -86,8 +86,8 @@ importers: specifier: ^22.3.0 version: 22.3.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^2.0.5 version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) @@ -108,8 +108,8 @@ importers: specifier: ^22.3.0 version: 22.3.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^2.0.5 version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) @@ -120,8 +120,8 @@ importers: specifier: ^22.3.0 version: 22.3.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 packages/driver-tests: dependencies: @@ -139,8 +139,8 @@ importers: specifier: ^22.3.0 version: 22.3.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 packages/wa-sqlite-driver: dependencies: @@ -162,13 +162,13 @@ importers: version: 22.3.0 '@vitest/browser': specifier: ^2.0.5 - version: 2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13)) + version: 2.0.5(playwright@1.46.1)(typescript@5.9.3)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13)) playwright: specifier: ^1.45.3 version: 1.46.1 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^5.9.3 + version: 5.9.3 vite-plugin-top-level-await: specifier: ^1.4.2 version: 1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@22.3.0)) @@ -2523,8 +2523,8 @@ packages: resolution: {integrity: sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==} engines: {node: '>=16'} - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -3291,13 +3291,13 @@ snapshots: '@types/node': 22.3.0 optional: true - '@vitest/browser@2.0.5(playwright@1.46.1)(typescript@5.5.4)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13))': + '@vitest/browser@2.0.5(playwright@1.46.1)(typescript@5.9.3)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13))': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/utils': 2.0.5 magic-string: 0.30.11 - msw: 2.3.5(typescript@5.5.4) + msw: 2.3.5(typescript@5.9.3) sirv: 2.0.4 vitest: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) ws: 8.18.0 @@ -3309,13 +3309,13 @@ snapshots: - typescript - utf-8-validate - '@vitest/browser@2.0.5(typescript@5.5.4)(vitest@2.0.5)': + '@vitest/browser@2.0.5(typescript@5.9.3)(vitest@2.0.5)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/utils': 2.0.5 magic-string: 0.30.11 - msw: 2.3.5(typescript@5.5.4) + msw: 2.3.5(typescript@5.9.3) sirv: 2.0.4 vitest: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) ws: 8.18.0 @@ -4524,7 +4524,7 @@ snapshots: ms@2.1.3: {} - msw@2.3.5(typescript@5.5.4): + msw@2.3.5(typescript@5.9.3): dependencies: '@bundled-es-modules/cookie': 2.0.0 '@bundled-es-modules/statuses': 1.0.1 @@ -4544,7 +4544,7 @@ snapshots: type-fest: 4.25.0 yargs: 17.7.2 optionalDependencies: - typescript: 5.5.4 + typescript: 5.9.3 mute-stream@1.0.0: {} @@ -5139,7 +5139,7 @@ snapshots: tr46@0.0.3: {} - ts-node@10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.5.4): + ts-node@10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -5153,7 +5153,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.4 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: @@ -5180,7 +5180,7 @@ snapshots: type-fest@4.25.0: {} - typescript@5.5.4: {} + typescript@5.9.3: {} unbzip2-stream@1.4.3: dependencies: @@ -5311,7 +5311,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.14.15 - '@vitest/browser': 2.0.5(typescript@5.5.4)(vitest@2.0.5) + '@vitest/browser': 2.0.5(typescript@5.9.3)(vitest@2.0.5) transitivePeerDependencies: - less - lightningcss @@ -5345,7 +5345,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.3.0 - '@vitest/browser': 2.0.5(typescript@5.5.4)(vitest@2.0.5) + '@vitest/browser': 2.0.5(typescript@5.9.3)(vitest@2.0.5) transitivePeerDependencies: - less - lightningcss From 1f751cfe11f491703003c7c9096975c381e3078d Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Wed, 22 Oct 2025 17:53:29 -0600 Subject: [PATCH 10/25] Fixes. --- benchmarks/package.json | 2 +- package.json | 2 +- packages/api/package.json | 2 +- packages/better-sqlite3-driver/package.json | 2 +- packages/driver-tests/package.json | 2 +- packages/driver/package.json | 2 +- packages/wa-sqlite-driver/package.json | 2 +- .../wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 23 ++- .../src/worker_threads/worker-driver.ts | 2 - packages/wa-sqlite-driver/tsconfig.json | 3 +- pnpm-lock.yaml | 189 ++++++------------ 11 files changed, 81 insertions(+), 150 deletions(-) diff --git a/benchmarks/package.json b/benchmarks/package.json index 59d168b..1dc4045 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -15,7 +15,7 @@ "@sqlite-js/api": "workspace:^" }, "devDependencies": { - "@types/node": "^20.14.2", + "@types/node": "^24.9.1", "typescript": "^5.9.3" } } diff --git a/package.json b/package.json index bcd63e5..8ed78c7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.9", "@types/mocha": "^10.0.6", - "@types/node": "^20.14.2", + "@types/node": "^24.9.1", "expect": "^29.7.0", "mocha": "^10.4.0", "prettier": "^3.2.5", diff --git a/packages/api/package.json b/packages/api/package.json index 5547045..e9557c0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -18,7 +18,7 @@ "@sqlite-js/driver": "workspace:^" }, "devDependencies": { - "@types/node": "^22.3.0", + "@types/node": "^24.9.1", "typescript": "^5.9.3", "vitest": "^2.0.5", "@sqlite-js/better-sqlite3-driver": "workspace:^", diff --git a/packages/better-sqlite3-driver/package.json b/packages/better-sqlite3-driver/package.json index 981d3c5..db4d9be 100644 --- a/packages/better-sqlite3-driver/package.json +++ b/packages/better-sqlite3-driver/package.json @@ -19,7 +19,7 @@ "@sqlite-js/driver": "workspace:^" }, "devDependencies": { - "@types/node": "^22.3.0", + "@types/node": "^24.9.1", "typescript": "^5.9.3", "vitest": "^2.0.5", "@sqlite-js/driver-tests": "workspace:^" diff --git a/packages/driver-tests/package.json b/packages/driver-tests/package.json index 57fd965..5538af3 100644 --- a/packages/driver-tests/package.json +++ b/packages/driver-tests/package.json @@ -22,7 +22,7 @@ "@sqlite-js/driver": "workspace:^" }, "devDependencies": { - "@types/node": "^22.3.0", + "@types/node": "^24.9.1", "typescript": "^5.9.3" } } diff --git a/packages/driver/package.json b/packages/driver/package.json index 7761c2b..fd78c52 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -20,7 +20,7 @@ "license": "MIT", "dependencies": {}, "devDependencies": { - "@types/node": "^22.3.0", + "@types/node": "^24.9.1", "typescript": "^5.9.3" } } diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index 3adb50f..a50fc67 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -23,7 +23,7 @@ "vite-plugin-top-level-await": "^1.4.2", "vite-plugin-wasm": "^3.3.0", "@sqlite-js/driver-tests": "workspace:^", - "@types/node": "^22.3.0", + "@types/node": "^24.9.1", "typescript": "^5.9.3", "@vitest/browser": "^2.0.5", "vitest": "^2.0.5", diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index 398e57b..fc668d8 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -52,11 +52,18 @@ class PersistentFile { } } +interface WithModule { + _module: { retryOps: Promise[] }; +} + export class OPFSCoopSyncVFS2 extends FacadeVFS { mapIdToFile = new Map(); lastError = null; - log = null; //function(...args) { console.log(`[${contextName}]`, ...args) }; + // log = null; //function(...args) { console.log(`[${contextName}]`, ...args) }; + log = function (...args) { + console.log(`[OPFSCoopSyncVFS2]`, ...args); + }; persistentFiles = new Map(); boundAccessHandles = new Map(); @@ -64,8 +71,6 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { accessiblePaths = new Set(); releaser: null | (() => void) = null; - _module: { retryOps: Promise[] }; - static async create(name, module) { const vfs = new OPFSCoopSyncVFS2(name, module); await Promise.all([ @@ -79,6 +84,10 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { super(name, module); } + get #module() { + return (this as unknown as WithModule)._module; + } + async #initialize(nTemporaryFiles) { // Delete temporary directories no longer in use. const root = await navigator.storage.getDirectory(); @@ -148,7 +157,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { // files are ready to be used. this.log?.(`creating persistent file for ${path}`); const create = !!(flags & VFS.SQLITE_OPEN_CREATE); - this._module.retryOps.push( + this.#module.retryOps.push( (async () => { try { // Get the path directory handle. @@ -192,7 +201,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { } else if (!persistentFile.accessHandle) { // This branch is reached if the database was previously opened // and closed. - this._module.retryOps.push( + this.#module.retryOps.push( (async () => { const file = new File(path, flags); file.persistentFile = this.persistentFiles.get(path); @@ -499,7 +508,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { console.assert(!file.persistentFile.handleLockReleaser); if (!file.persistentFile.isRequestInProgress) { file.persistentFile.isRequestInProgress = true; - this._module.retryOps.push( + this.#module.retryOps.push( (async () => { // Acquire the Web Lock. file.persistentFile.handleLockReleaser = await this.#acquireLock( @@ -523,7 +532,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { file.persistentFile.isRequestInProgress = false; })() ); - return this._module.retryOps.at(-1); + return this.#module.retryOps.at(-1); } return Promise.resolve(); } diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index ad1f6df..b549620 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -51,7 +51,6 @@ export class WorkerDriverConnection implements SqliteDriverConnection { this.ready = new Promise((resolve) => { worker.addEventListener('message', (event) => { const { id, value } = event.data; - console.log('gotta message', id, value); if (id == 0) { resolve(); return; @@ -121,7 +120,6 @@ export class WorkerDriverConnection implements SqliteDriverConnection { const p = new Promise((resolve) => { id = this.registerCallback(resolve); }); - console.log('posting', command, id, args); this.worker.postMessage([command, id!, args]); const result = await p; const error = (result as any)?.error; diff --git a/packages/wa-sqlite-driver/tsconfig.json b/packages/wa-sqlite-driver/tsconfig.json index 9d43e7e..84ee388 100644 --- a/packages/wa-sqlite-driver/tsconfig.json +++ b/packages/wa-sqlite-driver/tsconfig.json @@ -8,7 +8,8 @@ "module": "ESNext", "moduleResolution": "Bundler", // FIXME: issues with @journeyapps/wa-sqlite definitions - "strict": false + "strict": false, + "skipLibCheck": true }, "include": ["src"], "references": [{ "path": "../driver" }, { "path": "../driver-tests" }] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd6af96..830f7e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^10.0.6 version: 10.0.7 '@types/node': - specifier: ^20.14.2 - version: 20.14.15 + specifier: ^24.9.1 + version: 24.9.1 expect: specifier: ^29.7.0 version: 29.7.0 @@ -28,7 +28,7 @@ importers: version: 3.3.3 ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.7.18)(@types/node@24.9.1)(typescript@5.9.3) tsx: specifier: ^4.16.2 version: 4.17.0 @@ -37,7 +37,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) + version: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) benchmarks: dependencies: @@ -64,8 +64,8 @@ importers: version: 5.1.7 devDependencies: '@types/node': - specifier: ^20.14.2 - version: 20.14.15 + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -83,14 +83,14 @@ importers: specifier: workspace:^ version: link:../driver-tests '@types/node': - specifier: ^22.3.0 - version: 22.3.0 + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + version: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) packages/better-sqlite3-driver: dependencies: @@ -105,20 +105,20 @@ importers: specifier: workspace:^ version: link:../driver-tests '@types/node': - specifier: ^22.3.0 - version: 22.3.0 + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + version: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) packages/driver: devDependencies: '@types/node': - specifier: ^22.3.0 - version: 22.3.0 + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -133,11 +133,11 @@ importers: version: 10.7.3 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + version: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) devDependencies: '@types/node': - specifier: ^22.3.0 - version: 22.3.0 + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -158,8 +158,8 @@ importers: specifier: workspace:^ version: link:../driver-tests '@types/node': - specifier: ^22.3.0 - version: 22.3.0 + specifier: ^24.9.1 + version: 24.9.1 '@vitest/browser': specifier: ^2.0.5 version: 2.0.5(playwright@1.46.1)(typescript@5.9.3)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13)) @@ -171,13 +171,13 @@ importers: version: 5.9.3 vite-plugin-top-level-await: specifier: ^1.4.2 - version: 1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@22.3.0)) + version: 1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@24.9.1)) vite-plugin-wasm: specifier: ^3.3.0 - version: 3.3.0(vite@5.4.1(@types/node@22.3.0)) + version: 3.3.0(vite@5.4.1(@types/node@24.9.1)) vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + version: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) webdriverio: specifier: ^8.39.1 version: 8.40.3(encoding@0.1.13) @@ -828,12 +828,12 @@ packages: '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/node@20.14.15': - resolution: {integrity: sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==} - '@types/node@22.3.0': resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} + '@types/node@24.9.1': + resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -2531,12 +2531,12 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.18.2: resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unique-filename@1.1.1: resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} @@ -3011,7 +3011,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.3.0 + '@types/node': 24.9.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -3234,7 +3234,7 @@ snapshots: '@types/better-sqlite3@7.6.11': dependencies: - '@types/node': 20.14.15 + '@types/node': 24.9.1 '@types/cookie@0.6.0': {} @@ -3256,16 +3256,16 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 22.3.0 - - '@types/node@20.14.15': - dependencies: - undici-types: 5.26.5 + '@types/node': 24.9.1 '@types/node@22.3.0': dependencies: undici-types: 6.18.2 + '@types/node@24.9.1': + dependencies: + undici-types: 7.16.0 + '@types/stack-utils@2.0.3': {} '@types/statuses@2.0.5': {} @@ -3278,7 +3278,7 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 22.3.0 + '@types/node': 24.9.1 '@types/yargs-parser@21.0.3': {} @@ -3288,7 +3288,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.3.0 + '@types/node': 24.9.1 optional: true '@vitest/browser@2.0.5(playwright@1.46.1)(typescript@5.9.3)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13))': @@ -3299,7 +3299,7 @@ snapshots: magic-string: 0.30.11 msw: 2.3.5(typescript@5.9.3) sirv: 2.0.4 - vitest: 2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5) + vitest: 2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5) ws: 8.18.0 optionalDependencies: playwright: 1.46.1 @@ -3309,22 +3309,6 @@ snapshots: - typescript - utf-8-validate - '@vitest/browser@2.0.5(typescript@5.9.3)(vitest@2.0.5)': - dependencies: - '@testing-library/dom': 10.4.0 - '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) - '@vitest/utils': 2.0.5 - magic-string: 0.30.11 - msw: 2.3.5(typescript@5.9.3) - sirv: 2.0.4 - vitest: 2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5) - ws: 8.18.0 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - optional: true - '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5 @@ -4305,7 +4289,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.3.0 + '@types/node': 24.9.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -5139,14 +5123,14 @@ snapshots: tr46@0.0.3: {} - ts-node@10.9.2(@swc/core@1.7.18)(@types/node@20.14.15)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.7.18)(@types/node@24.9.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.14.15 + '@types/node': 24.9.1 acorn: 8.12.1 acorn-walk: 8.3.3 arg: 4.1.3 @@ -5187,10 +5171,10 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@5.26.5: {} - undici-types@6.18.2: {} + undici-types@7.16.0: {} + unique-filename@1.1.1: dependencies: unique-slug: 2.0.2 @@ -5220,13 +5204,13 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - vite-node@2.0.5(@types/node@20.14.15): + vite-node@2.0.5(@types/node@24.9.1): dependencies: cac: 6.7.14 debug: 4.3.6(supports-color@8.1.1) pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@20.14.15) + vite: 5.4.1(@types/node@24.9.1) transitivePeerDependencies: - '@types/node' - less @@ -5238,57 +5222,30 @@ snapshots: - supports-color - terser - vite-node@2.0.5(@types/node@22.3.0): - dependencies: - cac: 6.7.14 - debug: 4.3.6(supports-color@8.1.1) - pathe: 1.1.2 - tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@22.3.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite-plugin-top-level-await@1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@22.3.0)): + vite-plugin-top-level-await@1.4.4(rollup@4.20.0)(vite@5.4.1(@types/node@24.9.1)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.20.0) '@swc/core': 1.7.18 uuid: 10.0.0 - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@24.9.1) transitivePeerDependencies: - '@swc/helpers' - rollup - vite-plugin-wasm@3.3.0(vite@5.4.1(@types/node@22.3.0)): + vite-plugin-wasm@3.3.0(vite@5.4.1(@types/node@24.9.1)): dependencies: - vite: 5.4.1(@types/node@22.3.0) + vite: 5.4.1(@types/node@24.9.1) - vite@5.4.1(@types/node@20.14.15): + vite@5.4.1(@types/node@24.9.1): dependencies: esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.20.0 optionalDependencies: - '@types/node': 20.14.15 - fsevents: 2.3.3 - - vite@5.4.1(@types/node@22.3.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.41 - rollup: 4.20.0 - optionalDependencies: - '@types/node': 22.3.0 + '@types/node': 24.9.1 fsevents: 2.3.3 - vitest@2.0.5(@types/node@20.14.15)(@vitest/browser@2.0.5): + vitest@2.0.5(@types/node@24.9.1)(@vitest/browser@2.0.5): dependencies: '@ampproject/remapping': 2.3.0 '@vitest/expect': 2.0.5 @@ -5306,46 +5263,12 @@ snapshots: tinybench: 2.9.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@20.14.15) - vite-node: 2.0.5(@types/node@20.14.15) + vite: 5.4.1(@types/node@24.9.1) + vite-node: 2.0.5(@types/node@24.9.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.14.15 - '@vitest/browser': 2.0.5(typescript@5.9.3)(vitest@2.0.5) - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vitest@2.0.5(@types/node@22.3.0)(@vitest/browser@2.0.5): - dependencies: - '@ampproject/remapping': 2.3.0 - '@vitest/expect': 2.0.5 - '@vitest/pretty-format': 2.0.5 - '@vitest/runner': 2.0.5 - '@vitest/snapshot': 2.0.5 - '@vitest/spy': 2.0.5 - '@vitest/utils': 2.0.5 - chai: 5.1.1 - debug: 4.3.6(supports-color@8.1.1) - execa: 8.0.1 - magic-string: 0.30.11 - pathe: 1.1.2 - std-env: 3.7.0 - tinybench: 2.9.0 - tinypool: 1.0.0 - tinyrainbow: 1.2.0 - vite: 5.4.1(@types/node@22.3.0) - vite-node: 2.0.5(@types/node@22.3.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 22.3.0 - '@vitest/browser': 2.0.5(typescript@5.9.3)(vitest@2.0.5) + '@types/node': 24.9.1 + '@vitest/browser': 2.0.5(playwright@1.46.1)(typescript@5.9.3)(vitest@2.0.5)(webdriverio@8.40.3(encoding@0.1.13)) transitivePeerDependencies: - less - lightningcss From 87d2ce2fd0bc9383e9e18b79a4975df3baf8ac09 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Wed, 22 Oct 2025 18:51:26 -0600 Subject: [PATCH 11/25] POC of prelock. --- packages/driver/src/driver-api.ts | 3 + .../driver/src/util/SingleConnectionPool.ts | 14 +++- .../wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 84 +++++++++++-------- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 29 ++++++- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 2 +- .../src/worker_threads/WorkerDriverAdapter.ts | 4 + .../src/worker_threads/async-commands.ts | 17 +++- .../src/worker_threads/setup.ts | 7 ++ .../src/worker_threads/worker-driver.ts | 9 ++ 9 files changed, 126 insertions(+), 43 deletions(-) diff --git a/packages/driver/src/driver-api.ts b/packages/driver/src/driver-api.ts index 15e2266..cfa5155 100644 --- a/packages/driver/src/driver-api.ts +++ b/packages/driver/src/driver-api.ts @@ -35,6 +35,9 @@ export interface SqliteDriverConnection { getLastChanges(): Promise; close(): Promise; + + lock?(mode: 'exclusive' | 'shared' | 'deferred'): Promise; + release?(): void; } export type SqliteParameterBinding = diff --git a/packages/driver/src/util/SingleConnectionPool.ts b/packages/driver/src/util/SingleConnectionPool.ts index 5eb78cc..8598888 100644 --- a/packages/driver/src/util/SingleConnectionPool.ts +++ b/packages/driver/src/util/SingleConnectionPool.ts @@ -26,9 +26,10 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { await this.connection.close(); } - reserveConnection( + async reserveConnection( options?: ReserveConnectionOptions ): Promise { + console.log('single reserveConnection', this.connection.lock); if (options?.signal?.aborted) { throw new Error('Aborted'); } @@ -37,6 +38,7 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { async () => { // TODO: sync if (this.inUse === reserved) { + reserved.connection.release?.(); this.inUse = null; Promise.resolve().then(() => this.next()); } @@ -45,7 +47,10 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { if (this.inUse == null) { this.inUse = reserved; - return Promise.resolve(reserved); + await reserved.connection.lock?.( + options?.readonly ? 'shared' : 'exclusive' + ); + return reserved; } else { const promise = new Promise((resolve, reject) => { const item: QueuedItem = { @@ -64,8 +69,11 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { ); }); - return promise.then((r) => { + return promise.then(async (r) => { this.inUse = reserved; + await reserved.connection.lock?.( + options?.readonly ? 'shared' : 'exclusive' + ); return r; }); } diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index fc668d8..2c4234a 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -34,6 +34,7 @@ interface FileSystemSyncAccessHandle extends FileSystemHandle { } class PersistentFile { + path: string; fileHandle: FileSystemFileHandle; accessHandle: null | FileSystemSyncAccessHandle = null; @@ -47,7 +48,8 @@ class PersistentFile { handleRequestChannel: BroadcastChannel; isHandleRequested = false; - constructor(fileHandle) { + constructor(path: string, fileHandle: FileSystemFileHandle) { + this.path = path; this.fileHandle = fileHandle; } } @@ -88,6 +90,20 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return (this as unknown as WithModule)._module; } + async prelock(fileName: string): Promise { + const file = this.persistentFiles.get('/' + fileName); + this.log?.('prelock', fileName, file); + await this.#requestAccessHandle(file); + this.log?.('prelocked', fileName); + const self = this; + return { + [Symbol.dispose]() { + this.log?.('prelock release', fileName); + self.#releaseAccessHandle(file); + } + }; + } + async #initialize(nTemporaryFiles) { // Delete temporary directories no longer in use. const root = await navigator.storage.getDirectory(); @@ -183,11 +199,11 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { // Get access handles for the files. const file = new File(path, flags); file.persistentFile = this.persistentFiles.get(path); - await this.#requestAccessHandle(file); + await this.#requestAccessHandle(file.persistentFile); } catch (e) { // Use an invalid persistent file to signal this error // for the retried open. - const persistentFile = new PersistentFile(null); + const persistentFile = new PersistentFile(path, null); this.persistentFiles.set(path, persistentFile); console.error(e); } @@ -205,7 +221,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { (async () => { const file = new File(path, flags); file.persistentFile = this.persistentFiles.get(path); - await this.#requestAccessHandle(file); + await this.#requestAccessHandle(file.persistentFile); })() ); return VFS.SQLITE_BUSY; @@ -282,7 +298,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { if (file.persistentFile?.handleLockReleaser) { - this.#releaseAccessHandle(file); + this.#releaseAccessHandle(file.persistentFile); } } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { file.accessHandle.truncate(0); @@ -315,7 +331,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.persistentFile.isFileLocked ) { - this.#releaseAccessHandle(file); + this.#releaseAccessHandle(file.persistentFile); } if (bytesRead < pData.byteLength) { @@ -408,12 +424,12 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { file.persistentFile.isHandleRequested = true; } else { // Release the access handles immediately. - this.#releaseAccessHandle(file); + this.#releaseAccessHandle(file.persistentFile); } file.persistentFile.handleRequestChannel.onmessage = null; }; - this.#requestAccessHandle(file); + this.#requestAccessHandle(file.persistentFile); this.log?.('returning SQLITE_BUSY'); file.persistentFile.isLockBusy = true; return VFS.SQLITE_BUSY; @@ -430,7 +446,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { if (!file.persistentFile.isLockBusy) { if (file.persistentFile.isHandleRequested) { // Another connection wants the access handle. - this.#releaseAccessHandle(file); + this.#releaseAccessHandle(file.persistentFile); file.persistentFile.isHandleRequested = false; } file.persistentFile.isFileLocked = false; @@ -490,10 +506,10 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { async #createPersistentFile( fileHandle: FileSystemFileHandle ): Promise { - const persistentFile = new PersistentFile(fileHandle); const root = await navigator.storage.getDirectory(); const relativePath = await root.resolve(fileHandle); const path = `/${relativePath.join('/')}`; + const persistentFile = new PersistentFile(path, fileHandle); persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`); this.persistentFiles.set(path, persistentFile); @@ -504,32 +520,32 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return persistentFile; } - #requestAccessHandle(file: File): Promise { - console.assert(!file.persistentFile.handleLockReleaser); - if (!file.persistentFile.isRequestInProgress) { - file.persistentFile.isRequestInProgress = true; + #requestAccessHandle(persistentFile: PersistentFile): Promise { + console.assert(!persistentFile.handleLockReleaser); + if (!persistentFile.isRequestInProgress) { + persistentFile.isRequestInProgress = true; + this.log?.('Requesting lock for', persistentFile.path); this.#module.retryOps.push( (async () => { // Acquire the Web Lock. - file.persistentFile.handleLockReleaser = await this.#acquireLock( - file.persistentFile - ); + persistentFile.handleLockReleaser = + await this.#acquireLock(persistentFile); // Get access handles for the database and releated files in parallel. - this.log?.(`creating access handles for ${file.path}`); + this.log?.(`creating access handles for ${persistentFile.path}`); await Promise.all( DB_RELATED_FILE_SUFFIXES.map(async (suffix) => { - const persistentFile = this.persistentFiles.get( - file.path + suffix + const subPersistentFile = this.persistentFiles.get( + persistentFile.path + suffix ); - if (persistentFile) { - persistentFile.accessHandle = await ( - persistentFile.fileHandle as any + if (subPersistentFile) { + subPersistentFile.accessHandle = await ( + subPersistentFile.fileHandle as any ).createSyncAccessHandle(); } }) ); - file.persistentFile.isRequestInProgress = false; + persistentFile.isRequestInProgress = false; })() ); return this.#module.retryOps.at(-1); @@ -537,19 +553,21 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return Promise.resolve(); } - async #releaseAccessHandle(file: File): Promise { + async #releaseAccessHandle(persistentFile: PersistentFile): Promise { DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => { - const persistentFile = this.persistentFiles.get(file.path + suffix); - if (persistentFile) { - persistentFile.accessHandle?.close(); - persistentFile.accessHandle = null; + const subPersistentFile = this.persistentFiles.get( + persistentFile.path + suffix + ); + if (subPersistentFile) { + subPersistentFile.accessHandle?.close(); + subPersistentFile.accessHandle = null; } }); - this.log?.(`access handles closed for ${file.path}`); + this.log?.(`access handles closed for ${persistentFile.path}`); - file.persistentFile.handleLockReleaser?.(); - file.persistentFile.handleLockReleaser = null; - this.log?.(`lock released for ${file.path}`); + persistentFile.handleLockReleaser?.(); + persistentFile.handleLockReleaser = null; + this.log?.(`lock released for ${persistentFile.path}`); } #acquireLock(persistentFile: PersistentFile): Promise<() => void> { diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index 94bd453..b57cefc 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -14,6 +14,7 @@ import { UpdateListener } from '@sqlite-js/driver'; import * as mutex from 'async-mutex'; +import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; // Initialize SQLite. export const module = await SQLiteESMFactory(); @@ -31,6 +32,7 @@ class StatementImpl implements SqliteDriverStatement { constructor( private db: number, + private con: WaSqliteConnection, public source: string, public options: PrepareOptions ) { @@ -251,17 +253,36 @@ class StatementImpl implements SqliteDriverStatement { export class WaSqliteConnection implements SqliteDriverConnection { db: number; + vfs: OPFSCoopSyncVFS2; statements = new Set(); + lockDisposer: Disposable | null = null; - static async open(filename: string): Promise { + static async open( + filename: string, + vfs: OPFSCoopSyncVFS2 + ): Promise { // Open the database. const db = await sqlite3.open_v2(filename); - return new WaSqliteConnection(db); + return new WaSqliteConnection(db, vfs, filename); } - constructor(db: number) { + constructor( + db: number, + vfs: OPFSCoopSyncVFS2, + public path: string + ) { this.db = db; + this.vfs = vfs; + } + + async lock() { + this.lockDisposer = await this.vfs.prelock(this.path); + } + + release() { + this.lockDisposer[Symbol.dispose](); + this.lockDisposer = null; } async close() { @@ -285,7 +306,7 @@ export class WaSqliteConnection implements SqliteDriverConnection { } prepare(sql: string, options?: PrepareOptions): StatementImpl { - const st = new StatementImpl(this.db, sql, options ?? {}); + const st = new StatementImpl(this.db, this, sql, options ?? {}); // TODO: cleanup on finalize this.statements.add(st); return st; diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index 6a27fb8..80b400d 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -9,6 +9,6 @@ sqlite3.vfs_register(vfs as any, true); setupDriverWorker({ async openConnection(options) { - return await WaSqliteConnection.open(options.path); + return await WaSqliteConnection.open(options.path, vfs); } }); diff --git a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts index 09eedf5..48d30cb 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts @@ -112,6 +112,10 @@ export class WorkerConnectionAdapter implements WorkerDriver { return this._parse(command); case SqliteCommandType.changes: return this.connnection.getLastChanges(); + case SqliteCommandType.lock: + return this.connnection.lock?.(command.mode); + case SqliteCommandType.release: + return this.connnection.release?.(); default: throw new Error(`Unknown command: ${command.type}`); } diff --git a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts index d33641c..4ba4bfe 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts @@ -14,7 +14,9 @@ export enum SqliteCommandType { sync = 6, parse = 7, run = 8, - changes = 9 + changes = 9, + lock = 10, + release = 11 } export type SqliteDriverError = SerializedDriverError; @@ -89,6 +91,15 @@ export interface SqliteGetChanges { type: SqliteCommandType.changes; } +export interface SqliteLock { + type: SqliteCommandType.lock; + mode: 'exclusive' | 'shared' | 'deferred'; +} + +export interface SqliteRelease { + type: SqliteCommandType.release; +} + export type SqliteCommand = | SqlitePrepare | SqliteBind @@ -98,7 +109,9 @@ export type SqliteCommand = | SqliteFinalize | SqliteSync | SqliteParse - | SqliteGetChanges; + | SqliteGetChanges + | SqliteLock + | SqliteRelease; export type InferCommandResult = T extends SqliteRun ? SqliteChanges diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts index d432a40..773a182 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/setup.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -54,6 +54,13 @@ export function setupDriverPort(config: WorkerDriverConfig) { } catch (e: any) { port.postMessage({ id, value: { error: { message: e.message } } }); } + } else if (message == 'lock') { + try { + await opened; + port.postMessage({ id }); + } catch (e: any) { + port.postMessage({ id, value: { error: { message: e.message } } }); + } } else if (message == 'execute') { try { await opened; diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index b549620..f9d00d8 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -70,6 +70,14 @@ export class WorkerDriverConnection implements SqliteDriverConnection { return this.post('open', this.options); } + async lock(mode: 'exclusive' | 'shared' | 'deferred'): Promise { + await this._send({ type: SqliteCommandType.lock, mode: mode }); + } + + release(): void { + this._send({ type: SqliteCommandType.release }); + } + prepare(sql: string, options?: PrepareOptions): WorkerDriverStatement { const id = this.nextId++; this.buffer.push({ @@ -106,6 +114,7 @@ export class WorkerDriverConnection implements SqliteDriverConnection { _send(cmd: SqliteCommand): void { this.buffer.push({ cmd }); + this._maybeFlush(); } private registerCallback(callback: (value: any) => void) { From 53248e8a6ccaa2e7f7c8ead89ff416c5e27df260 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 15:29:14 -0600 Subject: [PATCH 12/25] Connection pool with workers; initial concurrency test. --- packages/driver-tests/src/index.ts | 1 + packages/wa-sqlite-driver/src/pool.ts | 32 +++++++++- .../test/src/concurrency.test.ts | 63 +++++++++++++++++++ .../wa-sqlite-driver/test/src/driver.test.ts | 4 +- 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 packages/wa-sqlite-driver/test/src/concurrency.test.ts diff --git a/packages/driver-tests/src/index.ts b/packages/driver-tests/src/index.ts index 47a51c7..e35dd66 100644 --- a/packages/driver-tests/src/index.ts +++ b/packages/driver-tests/src/index.ts @@ -1 +1,2 @@ export * from './driver-tests.js'; +export * from './test.js'; diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index 70c38d0..a8e5ce3 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -1,5 +1,13 @@ import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; -import { LazyConnectionPool } from '@sqlite-js/driver/util'; +import { + LazyConnectionPool, + MultiConnectionPool +} from '@sqlite-js/driver/util'; +import { + ReserveConnectionOptions, + SqliteDriverConnection +} from '@sqlite-js/driver'; + import { WorkerDriverConnection } from './worker_threads'; // import { WaSqliteConnection } from './wa-sqlite-driver'; @@ -9,7 +17,7 @@ import { WorkerDriverConnection } from './worker_threads'; // }); // } -export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { +export function waSqliteSingleWorker(path: string): SqliteDriverConnectionPool { return new LazyConnectionPool(async () => { const connection = new WorkerDriverConnection( new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { @@ -22,3 +30,23 @@ export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { // return await WaSqliteConnection.open(path); }); } + +export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { + return new MultiConnectionPool( + { + async openConnection( + options?: ReserveConnectionOptions & { connectionName?: string } + ): Promise { + const connection = new WorkerDriverConnection( + new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { + type: 'module' + }), + { path } + ); + await connection.open(); + return connection; + } + }, + {} + ); +} diff --git a/packages/wa-sqlite-driver/test/src/concurrency.test.ts b/packages/wa-sqlite-driver/test/src/concurrency.test.ts new file mode 100644 index 0000000..f8c128d --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/concurrency.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, test } from '@sqlite-js/driver-tests'; +import { waSqliteSingleWorker, waSqliteWorkerPool } from '../../lib/index.js'; + +describe('concurrency tests', () => { + let dbPath: string; + + const open = async () => { + const db = await waSqliteWorkerPool(dbPath); + return db; + }; + + beforeEach((context) => { + let testNameSanitized = context.fullName.replaceAll( + /[\s\/\\>\.\-\:]+/g, + '_' + ); + + if (testNameSanitized.length > 10) { + testNameSanitized = + testNameSanitized.substring(testNameSanitized.length - 7) + + String(Math.random()).substring(1, 4); + } + dbPath = `test-db/${testNameSanitized}.db`; + }); + + test('concurrent select', async () => { + await using driver = await open(); + { + await using connection = await driver.reserveConnection(); + using s1 = connection.prepare( + 'create table test_data(id integer primary key, data text)' + ); + await s1.step(); + using s2 = connection.prepare( + "insert into test_data(data) values('test')" + ); + await s2.step(); + } + + let promises: Promise[] = []; + + for (let i = 0; i < 5; i++) { + const p = (async () => { + const start = Date.now(); + await using connection = await driver.reserveConnection(); + + using b = connection.prepare('begin immediate'); + await b.step(); + using s = connection.prepare('select * from test_data'); + const { rows } = await s.step(); + + expect(rows).toEqual([{ id: 1, data: 'test' }]); + await new Promise((resolve) => setTimeout(resolve, 500)); + + using e = connection.prepare('commit'); + await e.step(); + console.log('tx done in', Date.now() - start); + })(); + promises.push(p); + } + await Promise.all(promises); + }); +}); diff --git a/packages/wa-sqlite-driver/test/src/driver.test.ts b/packages/wa-sqlite-driver/test/src/driver.test.ts index 7030eb4..1134801 100644 --- a/packages/wa-sqlite-driver/test/src/driver.test.ts +++ b/packages/wa-sqlite-driver/test/src/driver.test.ts @@ -1,11 +1,11 @@ import { describeDriverTests } from '@sqlite-js/driver-tests'; -import { waSqliteWorkerPool } from '../../lib/index.js'; +import { waSqliteSingleWorker } from '../../lib/index.js'; describeDriverTests( 'wa-sqlite', { getColumns: true, rawResults: true, allowsMissingParameters: false }, async (path) => { console.log('open', path); - return waSqliteWorkerPool(path); + return waSqliteSingleWorker(path); } ); From 4223302c80156187099139dc2627019250eb2775 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 15:34:30 -0600 Subject: [PATCH 13/25] Unsafe concurrency. --- packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index 2c4234a..31813c2 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -142,7 +142,9 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { // Populate temporary directory. for (let i = 0; i < nTemporaryFiles; i++) { const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true }); - const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle(); + const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle({ + mode: 'readwrite-unsafe' + }); this.unboundAccessHandles.add(tmpAccessHandle); } } @@ -541,7 +543,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { if (subPersistentFile) { subPersistentFile.accessHandle = await ( subPersistentFile.fileHandle as any - ).createSyncAccessHandle(); + ).createSyncAccessHandle({ mode: 'readwrite-unsafe' }); } }) ); @@ -582,7 +584,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { setTimeout(notify); this.log?.(`lock requested: ${lockName}`); - navigator.locks.request(lockName, (lock) => { + navigator.locks.request(lockName, { mode: 'shared' }, (lock) => { // We have the lock. Stop asking other connections for it. this.log?.(`lock acquired: ${lockName}`, lock); clearInterval(notifyId); From b82f30a81be23b251506182a052bf93d38989a00 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 16:19:49 -0600 Subject: [PATCH 14/25] [WIP] readwrite pool. --- .../driver/src/util/MultiConnectionPool.ts | 8 +++-- packages/driver/src/util/connection-pools.ts | 1 + .../wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 34 ++++++++++++------- packages/wa-sqlite-driver/src/pool.ts | 14 +++++--- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 4 ++- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 19 ++++++++--- .../src/worker_threads/WorkerDriverAdapter.ts | 13 ++++++- .../test/src/concurrency.test.ts | 26 +++++++++++++- 8 files changed, 91 insertions(+), 28 deletions(-) diff --git a/packages/driver/src/util/MultiConnectionPool.ts b/packages/driver/src/util/MultiConnectionPool.ts index c67c738..9311163 100644 --- a/packages/driver/src/util/MultiConnectionPool.ts +++ b/packages/driver/src/util/MultiConnectionPool.ts @@ -20,6 +20,7 @@ export class MultiConnectionPool implements SqliteDriverConnectionPool { private _availableReadConnections: SqliteDriverConnection[] = []; private _queue: QueuedPoolItem[] = []; private _maxConnections: number; + private _nextConnectionNumber = 1; private options: ConnectionPoolOptions; @@ -42,7 +43,8 @@ export class MultiConnectionPool implements SqliteDriverConnectionPool { const promise = new Promise((resolve, reject) => { this._queue.push({ resolve, - reject + reject, + options: options ?? {} }); }); @@ -57,7 +59,7 @@ export class MultiConnectionPool implements SqliteDriverConnectionPool { const connection = await this.factory.openConnection({ ...this.options, ...options, - connectionName: `connection-${this._allConnections.size + 1}` + connectionName: `connection-${this._nextConnectionNumber++}` }); this._allConnections.add(connection); return connection; @@ -82,7 +84,7 @@ export class MultiConnectionPool implements SqliteDriverConnectionPool { let connection: SqliteDriverConnection; if (this._availableReadConnections.length == 0) { // FIXME: prevent opening more than the max - connection = await this.expandPool(); + connection = await this.expandPool(item.options); } else { connection = this._availableReadConnections.shift()!; } diff --git a/packages/driver/src/util/connection-pools.ts b/packages/driver/src/util/connection-pools.ts index c98d642..1db698c 100644 --- a/packages/driver/src/util/connection-pools.ts +++ b/packages/driver/src/util/connection-pools.ts @@ -22,6 +22,7 @@ export interface DriverFactory { export interface QueuedPoolItem { resolve: (reserved: ReservedConnection) => void; reject: (err: any) => void; + options: ReserveConnectionOptions; } export class ReservedConnectionImpl implements ReservedConnection { diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index 31813c2..c1c7241 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -72,9 +72,10 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { unboundAccessHandles = new Set(); accessiblePaths = new Set(); releaser: null | (() => void) = null; + private readonly: boolean; - static async create(name, module) { - const vfs = new OPFSCoopSyncVFS2(name, module); + static async create(name: string, module, readonly: boolean) { + const vfs = new OPFSCoopSyncVFS2(name, module, readonly); await Promise.all([ (vfs as any).isReady(), vfs.#initialize(DEFAULT_TEMPORARY_FILES) @@ -82,8 +83,9 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return vfs; } - constructor(name, module) { + constructor(name: string, module, readonly: boolean) { super(name, module); + this.readonly = readonly; } get #module() { @@ -143,7 +145,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { for (let i = 0; i < nTemporaryFiles; i++) { const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true }); const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle({ - mode: 'readwrite-unsafe' + mode: this.readonly ? 'read-only' : 'readwrite' }); this.unboundAccessHandles.add(tmpAccessHandle); } @@ -543,7 +545,9 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { if (subPersistentFile) { subPersistentFile.accessHandle = await ( subPersistentFile.fileHandle as any - ).createSyncAccessHandle({ mode: 'readwrite-unsafe' }); + ).createSyncAccessHandle({ + mode: this.readonly ? 'read-only' : 'readwrite' + }); } }) ); @@ -584,14 +588,18 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { setTimeout(notify); this.log?.(`lock requested: ${lockName}`); - navigator.locks.request(lockName, { mode: 'shared' }, (lock) => { - // We have the lock. Stop asking other connections for it. - this.log?.(`lock acquired: ${lockName}`, lock); - clearInterval(notifyId); - return new Promise<() => void>((res) => { - resolve(res as () => void); - }); - }); + navigator.locks.request( + lockName, + { mode: this.readonly ? 'shared' : 'exclusive' }, + (lock) => { + // We have the lock. Stop asking other connections for it. + this.log?.(`lock acquired: ${lockName}`, lock); + clearInterval(notifyId); + return new Promise<() => void>((res) => { + resolve(res as () => void); + }); + } + ); }); } } diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index a8e5ce3..fdf66d2 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -1,7 +1,8 @@ import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; import { LazyConnectionPool, - MultiConnectionPool + MultiConnectionPool, + ReadWriteConnectionPool } from '@sqlite-js/driver/util'; import { ReserveConnectionOptions, @@ -32,21 +33,26 @@ export function waSqliteSingleWorker(path: string): SqliteDriverConnectionPool { } export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { - return new MultiConnectionPool( + return new ReadWriteConnectionPool( { async openConnection( options?: ReserveConnectionOptions & { connectionName?: string } ): Promise { + console.log('openConnection', options); const connection = new WorkerDriverConnection( new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { type: 'module' }), - { path } + { + path, + readonly: options?.readonly ?? false, + connectionName: options?.connectionName + } ); await connection.open(); return connection; } }, - {} + { maxConnections: 5 } ); } diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index b57cefc..a3f5409 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -190,6 +190,7 @@ class StatementImpl implements SqliteDriverStatement { } async _finalize() { + console.log('finalizing...', this.con.path); // Wait for these to complete, but ignore any errors. // TODO: also wait for run/step to complete await this.preparePromise; @@ -202,7 +203,7 @@ class StatementImpl implements SqliteDriverStatement { } finalize(): void { - this._finalize(); + m.runExclusive(() => this._finalize()); } reset(options?: ResetOptions): void { @@ -287,6 +288,7 @@ export class WaSqliteConnection implements SqliteDriverConnection { async close() { await m.runExclusive(async () => { + console.log('closing...', this.path); for (let statement of this.statements) { if (statement.options.persist) { statement.finalize(); diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index 80b400d..09e65e9 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -2,13 +2,22 @@ import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; import { sqlite3, module, WaSqliteConnection } from './wa-sqlite-driver'; import { setupDriverWorker } from './worker_threads'; -// Register a custom file system. -const vfs = await OPFSCoopSyncVFS2.create('test.db', module); -// @ts-ignore -sqlite3.vfs_register(vfs as any, true); - +let vfs: OPFSCoopSyncVFS2 | null = null; setupDriverWorker({ async openConnection(options) { + // Register a custom file system. + if (vfs != null) { + throw new Error('Can only open one connection'); + } + console.log('open', options); + vfs = await OPFSCoopSyncVFS2.create( + 'test.db', + module, + options.readonly ?? false + ); + // @ts-ignore + sqlite3.vfs_register(vfs as any, true); + return await WaSqliteConnection.open(options.path, vfs); } }); diff --git a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts index 48d30cb..c18362b 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts @@ -26,9 +26,12 @@ export class WorkerConnectionAdapter implements WorkerDriver { constructor(public connnection: SqliteDriverConnection) {} statements = new Map(); + private promise: Promise = Promise.resolve(); async close() { - await this.connnection.close(); + const p = this.promise.then(() => this.connnection.close()); + this.promise = p; + return p; } private requireStatement(id: number) { @@ -123,6 +126,14 @@ export class WorkerConnectionAdapter implements WorkerDriver { async execute( commands: T + ): Promise> { + const p = this.promise.then(() => this._execute(commands)); + this.promise = p.then(() => {}); + return p; + } + + async _execute( + commands: T ): Promise> { let results: SqliteCommandResponse[] = []; diff --git a/packages/wa-sqlite-driver/test/src/concurrency.test.ts b/packages/wa-sqlite-driver/test/src/concurrency.test.ts index f8c128d..655625b 100644 --- a/packages/wa-sqlite-driver/test/src/concurrency.test.ts +++ b/packages/wa-sqlite-driver/test/src/concurrency.test.ts @@ -42,7 +42,9 @@ describe('concurrency tests', () => { for (let i = 0; i < 5; i++) { const p = (async () => { const start = Date.now(); - await using connection = await driver.reserveConnection(); + await using connection = await driver.reserveConnection({ + readonly: true + }); using b = connection.prepare('begin immediate'); await b.step(); @@ -59,5 +61,27 @@ describe('concurrency tests', () => { promises.push(p); } await Promise.all(promises); + + for (let i = 0; i < 5; i++) { + const p = (async () => { + const start = Date.now(); + await using connection = await driver.reserveConnection({ + readonly: true + }); + + using b = connection.prepare('begin immediate'); + await b.step(); + using s = connection.prepare('select * from test_data'); + const { rows } = await s.step(); + + expect(rows).toEqual([{ id: 1, data: 'test' }]); + await new Promise((resolve) => setTimeout(resolve, 500)); + + using e = connection.prepare('commit'); + await e.step(); + console.log('tx done in', Date.now() - start); + })(); + promises.push(p); + } }); }); From 81465a640ff254f2547b55582765c04104bb9ada Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 19:57:17 -0600 Subject: [PATCH 15/25] Fix the test. --- packages/driver/src/util/SingleConnectionPool.ts | 1 - packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 8 ++++---- packages/wa-sqlite-driver/src/pool.ts | 1 - packages/wa-sqlite-driver/src/wa-sqlite-worker.ts | 1 - packages/wa-sqlite-driver/src/worker_threads/setup.ts | 2 -- .../wa-sqlite-driver/src/worker_threads/worker-driver.ts | 4 ++++ packages/wa-sqlite-driver/test/src/concurrency.test.ts | 1 + packages/wa-sqlite-driver/test/src/driver.test.ts | 1 - 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/driver/src/util/SingleConnectionPool.ts b/packages/driver/src/util/SingleConnectionPool.ts index 8598888..07219e6 100644 --- a/packages/driver/src/util/SingleConnectionPool.ts +++ b/packages/driver/src/util/SingleConnectionPool.ts @@ -29,7 +29,6 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { async reserveConnection( options?: ReserveConnectionOptions ): Promise { - console.log('single reserveConnection', this.connection.lock); if (options?.signal?.aborted) { throw new Error('Aborted'); } diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index c1c7241..5de653d 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -62,10 +62,10 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { mapIdToFile = new Map(); lastError = null; - // log = null; //function(...args) { console.log(`[${contextName}]`, ...args) }; - log = function (...args) { - console.log(`[OPFSCoopSyncVFS2]`, ...args); - }; + log = null; + // log = function (...args) { + // console.log(`[OPFSCoopSyncVFS2]`, ...args); + // }; persistentFiles = new Map(); boundAccessHandles = new Map(); diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index fdf66d2..6130d48 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -38,7 +38,6 @@ export function waSqliteWorkerPool(path: string): SqliteDriverConnectionPool { async openConnection( options?: ReserveConnectionOptions & { connectionName?: string } ): Promise { - console.log('openConnection', options); const connection = new WorkerDriverConnection( new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { type: 'module' diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index 09e65e9..e12a386 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -9,7 +9,6 @@ setupDriverWorker({ if (vfs != null) { throw new Error('Can only open one connection'); } - console.log('open', options); vfs = await OPFSCoopSyncVFS2.create( 'test.db', module, diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts index 773a182..587ddac 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/setup.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -27,8 +27,6 @@ export function setupDriverPort(config: WorkerDriverConfig) { const listener = async (value: any) => { const [message, id, args] = value.data; - console.log('received', message, id, args); - if (message == 'open') { const open = new Deferred(); opened = open.promise; diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index f9d00d8..0700e19 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -117,6 +117,10 @@ export class WorkerDriverConnection implements SqliteDriverConnection { this._maybeFlush(); } + log(...args: any[]) { + console.log(this.options.path, this.options.connectionName, ...args); + } + private registerCallback(callback: (value: any) => void) { const id = this.nextCallbackId++; this.callbacks.set(id, callback); diff --git a/packages/wa-sqlite-driver/test/src/concurrency.test.ts b/packages/wa-sqlite-driver/test/src/concurrency.test.ts index 655625b..b5fd39b 100644 --- a/packages/wa-sqlite-driver/test/src/concurrency.test.ts +++ b/packages/wa-sqlite-driver/test/src/concurrency.test.ts @@ -83,5 +83,6 @@ describe('concurrency tests', () => { })(); promises.push(p); } + await Promise.all(promises); }); }); diff --git a/packages/wa-sqlite-driver/test/src/driver.test.ts b/packages/wa-sqlite-driver/test/src/driver.test.ts index 1134801..3aef1df 100644 --- a/packages/wa-sqlite-driver/test/src/driver.test.ts +++ b/packages/wa-sqlite-driver/test/src/driver.test.ts @@ -5,7 +5,6 @@ describeDriverTests( 'wa-sqlite', { getColumns: true, rawResults: true, allowsMissingParameters: false }, async (path) => { - console.log('open', path); return waSqliteSingleWorker(path); } ); From 05a3e575a264c40d6dabe7ff39e8b00a69c30965 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 20:00:08 -0600 Subject: [PATCH 16/25] Remove prelock support. --- packages/driver/src/driver-api.ts | 3 --- packages/driver/src/util/SingleConnectionPool.ts | 7 ------- packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 14 -------------- packages/wa-sqlite-driver/src/wa-sqlite-driver.ts | 10 ---------- .../src/worker_threads/WorkerDriverAdapter.ts | 4 ---- .../src/worker_threads/async-commands.ts | 13 +------------ .../src/worker_threads/worker-driver.ts | 8 -------- 7 files changed, 1 insertion(+), 58 deletions(-) diff --git a/packages/driver/src/driver-api.ts b/packages/driver/src/driver-api.ts index cfa5155..15e2266 100644 --- a/packages/driver/src/driver-api.ts +++ b/packages/driver/src/driver-api.ts @@ -35,9 +35,6 @@ export interface SqliteDriverConnection { getLastChanges(): Promise; close(): Promise; - - lock?(mode: 'exclusive' | 'shared' | 'deferred'): Promise; - release?(): void; } export type SqliteParameterBinding = diff --git a/packages/driver/src/util/SingleConnectionPool.ts b/packages/driver/src/util/SingleConnectionPool.ts index 07219e6..66a0e4e 100644 --- a/packages/driver/src/util/SingleConnectionPool.ts +++ b/packages/driver/src/util/SingleConnectionPool.ts @@ -37,7 +37,6 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { async () => { // TODO: sync if (this.inUse === reserved) { - reserved.connection.release?.(); this.inUse = null; Promise.resolve().then(() => this.next()); } @@ -46,9 +45,6 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { if (this.inUse == null) { this.inUse = reserved; - await reserved.connection.lock?.( - options?.readonly ? 'shared' : 'exclusive' - ); return reserved; } else { const promise = new Promise((resolve, reject) => { @@ -70,9 +66,6 @@ export class SingleConnectionPool implements SqliteDriverConnectionPool { return promise.then(async (r) => { this.inUse = reserved; - await reserved.connection.lock?.( - options?.readonly ? 'shared' : 'exclusive' - ); return r; }); } diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index 5de653d..ed0ef11 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -92,20 +92,6 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return (this as unknown as WithModule)._module; } - async prelock(fileName: string): Promise { - const file = this.persistentFiles.get('/' + fileName); - this.log?.('prelock', fileName, file); - await this.#requestAccessHandle(file); - this.log?.('prelocked', fileName); - const self = this; - return { - [Symbol.dispose]() { - this.log?.('prelock release', fileName); - self.#releaseAccessHandle(file); - } - }; - } - async #initialize(nTemporaryFiles) { // Delete temporary directories no longer in use. const root = await navigator.storage.getDirectory(); diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index a3f5409..b2abb86 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -257,7 +257,6 @@ export class WaSqliteConnection implements SqliteDriverConnection { vfs: OPFSCoopSyncVFS2; statements = new Set(); - lockDisposer: Disposable | null = null; static async open( filename: string, @@ -277,15 +276,6 @@ export class WaSqliteConnection implements SqliteDriverConnection { this.vfs = vfs; } - async lock() { - this.lockDisposer = await this.vfs.prelock(this.path); - } - - release() { - this.lockDisposer[Symbol.dispose](); - this.lockDisposer = null; - } - async close() { await m.runExclusive(async () => { console.log('closing...', this.path); diff --git a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts index c18362b..0fbe4f7 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts @@ -115,10 +115,6 @@ export class WorkerConnectionAdapter implements WorkerDriver { return this._parse(command); case SqliteCommandType.changes: return this.connnection.getLastChanges(); - case SqliteCommandType.lock: - return this.connnection.lock?.(command.mode); - case SqliteCommandType.release: - return this.connnection.release?.(); default: throw new Error(`Unknown command: ${command.type}`); } diff --git a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts index 4ba4bfe..0b4de11 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts @@ -91,15 +91,6 @@ export interface SqliteGetChanges { type: SqliteCommandType.changes; } -export interface SqliteLock { - type: SqliteCommandType.lock; - mode: 'exclusive' | 'shared' | 'deferred'; -} - -export interface SqliteRelease { - type: SqliteCommandType.release; -} - export type SqliteCommand = | SqlitePrepare | SqliteBind @@ -109,9 +100,7 @@ export type SqliteCommand = | SqliteFinalize | SqliteSync | SqliteParse - | SqliteGetChanges - | SqliteLock - | SqliteRelease; + | SqliteGetChanges; export type InferCommandResult = T extends SqliteRun ? SqliteChanges diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index 0700e19..e8cf541 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -70,14 +70,6 @@ export class WorkerDriverConnection implements SqliteDriverConnection { return this.post('open', this.options); } - async lock(mode: 'exclusive' | 'shared' | 'deferred'): Promise { - await this._send({ type: SqliteCommandType.lock, mode: mode }); - } - - release(): void { - this._send({ type: SqliteCommandType.release }); - } - prepare(sql: string, options?: PrepareOptions): WorkerDriverStatement { const id = this.nextId++; this.buffer.push({ From c3671fd0361a1644b7d48a674eb4a0dc1098268b Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 20:37:16 -0600 Subject: [PATCH 17/25] WIP mptest. --- packages/wa-sqlite-driver/src/pool.ts | 19 +- .../test/src/mptest-runner.test.ts | 879 ++++++++++ .../test/src/mptest/README.md | 99 ++ .../test/src/mptest/config01.test | 46 + .../test/src/mptest/config02.test | 123 ++ .../test/src/mptest/crash01.test | 106 ++ .../test/src/mptest/crash02.subtest | 53 + .../wa-sqlite-driver/test/src/mptest/mptest.c | 1470 +++++++++++++++++ .../test/src/mptest/multiwrite01.test | 415 +++++ 9 files changed, 3196 insertions(+), 14 deletions(-) create mode 100644 packages/wa-sqlite-driver/test/src/mptest-runner.test.ts create mode 100644 packages/wa-sqlite-driver/test/src/mptest/README.md create mode 100644 packages/wa-sqlite-driver/test/src/mptest/config01.test create mode 100644 packages/wa-sqlite-driver/test/src/mptest/config02.test create mode 100644 packages/wa-sqlite-driver/test/src/mptest/crash01.test create mode 100644 packages/wa-sqlite-driver/test/src/mptest/crash02.subtest create mode 100644 packages/wa-sqlite-driver/test/src/mptest/mptest.c create mode 100644 packages/wa-sqlite-driver/test/src/mptest/multiwrite01.test diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index 6130d48..942ab41 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -1,22 +1,14 @@ -import { SqliteDriverConnectionPool } from '@sqlite-js/driver'; +import { + ReserveConnectionOptions, + SqliteDriverConnection, + SqliteDriverConnectionPool +} from '@sqlite-js/driver'; import { LazyConnectionPool, - MultiConnectionPool, ReadWriteConnectionPool } from '@sqlite-js/driver/util'; -import { - ReserveConnectionOptions, - SqliteDriverConnection -} from '@sqlite-js/driver'; import { WorkerDriverConnection } from './worker_threads'; -// import { WaSqliteConnection } from './wa-sqlite-driver'; - -// export function waSqlitePool(path: string): SqliteDriverConnectionPool { -// return new LazyConnectionPool(async () => { -// return await WaSqliteConnection.open(path); -// }); -// } export function waSqliteSingleWorker(path: string): SqliteDriverConnectionPool { return new LazyConnectionPool(async () => { @@ -28,7 +20,6 @@ export function waSqliteSingleWorker(path: string): SqliteDriverConnectionPool { ); await connection.open(); return connection; - // return await WaSqliteConnection.open(path); }); } diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts new file mode 100644 index 0000000..a9c30bf --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -0,0 +1,879 @@ +import { describe, test } from '@sqlite-js/driver-tests'; +import { SqliteError, SqliteValue } from '@sqlite-js/driver'; +import type { + ReservedConnection, + SqliteDriverConnectionPool, + SqliteRowRaw +} from '@sqlite-js/driver'; + +import { waSqliteSingleWorker } from '../../lib/index.js'; + +const scriptModules = (import.meta as any).glob('./mptest/**/*test', { + as: 'raw', + eager: true +}); + +const scriptMap = new Map(); +for (const [key, value] of Object.entries(scriptModules)) { + scriptMap.set(normalizePath(key), value as string); +} + +const topLevelScripts = [...scriptMap.keys()] + .filter((name) => name.endsWith('.test')) + .sort(); + +interface ScriptContext { + clientId: number; + connection: ReservedConnection; + filename: string; + displayName: string; +} + +interface TokenInfo { + length: number; + newlines: number; +} + +class ResultBuffer { + private parts: string[] = []; + + append(value: SqliteValue | string): void { + this.parts.push(formatTerm(value)); + } + + appendError(error: SqliteError): void { + const code = error.code ?? 'SQLITE_ERROR'; + this.parts.push(formatTerm(`error(${code})`)); + this.parts.push(formatTerm(error.message)); + } + + reset(): void { + this.parts = []; + } + + tokens(): string[] { + return [...this.parts]; + } + + toString(): string { + return this.parts.join(' '); + } +} + +interface ClientContext { + id: number; + pool: SqliteDriverConnectionPool; + reserved: ReservedConnection; + queue: Promise; + pending: Set>; +} + +class MptestRunner { + private clients = new Map(); + private pendingTasks = new Set>(); + + constructor(private readonly dbPath: string) {} + + async runScript(scriptName: string): Promise { + const script = requireScript(scriptName); + const master = await this.ensureClient(0); + await this.runScriptInternal( + { + clientId: 0, + connection: master.reserved, + filename: scriptName, + displayName: baseName(scriptName) + }, + script, + 1 + ); + await this.waitForAll(0); + } + + async close(): Promise { + const errors: Error[] = []; + await this.waitForAll(0).catch((err) => { + errors.push(err instanceof Error ? err : new Error(String(err))); + }); + await Promise.all( + [...this.clients.values()].map(async (client) => { + try { + await client.reserved.release(); + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); + } + try { + await client.pool.close(); + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); + } + }) + ); + this.clients.clear(); + if (errors.length > 0) { + throw errors[0]; + } + } + + private async ensureClient(clientId: number): Promise { + let client = this.clients.get(clientId); + if (client) { + return client; + } + const pool = waSqliteSingleWorker(this.dbPath); + const reserved = await pool.reserveConnection(); + client = { + id: clientId, + pool, + reserved, + queue: Promise.resolve(), + pending: new Set() + }; + this.clients.set(clientId, client); + return client; + } + + private trackTask(client: ClientContext, taskPromise: Promise): void { + client.pending.add(taskPromise); + this.pendingTasks.add(taskPromise); + taskPromise.finally(() => { + client.pending.delete(taskPromise); + this.pendingTasks.delete(taskPromise); + }); + } + + private async scheduleTask( + parent: ScriptContext, + clientId: number, + script: string, + startLine: number, + taskLabel: string + ): Promise { + const client = await this.ensureClient(clientId); + const run = async () => { + await this.runScriptInternal( + { + clientId, + connection: client.reserved, + filename: parent.filename, + displayName: `${parent.displayName}#client${clientId}:${taskLabel}` + }, + script, + startLine + ); + }; + const task = client.queue.then(run); + client.queue = task.catch(() => {}); + this.trackTask(client, task); + } + + private async waitForAll(timeoutMs: number): Promise { + await this.waitWithTimeout( + [...this.pendingTasks], + 'all clients', + timeoutMs + ); + } + + private async waitForClient( + clientId: number, + timeoutMs: number + ): Promise { + const client = this.clients.get(clientId); + const pending = client ? [...client.pending] : []; + await this.waitWithTimeout(pending, `client ${clientId}`, timeoutMs); + } + + private async waitWithTimeout( + promises: Promise[], + label: string, + timeoutMs: number + ): Promise { + if (promises.length === 0) { + return; + } + const waitPromise = Promise.all(promises).then(() => undefined); + if (timeoutMs <= 0) { + await waitPromise; + return; + } + let handle: ReturnType | undefined; + try { + await Promise.race([ + waitPromise, + new Promise((_, reject) => { + handle = setTimeout( + () => + reject( + new Error(`timeout waiting for ${label} (${timeoutMs}ms)`) + ), + timeoutMs + ); + }) + ]); + } finally { + if (handle) { + clearTimeout(handle); + } + } + } + + private async runScriptInternal( + context: ScriptContext, + script: string, + initialLine: number + ): Promise { + const result = new ResultBuffer(); + let index = 0; + let begin = 0; + let line = initialLine; + while (index < script.length) { + const prevLine = line; + const token = tokenLength(script, index); + if (token.length <= 0) { + break; + } + line += token.newlines; + const chr = script[index]; + if (isWhitespace(chr) || (chr === '/' && script[index + 1] === '*')) { + index += token.length; + continue; + } + if ( + chr !== '-' || + script[index + 1] !== '-' || + !isAlpha(script[index + 2]) + ) { + index += token.length; + continue; + } + if (index > begin) { + const sqlChunk = script.slice(begin, index); + await this.executeSql(context, sqlChunk, result); + } + let consumed = token.length; + const command = parseCommand(script, index, token.length); + switch (command.name) { + case 'sleep': + await sleepMs(Number(command.args[0] ?? '0')); + break; + case 'match': { + const expectedRaw = command.payload.trim(); + const expectedTokens = tokenizeMatch(expectedRaw); + const actualTokens = result.tokens(); + if (!tokensEqual(expectedTokens, actualTokens)) { + throw new Error( + `${context.displayName}:${prevLine} expected [${expectedRaw}] but got [${result.toString()}]` + ); + } + result.reset(); + break; + } + case 'task': { + const clientId = Number(command.args[0]); + if (!Number.isInteger(clientId) || clientId < 0) { + throw new Error( + `${context.displayName}:${prevLine} invalid client ${command.args[0]}` + ); + } + const lineRef = { value: line }; + const blockStart = index + consumed; + const blockLength = findEnd(script, blockStart, lineRef); + const endToken = tokenLength(script, blockStart + blockLength); + line = lineRef.value + endToken.newlines; + consumed += blockLength + endToken.length; + const taskScript = script.slice(blockStart, blockStart + blockLength); + const taskLabel = command.args[1] + ? command.args[1] + : `${baseName(context.filename)}:${prevLine}`; + await this.scheduleTask( + context, + clientId, + taskScript, + prevLine + 1, + taskLabel + ); + break; + } + case 'wait': { + const target = command.args[0] ?? 'all'; + const timeout = command.args[1] ? Number(command.args[1]) : 10000; + if (target === 'all') { + await this.waitForAll(timeout); + } else { + await this.waitForClient(Number(target), timeout); + } + break; + } + case 'source': { + const sourceName = command.args[0]; + if (!sourceName) { + throw new Error( + `${context.displayName}:${prevLine} missing source filename` + ); + } + const resolved = resolveSourcePath(context.filename, sourceName); + const subScript = requireScript(resolved); + const master = await this.ensureClient(0); + await this.runScriptInternal( + { + clientId: 0, + connection: master.reserved, + filename: resolved, + displayName: baseName(resolved) + }, + subScript, + 1 + ); + break; + } + case 'if': { + const condition = await this.evaluateCondition( + context, + command.payload + ); + if (!condition) { + const lineRef = { value: line }; + const skip = findEndif(script, index + consumed, true, lineRef); + line = lineRef.value; + consumed += skip; + } + break; + } + case 'else': { + const lineRef = { value: line }; + const skip = findEndif(script, index + consumed, false, lineRef); + line = lineRef.value; + consumed += skip; + break; + } + case 'endif': + break; + default: + throw new Error( + `${context.displayName}:${prevLine} unknown command --${command.name}` + ); + } + begin = index + consumed; + index += consumed; + } + if (begin < script.length) { + const remainder = script.slice(begin); + await this.executeSql(context, remainder, result); + } + } + + private async evaluateCondition( + context: ScriptContext, + expr: string + ): Promise { + const sql = `SELECT ${expr}`; + const statement = context.connection.prepare(sql, { rawResults: true }); + try { + const { rows } = await statement.step(); + if (!rows || rows.length === 0) { + return false; + } + const row = rows[0] as SqliteRowRaw; + const value = row[0]; + if (value == null) { + return false; + } + if (typeof value === 'number') { + return value !== 0; + } + if (typeof value === 'bigint') { + return value !== BigInt(0); + } + if (typeof value === 'string') { + return value.length > 0; + } + return true; + } finally { + statement.finalize(); + } + } + + private async executeSql( + context: ScriptContext, + chunk: string, + result: ResultBuffer + ): Promise { + const statements = splitSql(chunk); + for (const sql of statements) { + if (!sql.trim()) { + continue; + } + const statement = context.connection.prepare(sql, { rawResults: true }); + try { + const { rows } = await statement.step(); + if (rows) { + for (const row of rows) { + for (const value of row as SqliteRowRaw) { + result.append(value); + } + } + } + } catch (err) { + if (isSqliteError(err)) { + result.appendError(err); + } else { + throw err; + } + } finally { + statement.finalize(); + } + } + } +} + +describe('mptest scripts', () => { + for (const script of topLevelScripts) { + test(script, async () => { + const dbPath = `mptest-${sanitizeForFilename(script)}-${Math.random() + .toString(36) + .slice(2)}.db`; + const runner = new MptestRunner(dbPath); + try { + await runner.runScript(script); + } finally { + await runner.close(); + } + }); + } +}); + +function normalizePath(input: string): string { + return input.startsWith('./') ? input.slice(2) : input; +} + +function baseName(file: string): string { + const normalized = file.replace(/\\/g, '/'); + const idx = normalized.lastIndexOf('/'); + return idx >= 0 ? normalized.slice(idx + 1) : normalized; +} + +function sanitizeForFilename(input: string): string { + return baseName(input).replace(/[^a-zA-Z0-9_-]+/g, '_'); +} + +function requireScript(path: string): string { + const normalized = normalizePath(path); + const script = scriptMap.get(normalized); + if (script == null) { + throw new Error(`Unknown script: ${normalized}`); + } + return script; +} + +function resolveSourcePath(currentFile: string, relative: string): string { + if (relative.startsWith('/')) { + return normalizePath(relative.slice(1)); + } + const currentParts = normalizePath(currentFile).split('/'); + currentParts.pop(); + for (const part of relative.split('/')) { + if (part === '' || part === '.') { + continue; + } + if (part === '..') { + if (currentParts.length > 0) { + currentParts.pop(); + } + } else { + currentParts.push(part); + } + } + return currentParts.join('/'); +} + +function isWhitespace(ch: string | undefined): boolean { + if (ch == null) { + return false; + } + return ( + ch === ' ' || + ch === '\n' || + ch === '\r' || + ch === '\t' || + ch === '\f' || + ch === '\v' + ); +} + +function isAlpha(ch: string | undefined): boolean { + if (!ch) { + return false; + } + const code = ch.charCodeAt(0); + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); +} + +function tokenLength(script: string, start: number): TokenInfo { + let n = 0; + let newlines = 0; + const first = script[start]; + if (first == null) { + return { length: 0, newlines: 0 }; + } + if (isWhitespace(first) || (first === '/' && script[start + 1] === '*')) { + let inComment = first === '/' ? 1 : 0; + if (first === '/') { + n = 2; + } + while (true) { + const c = script[start + n]; + n++; + if (c == null) { + break; + } + if (c === '\n') { + newlines++; + } + if (isWhitespace(c)) { + continue; + } + if (inComment && c === '*' && script[start + n] === '/') { + n++; + inComment = 0; + } else if (!inComment && c === '/' && script[start + n] === '*') { + n++; + inComment = 1; + } else if (!inComment) { + break; + } + } + n--; + return { length: n, newlines }; + } + if (first === '-' && script[start + 1] === '-') { + n = 2; + while (start + n < script.length && script[start + n] !== '\n') { + n++; + } + if (start + n < script.length) { + newlines++; + n++; + } + return { length: n, newlines }; + } + if (first === '"' || first === "'") { + const delim = first; + n = 1; + while (start + n < script.length) { + const c = script[start + n]; + if (c === '\n') { + newlines++; + } + n++; + if (c === delim) { + if (script[start + n] !== delim) { + break; + } + n++; + } + } + return { length: n, newlines }; + } + n = 1; + while (start + n < script.length) { + const c = script[start + n]; + if (!c || isWhitespace(c) || c === '"' || c === "'" || c === ';') { + break; + } + n++; + } + return { length: n, newlines }; +} + +function findEnd( + script: string, + start: number, + lineRef: { value: number } +): number { + let offset = 0; + while (start + offset < script.length) { + if ( + script.startsWith('--end', start + offset) && + isWhitespace(script[start + offset + 5] ?? '\n') + ) { + break; + } + const token = tokenLength(script, start + offset); + lineRef.value += token.newlines; + if (token.length <= 0) { + break; + } + offset += token.length; + } + return offset; +} + +function findEndif( + script: string, + start: number, + stopAtElse: boolean, + lineRef: { value: number } +): number { + let offset = 0; + while (start + offset < script.length) { + const current = start + offset; + const token = tokenLength(script, current); + lineRef.value += token.newlines; + if ( + script.startsWith('--endif', current) && + isWhitespace(script[current + 7] ?? '\n') + ) { + return offset + token.length; + } + if ( + stopAtElse && + script.startsWith('--else', current) && + isWhitespace(script[current + 6] ?? '\n') + ) { + return offset + token.length; + } + if ( + script.startsWith('--if', current) && + isWhitespace(script[current + 4] ?? '\n') + ) { + const inner = findEndif(script, current + token.length, false, lineRef); + offset += token.length + inner; + } else { + if (token.length <= 0) { + break; + } + offset += token.length; + } + } + return offset; +} + +interface ParsedCommand { + name: string; + args: string[]; + payload: string; +} + +function parseCommand( + script: string, + start: number, + length: number +): ParsedCommand { + const segment = script.slice(start, start + length).replace(/\r?\n?$/, ''); + const trimmed = segment.trim(); + const body = trimmed.slice(2).trim(); + const firstSpace = body.search(/\s/); + let name: string; + let payload: string; + if (firstSpace === -1) { + name = body; + payload = ''; + } else { + name = body.slice(0, firstSpace); + payload = body.slice(firstSpace).trimStart(); + } + const args = payload ? payload.split(/\s+/) : []; + return { name, args, payload }; +} + +function splitSql(script: string): string[] { + const statements: string[] = []; + let current = ''; + let inSingle = false; + let inDouble = false; + let inBracket = false; + let inLineComment = false; + let inBlockComment = false; + + for (let i = 0; i < script.length; i++) { + const c = script[i]; + const next = script[i + 1]; + if (inLineComment) { + current += c; + if (c === '\n') { + inLineComment = false; + } + continue; + } + if (inBlockComment) { + current += c; + if (c === '*' && next === '/') { + current += next; + i++; + inBlockComment = false; + } + continue; + } + if (!inSingle && !inDouble) { + if (c === '-' && next === '-') { + inLineComment = true; + current += c; + continue; + } + if (c === '/' && next === '*') { + inBlockComment = true; + current += c; + continue; + } + } + if (c === "'" && !inDouble) { + inSingle = !inSingle; + current += c; + if (inSingle && next === "'") { + current += next; + i++; + } + continue; + } + if (c === '"' && !inSingle) { + inDouble = !inDouble; + current += c; + if (inDouble && next === '"') { + current += next; + i++; + } + continue; + } + if (!inSingle && !inDouble) { + if (c === '[') { + inBracket = true; + } else if (c === ']' && inBracket) { + inBracket = false; + } + } + if (!inSingle && !inDouble && !inBracket && c === ';') { + const statement = current.trim(); + if (statement) { + statements.push(statement); + } + current = ''; + continue; + } + current += c; + } + const trailing = current.trim(); + if (trailing) { + statements.push(trailing); + } + return statements; +} + +function tokenizeMatch(input: string): string[] { + const tokens: string[] = []; + let i = 0; + while (i < input.length) { + while (i < input.length && /\s/.test(input[i]!)) i++; + if (i >= input.length) { + break; + } + if (input[i] === "'") { + const start = i; + i++; + while (i < input.length) { + if (input[i] === "'") { + i++; + if (input[i] === "'") { + i++; + continue; + } + break; + } + i++; + } + tokens.push(input.slice(start, i)); + } else { + const start = i; + while (i < input.length && !/\s/.test(input[i]!)) i++; + tokens.push(input.slice(start, i)); + } + } + return tokens; +} + +function tokensEqual(expected: string[], actual: string[]): boolean { + if (expected.length !== actual.length) { + return false; + } + for (let i = 0; i < expected.length; i++) { + const e = expected[i]; + const a = actual[i]; + if (e === a) { + continue; + } + if (normalizeNumeric(e) === normalizeNumeric(a)) { + continue; + } + return false; + } + return true; +} + +function normalizeNumeric(token: string): string { + if (!/^-?\d+(?:\.\d+)?$/.test(token)) { + return token; + } + if (!token.includes('.')) { + return token; + } + const negative = token.startsWith('-'); + let body = negative ? token.slice(1) : token; + body = body.replace(/\.0+$/, ''); + body = body.replace(/(\.\d*?[1-9])0+$/, '$1'); + if (body.endsWith('.')) { + body = body.slice(0, -1); + } + if (!body) { + body = '0'; + } + return negative ? `-${body}` : body; +} + +function formatTerm(value: SqliteValue | string): string { + if (value == null) { + return 'nil'; + } + if (typeof value === 'string') { + return formatString(value); + } + if (typeof value === 'number' || typeof value === 'bigint') { + return String(value); + } + if (value instanceof Uint8Array) { + return `x'${toHex(value)}'`; + } + return formatString(String(value)); +} + +function formatString(value: string): string { + if (!/\s/.test(value) && !value.includes("'")) { + return value; + } + return `'${value.replace(/'/g, "''")}'`; +} + +function toHex(data: Uint8Array): string { + let hex = ''; + for (let i = 0; i < data.length; i++) { + const byte = data[i]; + hex += byte.toString(16).padStart(2, '0'); + } + return hex; +} + +function sleepMs(ms: number): Promise { + if (ms <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isSqliteError(err: unknown): err is SqliteError { + return ( + err instanceof SqliteError || + (typeof err === 'object' && + err != null && + 'code' in err && + 'message' in err) + ); +} diff --git a/packages/wa-sqlite-driver/test/src/mptest/README.md b/packages/wa-sqlite-driver/test/src/mptest/README.md new file mode 100644 index 0000000..1e0dd04 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/README.md @@ -0,0 +1,99 @@ + + /* + ** --sleep N + ** + ** Pause for N milliseconds + */ + /* + ** --exit N + ** + ** Exit this process. If N>0 then exit without shutting down + ** SQLite. (In other words, simulate a crash.) + */ + /* + ** --testcase NAME + ** + ** Begin a new test case. Announce in the log that the test case + ** has begun. + */ + + /* + ** --finish + ** + ** Mark the current task as having finished, even if it is not. + ** This can be used in conjunction with --exit to simulate a crash. + */ + /* + ** --reset + ** + ** Reset accumulated results back to an empty string + */ + + /* + ** --match ANSWER... + ** + ** Check to see if output matches ANSWER. Report an error if not. + */ + /* + ** --glob ANSWER... + ** --notglob ANSWER.... + ** + ** Check to see if output does or does not match the glob pattern + ** ANSWER. + */ + /* + ** --output + ** + ** Output the result of the previous SQL. + */ + + /* + ** --source FILENAME + ** + ** Run a subscript from a separate file. + */ + /* + ** --print MESSAGE.... + ** + ** Output the remainder of the line to the log file + */ + + /* + ** --if EXPR + ** + ** Skip forward to the next matching --endif or --else if EXPR is false. + */ + /* + ** --else + ** + ** This command can only be encountered if currently inside an --if that + ** is true. Skip forward to the next matching --endif. + */ + /* + ** --endif + ** + ** This command can only be encountered if currently inside an --if that + ** is true or an --else of a false if. This is a no-op. + */ + + /* + ** --start CLIENT + ** + ** Start up the given client. + */ + /* + ** --wait CLIENT TIMEOUT + ** + ** Wait until all tasks complete for the given client. If CLIENT is + ** "all" then wait for all clients to complete. Wait no longer than + ** TIMEOUT milliseconds (default 10,000) + */ + + /* + ** --task CLIENT + ** + ** --end + ** + ** Assign work to a client. Start the client if it is not running + ** already. + */ \ No newline at end of file diff --git a/packages/wa-sqlite-driver/test/src/mptest/config01.test b/packages/wa-sqlite-driver/test/src/mptest/config01.test new file mode 100644 index 0000000..683ee91 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/config01.test @@ -0,0 +1,46 @@ +/* +** Configure five tasks in different ways, then run tests. +*/ +--if vfsname() GLOB 'unix' +PRAGMA page_size=8192; +--task 1 + PRAGMA journal_mode=PERSIST; + PRAGMA mmap_size=0; +--end +--task 2 + PRAGMA journal_mode=TRUNCATE; + PRAGMA mmap_size=28672; +--end +--task 3 + PRAGMA journal_mode=MEMORY; +--end +--task 4 + PRAGMA journal_mode=OFF; +--end +--task 4 + PRAGMA mmap_size(268435456); +--end +--source multiwrite01.test +--wait all +PRAGMA page_size=16384; +VACUUM; +CREATE TABLE pgsz(taskid, sz INTEGER); +--task 1 + INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); +--end +--task 2 + INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); +--end +--task 3 + INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); +--end +--task 4 + INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); +--end +--task 5 + INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); +--end +--source multiwrite01.test +--wait all +SELECT sz FROM pgsz; +--match 16384 16384 16384 16384 16384 diff --git a/packages/wa-sqlite-driver/test/src/mptest/config02.test b/packages/wa-sqlite-driver/test/src/mptest/config02.test new file mode 100644 index 0000000..7d4b278 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/config02.test @@ -0,0 +1,123 @@ +/* +** Configure five tasks in different ways, then run tests. +*/ +PRAGMA page_size=512; +--task 1 + PRAGMA mmap_size=0; +--end +--task 2 + PRAGMA mmap_size=28672; +--end +--task 3 + PRAGMA mmap_size=8192; +--end +--task 4 + PRAGMA mmap_size=65536; +--end +--task 5 + PRAGMA mmap_size=268435456; +--end +--source multiwrite01.test +--source crash02.subtest +PRAGMA page_size=1024; +VACUUM; +CREATE TABLE pgsz(taskid, sz INTEGER); +--task 1 + INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); +--end +--task 2 + INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); +--end +--task 3 + INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); +--end +--task 4 + INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); +--end +--task 5 + INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); +--end +--source multiwrite01.test +--source crash02.subtest +--wait all +SELECT sz FROM pgsz; +--match 1024 1024 1024 1024 1024 +PRAGMA page_size=2048; +VACUUM; +DELETE FROM pgsz; +--task 1 + INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); +--end +--task 2 + INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); +--end +--task 3 + INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); +--end +--task 4 + INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); +--end +--task 5 + INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); +--end +--source multiwrite01.test +--source crash02.subtest +--wait all +SELECT sz FROM pgsz; +--match 2048 2048 2048 2048 2048 +PRAGMA page_size=8192; +VACUUM; +DELETE FROM pgsz; +--task 1 + INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); +--end +--task 2 + INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); +--end +--task 3 + INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); +--end +--task 4 + INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); +--end +--task 5 + INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); +--end +--source multiwrite01.test +--source crash02.subtest +--wait all +SELECT sz FROM pgsz; +--match 8192 8192 8192 8192 8192 +PRAGMA page_size=16384; +VACUUM; +DELETE FROM pgsz; +--task 1 + INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); +--end +--task 2 + INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); +--end +--task 3 + INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); +--end +--task 4 + INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); +--end +--task 5 + INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); +--end +--source multiwrite01.test +--source crash02.subtest +--wait all +SELECT sz FROM pgsz; +--match 16384 16384 16384 16384 16384 +PRAGMA auto_vacuum=FULL; +VACUUM; +--source multiwrite01.test +--source crash02.subtest +--wait all +PRAGMA auto_vacuum=FULL; +PRAGMA page_size=512; +VACUUM; +--source multiwrite01.test +--source crash02.subtest diff --git a/packages/wa-sqlite-driver/test/src/mptest/crash01.test b/packages/wa-sqlite-driver/test/src/mptest/crash01.test new file mode 100644 index 0000000..f1483df --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/crash01.test @@ -0,0 +1,106 @@ +/* Test cases involving incomplete transactions that must be rolled back. +*/ +--task 1 + DROP TABLE IF EXISTS t1; + CREATE TABLE t1(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t1 VALUES(1, randomblob(2000)); + INSERT INTO t1 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t1 SELECT a+2, randomblob(1500) FROM t1; + INSERT INTO t1 SELECT a+4, randomblob(1500) FROM t1; + INSERT INTO t1 SELECT a+8, randomblob(1500) FROM t1; + --sleep 1 + INSERT INTO t1 SELECT a+16, randomblob(1500) FROM t1; + --sleep 1 + INSERT INTO t1 SELECT a+32, randomblob(1500) FROM t1; + SELECT count(*) FROM t1; + --match 64 + SELECT avg(length(b)) FROM t1; + --match 1500.0 + --sleep 2 + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t1; + --match 247 + SELECT a FROM t1 WHERE b='x17y'; + --match 17 + CREATE INDEX t1b ON t1(b); + SELECT a FROM t1 WHERE b='x17y'; + --match 17 + SELECT a FROM t1 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end +--wait 1 +--task 2 + DROP TABLE IF EXISTS t2; + CREATE TABLE t2(a INTEGER PRIMARY KEY, b); + INSERT INTO t2 SELECT a, b FROM t1; + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t2; + --match 247 + SELECT a FROM t2 WHERE b='x17y'; + --match 17 + CREATE INDEX t2b ON t2(b); + SELECT a FROM t2 WHERE b='x17y'; + --match 17 + SELECT a FROM t2 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end +--task 3 + DROP TABLE IF EXISTS t3; + CREATE TABLE t3(a INTEGER PRIMARY KEY, b); + INSERT INTO t3 SELECT a, b FROM t1; + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t3; + --match 247 + SELECT a FROM t3 WHERE b='x17y'; + --match 17 + CREATE INDEX t3b ON t3(b); + SELECT a FROM t3 WHERE b='x17y'; + --match 17 + SELECT a FROM t3 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end +--task 4 + DROP TABLE IF EXISTS t4; + CREATE TABLE t4(a INTEGER PRIMARY KEY, b); + INSERT INTO t4 SELECT a, b FROM t1; + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t4; + --match 247 + SELECT a FROM t4 WHERE b='x17y'; + --match 17 + CREATE INDEX t4b ON t4(b); + SELECT a FROM t4 WHERE b='x17y'; + --match 17 + SELECT a FROM t4 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end +--task 5 + DROP TABLE IF EXISTS t5; + CREATE TABLE t5(a INTEGER PRIMARY KEY, b); + INSERT INTO t5 SELECT a, b FROM t1; + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t5; + --match 247 + SELECT a FROM t5 WHERE b='x17y'; + --match 17 + CREATE INDEX t5b ON t5(b); + SELECT a FROM t5 WHERE b='x17y'; + --match 17 + SELECT a FROM t5 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + +--wait all +/* After the database file has been set up, run the crash2 subscript +** multiple times. */ +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest +--source crash02.subtest diff --git a/packages/wa-sqlite-driver/test/src/mptest/crash02.subtest b/packages/wa-sqlite-driver/test/src/mptest/crash02.subtest new file mode 100644 index 0000000..86f64dd --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/crash02.subtest @@ -0,0 +1,53 @@ +/* +** This script is called from crash01.test and config02.test and perhaps other +** script. After the database file has been set up, make a big rollback +** journal in client 1, then crash client 1. +** Then in the other clients, do an integrity check. +*/ +--task 1 leave-hot-journal + --sleep 5 + --finish + PRAGMA cache_size=10; + BEGIN; + UPDATE t1 SET b=randomblob(20000); + UPDATE t2 SET b=randomblob(20000); + UPDATE t3 SET b=randomblob(20000); + UPDATE t4 SET b=randomblob(20000); + UPDATE t5 SET b=randomblob(20000); + UPDATE t1 SET b=NULL; + UPDATE t2 SET b=NULL; + UPDATE t3 SET b=NULL; + UPDATE t4 SET b=NULL; + UPDATE t5 SET b=NULL; + --print Task one crashing an incomplete transaction + --exit 1 +--end +--task 2 integrity_check-2 + SELECT count(*) FROM t1; + --match 64 + --sleep 100 + PRAGMA integrity_check(10); + --match ok +--end +--task 3 integrity_check-3 + SELECT count(*) FROM t1; + --match 64 + --sleep 100 + PRAGMA integrity_check(10); + --match ok +--end +--task 4 integrity_check-4 + SELECT count(*) FROM t1; + --match 64 + --sleep 100 + PRAGMA integrity_check(10); + --match ok +--end +--task 5 integrity_check-5 + SELECT count(*) FROM t1; + --match 64 + --sleep 100 + PRAGMA integrity_check(10); + --match ok +--end +--wait all diff --git a/packages/wa-sqlite-driver/test/src/mptest/mptest.c b/packages/wa-sqlite-driver/test/src/mptest/mptest.c new file mode 100644 index 0000000..5022b00 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/mptest.c @@ -0,0 +1,1470 @@ +/* +** 2013-04-05 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** +** This is a program used for testing SQLite, and specifically for testing +** the ability of independent processes to access the same SQLite database +** concurrently. +** +** Compile this program as follows: +** +** gcc -g -c -Wall sqlite3.c $(OPTS) +** gcc -g -o mptest mptest.c sqlite3.o $(LIBS) +** +** Recommended options: +** +** -DHAVE_USLEEP +** -DSQLITE_NO_SYNC +** -DSQLITE_THREADSAFE=0 +** -DSQLITE_OMIT_LOAD_EXTENSION +** +** Run like this: +** +** ./mptest $database $script +** +** where $database is the database to use for testing and $script is a +** test script. +*/ +#include "sqlite3.h" +#include +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +#endif +#include +#include +#include +#include +#include + +#define ISSPACE(X) isspace((unsigned char)(X)) +#define ISDIGIT(X) isdigit((unsigned char)(X)) + +/* The suffix to append to the child command lines, if any */ +#if defined(_WIN32) +# define GETPID (int)GetCurrentProcessId +#else +# define GETPID getpid +#endif + +/* The directory separator character(s) */ +#if defined(_WIN32) +# define isDirSep(c) (((c) == '/') || ((c) == '\\')) +#else +# define isDirSep(c) ((c) == '/') +#endif + +/* Mark a parameter as unused to suppress compiler warnings */ +#define UNUSED_PARAMETER(x) (void)x + +/* Global data +*/ +static struct Global { + char *argv0; /* Name of the executable */ + const char *zVfs; /* Name of VFS to use. Often NULL meaning "default" */ + char *zDbFile; /* Name of the database */ + sqlite3 *db; /* Open connection to database */ + char *zErrLog; /* Filename for error log */ + FILE *pErrLog; /* Where to write errors */ + char *zLog; /* Name of output log file */ + FILE *pLog; /* Where to write log messages */ + char zName[32]; /* Symbolic name of this process */ + int taskId; /* Task ID. 0 means supervisor. */ + int iTrace; /* Tracing level */ + int bSqlTrace; /* True to trace SQL commands */ + int bIgnoreSqlErrors; /* Ignore errors in SQL statements */ + int nError; /* Number of errors */ + int nTest; /* Number of --match operators */ + int iTimeout; /* Milliseconds until a busy timeout */ + int bSync; /* Call fsync() */ +} g; + +/* Default timeout */ +#define DEFAULT_TIMEOUT 10000 + +/* +** Print a message adding zPrefix[] to the beginning of every line. +*/ +static void printWithPrefix(FILE *pOut, const char *zPrefix, const char *zMsg){ + while( zMsg && zMsg[0] ){ + int i; + for(i=0; zMsg[i] && zMsg[i]!='\n' && zMsg[i]!='\r'; i++){} + fprintf(pOut, "%s%.*s\n", zPrefix, i, zMsg); + zMsg += i; + while( zMsg[0]=='\n' || zMsg[0]=='\r' ) zMsg++; + } +} + +/* +** Compare two pointers to strings, where the pointers might be NULL. +*/ +static int safe_strcmp(const char *a, const char *b){ + if( a==b ) return 0; + if( a==0 ) return -1; + if( b==0 ) return 1; + return strcmp(a,b); +} + +/* +** Return TRUE if string z[] matches glob pattern zGlob[]. +** Return FALSE if the pattern does not match. +** +** Globbing rules: +** +** '*' Matches any sequence of zero or more characters. +** +** '?' Matches exactly one character. +** +** [...] Matches one character from the enclosed list of +** characters. +** +** [^...] Matches one character not in the enclosed list. +** +** '#' Matches any sequence of one or more digits with an +** optional + or - sign in front +*/ +int strglob(const char *zGlob, const char *z){ + int c, c2; + int invert; + int seen; + + while( (c = (*(zGlob++)))!=0 ){ + if( c=='*' ){ + while( (c=(*(zGlob++))) == '*' || c=='?' ){ + if( c=='?' && (*(z++))==0 ) return 0; + } + if( c==0 ){ + return 1; + }else if( c=='[' ){ + while( *z && strglob(zGlob-1,z) ){ + z++; + } + return (*z)!=0; + } + while( (c2 = (*(z++)))!=0 ){ + while( c2!=c ){ + c2 = *(z++); + if( c2==0 ) return 0; + } + if( strglob(zGlob,z) ) return 1; + } + return 0; + }else if( c=='?' ){ + if( (*(z++))==0 ) return 0; + }else if( c=='[' ){ + int prior_c = 0; + seen = 0; + invert = 0; + c = *(z++); + if( c==0 ) return 0; + c2 = *(zGlob++); + if( c2=='^' ){ + invert = 1; + c2 = *(zGlob++); + } + if( c2==']' ){ + if( c==']' ) seen = 1; + c2 = *(zGlob++); + } + while( c2 && c2!=']' ){ + if( c2=='-' && zGlob[0]!=']' && zGlob[0]!=0 && prior_c>0 ){ + c2 = *(zGlob++); + if( c>=prior_c && c<=c2 ) seen = 1; + prior_c = 0; + }else{ + if( c==c2 ){ + seen = 1; + } + prior_c = c2; + } + c2 = *(zGlob++); + } + if( c2==0 || (seen ^ invert)==0 ) return 0; + }else if( c=='#' ){ + if( (z[0]=='-' || z[0]=='+') && ISDIGIT(z[1]) ) z++; + if( !ISDIGIT(z[0]) ) return 0; + z++; + while( ISDIGIT(z[0]) ){ z++; } + }else{ + if( c!=(*(z++)) ) return 0; + } + } + return *z==0; +} + +/* +** Close output stream pOut if it is not stdout or stderr +*/ +static void maybeClose(FILE *pOut){ + if( pOut!=stdout && pOut!=stderr ) fclose(pOut); +} + +/* +** Print an error message +*/ +static void errorMessage(const char *zFormat, ...){ + va_list ap; + char *zMsg; + char zPrefix[30]; + va_start(ap, zFormat); + zMsg = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%s:ERROR: ", g.zName); + if( g.pLog ){ + printWithPrefix(g.pLog, zPrefix, zMsg); + fflush(g.pLog); + } + if( g.pErrLog && safe_strcmp(g.zErrLog,g.zLog) ){ + printWithPrefix(g.pErrLog, zPrefix, zMsg); + fflush(g.pErrLog); + } + sqlite3_free(zMsg); + g.nError++; +} + +/* Forward declaration */ +static int trySql(const char*, ...); + +/* +** Print an error message and then quit. +*/ +static void fatalError(const char *zFormat, ...){ + va_list ap; + char *zMsg; + char zPrefix[30]; + va_start(ap, zFormat); + zMsg = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%s:FATAL: ", g.zName); + if( g.pLog ){ + printWithPrefix(g.pLog, zPrefix, zMsg); + fflush(g.pLog); + maybeClose(g.pLog); + } + if( g.pErrLog && safe_strcmp(g.zErrLog,g.zLog) ){ + printWithPrefix(g.pErrLog, zPrefix, zMsg); + fflush(g.pErrLog); + maybeClose(g.pErrLog); + } + sqlite3_free(zMsg); + if( g.db ){ + int nTry = 0; + g.iTimeout = 0; + while( trySql("UPDATE client SET wantHalt=1;")==SQLITE_BUSY + && (nTry++)<100 ){ + sqlite3_sleep(10); + } + } + sqlite3_close(g.db); + exit(1); +} + + +/* +** Print a log message +*/ +static void logMessage(const char *zFormat, ...){ + va_list ap; + char *zMsg; + char zPrefix[30]; + va_start(ap, zFormat); + zMsg = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%s: ", g.zName); + if( g.pLog ){ + printWithPrefix(g.pLog, zPrefix, zMsg); + fflush(g.pLog); + } + sqlite3_free(zMsg); +} + +/* +** Return the length of a string omitting trailing whitespace +*/ +static int clipLength(const char *z){ + int n = (int)strlen(z); + while( n>0 && ISSPACE(z[n-1]) ){ n--; } + return n; +} + +/* +** Auxiliary SQL function to return the name of the VFS +*/ +static void vfsNameFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3 *db = sqlite3_context_db_handle(context); + char *zVfs = 0; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + sqlite3_file_control(db, "main", SQLITE_FCNTL_VFSNAME, &zVfs); + if( zVfs ){ + sqlite3_result_text(context, zVfs, -1, sqlite3_free); + } +} + +/* +** Busy handler with a g.iTimeout-millisecond timeout +*/ +static int busyHandler(void *pCD, int count){ + UNUSED_PARAMETER(pCD); + if( count*10>g.iTimeout ){ + if( g.iTimeout>0 ) errorMessage("timeout after %dms", g.iTimeout); + return 0; + } + sqlite3_sleep(10); + return 1; +} + +/* +** SQL Trace callback +*/ +static void sqlTraceCallback(void *NotUsed1, const char *zSql){ + UNUSED_PARAMETER(NotUsed1); + logMessage("[%.*s]", clipLength(zSql), zSql); +} + +/* +** SQL error log callback +*/ +static void sqlErrorCallback(void *pArg, int iErrCode, const char *zMsg){ + UNUSED_PARAMETER(pArg); + if( iErrCode==SQLITE_ERROR && g.bIgnoreSqlErrors ) return; + if( (iErrCode&0xff)==SQLITE_SCHEMA && g.iTrace<3 ) return; + if( g.iTimeout==0 && (iErrCode&0xff)==SQLITE_BUSY && g.iTrace<3 ) return; + if( (iErrCode&0xff)==SQLITE_NOTICE ){ + logMessage("(info) %s", zMsg); + }else{ + errorMessage("(errcode=%d) %s", iErrCode, zMsg); + } +} + +/* +** Prepare an SQL statement. Issue a fatal error if unable. +*/ +static sqlite3_stmt *prepareSql(const char *zFormat, ...){ + va_list ap; + char *zSql; + int rc; + sqlite3_stmt *pStmt = 0; + va_start(ap, zFormat); + zSql = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + rc = sqlite3_prepare_v2(g.db, zSql, -1, &pStmt, 0); + if( rc!=SQLITE_OK ){ + sqlite3_finalize(pStmt); + fatalError("%s\n%s\n", sqlite3_errmsg(g.db), zSql); + } + sqlite3_free(zSql); + return pStmt; +} + +/* +** Run arbitrary SQL. Issue a fatal error on failure. +*/ +static void runSql(const char *zFormat, ...){ + va_list ap; + char *zSql; + int rc; + va_start(ap, zFormat); + zSql = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + rc = sqlite3_exec(g.db, zSql, 0, 0, 0); + if( rc!=SQLITE_OK ){ + fatalError("%s\n%s\n", sqlite3_errmsg(g.db), zSql); + } + sqlite3_free(zSql); +} + +/* +** Try to run arbitrary SQL. Return success code. +*/ +static int trySql(const char *zFormat, ...){ + va_list ap; + char *zSql; + int rc; + va_start(ap, zFormat); + zSql = sqlite3_vmprintf(zFormat, ap); + va_end(ap); + rc = sqlite3_exec(g.db, zSql, 0, 0, 0); + sqlite3_free(zSql); + return rc; +} + +/* Structure for holding an arbitrary length string +*/ +typedef struct String String; +struct String { + char *z; /* the string */ + int n; /* Slots of z[] used */ + int nAlloc; /* Slots of z[] allocated */ +}; + +/* Free a string */ +static void stringFree(String *p){ + if( p->z ) sqlite3_free(p->z); + memset(p, 0, sizeof(*p)); +} + +/* Append n bytes of text to a string. If n<0 append the entire string. */ +static void stringAppend(String *p, const char *z, int n){ + if( n<0 ) n = (int)strlen(z); + if( p->n+n>=p->nAlloc ){ + int nAlloc = p->nAlloc*2 + n + 100; + char *zNew = sqlite3_realloc(p->z, nAlloc); + if( zNew==0 ) fatalError("out of memory"); + p->z = zNew; + p->nAlloc = nAlloc; + } + memcpy(p->z+p->n, z, n); + p->n += n; + p->z[p->n] = 0; +} + +/* Reset a string to an empty string */ +static void stringReset(String *p){ + if( p->z==0 ) stringAppend(p, " ", 1); + p->n = 0; + p->z[0] = 0; +} + +/* Append a new token onto the end of the string */ +static void stringAppendTerm(String *p, const char *z){ + int i; + if( p->n ) stringAppend(p, " ", 1); + if( z==0 ){ + stringAppend(p, "nil", 3); + return; + } + for(i=0; z[i] && !ISSPACE(z[i]); i++){} + if( i>0 && z[i]==0 ){ + stringAppend(p, z, i); + return; + } + stringAppend(p, "'", 1); + while( z[0] ){ + for(i=0; z[i] && z[i]!='\''; i++){} + if( z[i] ){ + stringAppend(p, z, i+1); + stringAppend(p, "'", 1); + z += i+1; + }else{ + stringAppend(p, z, i); + break; + } + } + stringAppend(p, "'", 1); +} + +/* +** Callback function for evalSql() +*/ +static int evalCallback(void *pCData, int argc, char **argv, char **azCol){ + String *p = (String*)pCData; + int i; + UNUSED_PARAMETER(azCol); + for(i=0; i0 ); + rc = sqlite3_exec(g.db, zSql, evalCallback, p, &zErrMsg); + sqlite3_free(zSql); + if( rc ){ + char zErr[30]; + sqlite3_snprintf(sizeof(zErr), zErr, "error(%d)", rc); + stringAppendTerm(p, zErr); + if( zErrMsg ){ + stringAppendTerm(p, zErrMsg); + sqlite3_free(zErrMsg); + } + } + return rc; +} + +/* +** Auxiliary SQL function to recursively evaluate SQL. +*/ +static void evalFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3 *db = sqlite3_context_db_handle(context); + const char *zSql = (const char*)sqlite3_value_text(argv[0]); + String res; + char *zErrMsg = 0; + int rc; + UNUSED_PARAMETER(argc); + memset(&res, 0, sizeof(res)); + rc = sqlite3_exec(db, zSql, evalCallback, &res, &zErrMsg); + if( zErrMsg ){ + sqlite3_result_error(context, zErrMsg, -1); + sqlite3_free(zErrMsg); + }else if( rc ){ + sqlite3_result_error_code(context, rc); + }else{ + sqlite3_result_text(context, res.z, -1, SQLITE_TRANSIENT); + } + stringFree(&res); +} + +/* +** Look up the next task for client iClient in the database. +** Return the task script and the task number and mark that +** task as being under way. +*/ +static int startScript( + int iClient, /* The client number */ + char **pzScript, /* Write task script here */ + int *pTaskId, /* Write task number here */ + char **pzTaskName /* Name of the task */ +){ + sqlite3_stmt *pStmt = 0; + int taskId; + int rc; + int totalTime = 0; + + *pzScript = 0; + g.iTimeout = 0; + while(1){ + rc = trySql("BEGIN IMMEDIATE"); + if( rc==SQLITE_BUSY ){ + sqlite3_sleep(10); + totalTime += 10; + continue; + } + if( rc!=SQLITE_OK ){ + fatalError("in startScript: %s", sqlite3_errmsg(g.db)); + } + if( g.nError || g.nTest ){ + runSql("UPDATE counters SET nError=nError+%d, nTest=nTest+%d", + g.nError, g.nTest); + g.nError = 0; + g.nTest = 0; + } + pStmt = prepareSql("SELECT 1 FROM client WHERE id=%d AND wantHalt",iClient); + rc = sqlite3_step(pStmt); + sqlite3_finalize(pStmt); + if( rc==SQLITE_ROW ){ + runSql("DELETE FROM client WHERE id=%d", iClient); + g.iTimeout = DEFAULT_TIMEOUT; + runSql("COMMIT TRANSACTION;"); + return SQLITE_DONE; + } + pStmt = prepareSql( + "SELECT script, id, name FROM task" + " WHERE client=%d AND starttime IS NULL" + " ORDER BY id LIMIT 1", iClient); + rc = sqlite3_step(pStmt); + if( rc==SQLITE_ROW ){ + int n = sqlite3_column_bytes(pStmt, 0); + *pzScript = sqlite3_malloc(n+1); + strcpy(*pzScript, (const char*)sqlite3_column_text(pStmt, 0)); + *pTaskId = taskId = sqlite3_column_int(pStmt, 1); + *pzTaskName = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 2)); + sqlite3_finalize(pStmt); + runSql("UPDATE task" + " SET starttime=strftime('%%Y-%%m-%%d %%H:%%M:%%f','now')" + " WHERE id=%d;", taskId); + g.iTimeout = DEFAULT_TIMEOUT; + runSql("COMMIT TRANSACTION;"); + return SQLITE_OK; + } + sqlite3_finalize(pStmt); + if( rc==SQLITE_DONE ){ + if( totalTime>30000 ){ + errorMessage("Waited over 30 seconds with no work. Giving up."); + runSql("DELETE FROM client WHERE id=%d; COMMIT;", iClient); + sqlite3_close(g.db); + exit(1); + } + while( trySql("COMMIT")==SQLITE_BUSY ){ + sqlite3_sleep(10); + totalTime += 10; + } + sqlite3_sleep(100); + totalTime += 100; + continue; + } + fatalError("%s", sqlite3_errmsg(g.db)); + } + g.iTimeout = DEFAULT_TIMEOUT; +} + +/* +** Mark a script as having finished. Remove the CLIENT table entry +** if bShutdown is true. +*/ +static int finishScript(int iClient, int taskId, int bShutdown){ + runSql("UPDATE task" + " SET endtime=strftime('%%Y-%%m-%%d %%H:%%M:%%f','now')" + " WHERE id=%d;", taskId); + if( bShutdown ){ + runSql("DELETE FROM client WHERE id=%d", iClient); + } + return SQLITE_OK; +} + +/* +** Start up a client process for iClient, if it is not already +** running. If the client is already running, then this routine +** is a no-op. +*/ +static void startClient(int iClient){ + runSql("INSERT OR IGNORE INTO client VALUES(%d,0)", iClient); + if( sqlite3_changes(g.db) ){ + char *zSys; + int rc; + zSys = sqlite3_mprintf("%s \"%s\" --client %d --trace %d", + g.argv0, g.zDbFile, iClient, g.iTrace); + if( g.bSqlTrace ){ + zSys = sqlite3_mprintf("%z --sqltrace", zSys); + } + if( g.bSync ){ + zSys = sqlite3_mprintf("%z --sync", zSys); + } + if( g.zVfs ){ + zSys = sqlite3_mprintf("%z --vfs \"%s\"", zSys, g.zVfs); + } + if( g.iTrace>=2 ) logMessage("system('%q')", zSys); +#if !defined(_WIN32) + zSys = sqlite3_mprintf("%z &", zSys); + rc = system(zSys); + if( rc ) errorMessage("system() fails with error code %d", rc); +#else + { + STARTUPINFOA startupInfo; + PROCESS_INFORMATION processInfo; + memset(&startupInfo, 0, sizeof(startupInfo)); + startupInfo.cb = sizeof(startupInfo); + memset(&processInfo, 0, sizeof(processInfo)); + rc = CreateProcessA(NULL, zSys, NULL, NULL, FALSE, 0, NULL, NULL, + &startupInfo, &processInfo); + if( rc ){ + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + }else{ + errorMessage("CreateProcessA() fails with error code %lu", + GetLastError()); + } + } +#endif + sqlite3_free(zSys); + } +} + +/* +** Read the entire content of a file into memory +*/ +static char *readFile(const char *zFilename){ + FILE *in = fopen(zFilename, "rb"); + long sz; + char *z; + if( in==0 ){ + fatalError("cannot open \"%s\" for reading", zFilename); + } + fseek(in, 0, SEEK_END); + sz = ftell(in); + rewind(in); + z = sqlite3_malloc( sz+1 ); + sz = (long)fread(z, 1, sz, in); + z[sz] = 0; + fclose(in); + return z; +} + +/* +** Return the length of the next token. +*/ +static int tokenLength(const char *z, int *pnLine){ + int n = 0; + if( ISSPACE(z[0]) || (z[0]=='/' && z[1]=='*') ){ + int inC = 0; + int c; + if( z[0]=='/' ){ + inC = 1; + n = 2; + } + while( (c = z[n++])!=0 ){ + if( c=='\n' ) (*pnLine)++; + if( ISSPACE(c) ) continue; + if( inC && c=='*' && z[n]=='/' ){ + n++; + inC = 0; + }else if( !inC && c=='/' && z[n]=='*' ){ + n++; + inC = 1; + }else if( !inC ){ + break; + } + } + n--; + }else if( z[0]=='-' && z[1]=='-' ){ + for(n=2; z[n] && z[n]!='\n'; n++){} + if( z[n] ){ (*pnLine)++; n++; } + }else if( z[0]=='"' || z[0]=='\'' ){ + int delim = z[0]; + for(n=1; z[n]; n++){ + if( z[n]=='\n' ) (*pnLine)++; + if( z[n]==delim ){ + n++; + if( z[n+1]!=delim ) break; + } + } + }else{ + int c; + for(n=1; (c = z[n])!=0 && !ISSPACE(c) && c!='"' && c!='\'' && c!=';'; n++){} + } + return n; +} + +/* +** Copy a single token into a string buffer. +*/ +static int extractToken(const char *zIn, int nIn, char *zOut, int nOut){ + int i; + if( nIn<=0 ){ + zOut[0] = 0; + return 0; + } + for(i=0; i0 ){ + pStmt = prepareSql( + "SELECT 1 FROM task" + " WHERE client=%d" + " AND client IN (SELECT id FROM client)" + " AND endtime IS NULL", + iClient); + }else{ + pStmt = prepareSql( + "SELECT 1 FROM task" + " WHERE client IN (SELECT id FROM client)" + " AND endtime IS NULL"); + } + g.iTimeout = 0; + while( ((rc = sqlite3_step(pStmt))==SQLITE_BUSY || rc==SQLITE_ROW) + && iTimeout>0 + ){ + sqlite3_reset(pStmt); + sqlite3_sleep(50); + iTimeout -= 50; + } + sqlite3_finalize(pStmt); + g.iTimeout = DEFAULT_TIMEOUT; + if( rc!=SQLITE_DONE ){ + if( zErrPrefix==0 ) zErrPrefix = ""; + if( iClient>0 ){ + errorMessage("%stimeout waiting for client %d", zErrPrefix, iClient); + }else{ + errorMessage("%stimeout waiting for all clients", zErrPrefix); + } + } +} + +/* Return a pointer to the tail of a filename +*/ +static char *filenameTail(char *z){ + int i, j; + for(i=j=0; z[i]; i++) if( isDirSep(z[i]) ) j = i+1; + return z+j; +} + +/* +** Interpret zArg as a boolean value. Return either 0 or 1. +*/ +static int booleanValue(char *zArg){ + int i; + if( zArg==0 ) return 0; + for(i=0; zArg[i]>='0' && zArg[i]<='9'; i++){} + if( i>0 && zArg[i]==0 ) return atoi(zArg); + if( sqlite3_stricmp(zArg, "on")==0 || sqlite3_stricmp(zArg,"yes")==0 ){ + return 1; + } + if( sqlite3_stricmp(zArg, "off")==0 || sqlite3_stricmp(zArg,"no")==0 ){ + return 0; + } + errorMessage("unknown boolean: [%s]", zArg); + return 0; +} + + +/* This routine exists as a convenient place to set a debugger +** breakpoint. +*/ +static void test_breakpoint(void){ static volatile int cnt = 0; cnt++; } + +/* Maximum number of arguments to a --command */ +#define MX_ARG 2 + +/* +** Run a script. +*/ +static void runScript( + int iClient, /* The client number, or 0 for the master */ + int taskId, /* The task ID for clients. 0 for master */ + char *zScript, /* Text of the script */ + char *zFilename /* File from which script was read. */ +){ + int lineno = 1; + int prevLine = 1; + int ii = 0; + int iBegin = 0; + int n, c, j; + int len; + int nArg; + String sResult; + char zCmd[30]; + char zError[1000]; + char azArg[MX_ARG][100]; + + memset(&sResult, 0, sizeof(sResult)); + stringReset(&sResult); + while( (c = zScript[ii])!=0 ){ + prevLine = lineno; + len = tokenLength(zScript+ii, &lineno); + if( ISSPACE(c) || (c=='/' && zScript[ii+1]=='*') ){ + ii += len; + continue; + } + if( c!='-' || zScript[ii+1]!='-' || !isalpha(zScript[ii+2]) ){ + ii += len; + continue; + } + + /* Run any prior SQL before processing the new --command */ + if( ii>iBegin ){ + char *zSql = sqlite3_mprintf("%.*s", ii-iBegin, zScript+iBegin); + evalSql(&sResult, zSql); + sqlite3_free(zSql); + iBegin = ii + len; + } + + /* Parse the --command */ + if( g.iTrace>=2 ) logMessage("%.*s", len, zScript+ii); + n = extractToken(zScript+ii+2, len-2, zCmd, sizeof(zCmd)); + for(nArg=0; n=len-2 ) break; + n += extractToken(zScript+ii+2+n, len-2-n, + azArg[nArg], sizeof(azArg[nArg])); + } + for(j=nArg; j0 then exit without shutting down + ** SQLite. (In other words, simulate a crash.) + */ + if( strcmp(zCmd, "exit")==0 ){ + int rc = atoi(azArg[0]); + finishScript(iClient, taskId, 1); + if( rc==0 ) sqlite3_close(g.db); + exit(rc); + }else + + /* + ** --testcase NAME + ** + ** Begin a new test case. Announce in the log that the test case + ** has begun. + */ + if( strcmp(zCmd, "testcase")==0 ){ + if( g.iTrace==1 ) logMessage("%.*s", len - 1, zScript+ii); + stringReset(&sResult); + }else + + /* + ** --finish + ** + ** Mark the current task as having finished, even if it is not. + ** This can be used in conjunction with --exit to simulate a crash. + */ + if( strcmp(zCmd, "finish")==0 && iClient>0 ){ + finishScript(iClient, taskId, 1); + }else + + /* + ** --reset + ** + ** Reset accumulated results back to an empty string + */ + if( strcmp(zCmd, "reset")==0 ){ + stringReset(&sResult); + }else + + /* + ** --match ANSWER... + ** + ** Check to see if output matches ANSWER. Report an error if not. + */ + if( strcmp(zCmd, "match")==0 ){ + int jj; + char *zAns = zScript+ii; + for(jj=7; jj=0 && !isDirSep(zFilename[k]); k--){} + if( k>0 ){ + zNewFile = zToDel = sqlite3_mprintf("%.*s/%s", k,zFilename,zNewFile); + } + } + zNewScript = readFile(zNewFile); + if( g.iTrace ) logMessage("begin script [%s]\n", zNewFile); + runScript(0, 0, zNewScript, zNewFile); + sqlite3_free(zNewScript); + if( g.iTrace ) logMessage("end script [%s]\n", zNewFile); + sqlite3_free(zToDel); + }else + + /* + ** --print MESSAGE.... + ** + ** Output the remainder of the line to the log file + */ + if( strcmp(zCmd, "print")==0 ){ + int jj; + for(jj=7; jj0 ){ + startClient(iNewClient); + } + }else + + /* + ** --wait CLIENT TIMEOUT + ** + ** Wait until all tasks complete for the given client. If CLIENT is + ** "all" then wait for all clients to complete. Wait no longer than + ** TIMEOUT milliseconds (default 10,000) + */ + if( strcmp(zCmd, "wait")==0 && iClient==0 ){ + int iTimeout = nArg>=2 ? atoi(azArg[1]) : 10000; + sqlite3_snprintf(sizeof(zError),zError,"line %d of %s\n", + prevLine, zFilename); + waitForClient(atoi(azArg[0]), iTimeout, zError); + }else + + /* + ** --task CLIENT + ** + ** --end + ** + ** Assign work to a client. Start the client if it is not running + ** already. + */ + if( strcmp(zCmd, "task")==0 && iClient==0 ){ + int iTarget = atoi(azArg[0]); + int iEnd; + char *zTask; + char *zTName; + iEnd = findEnd(zScript+ii+len, &lineno); + if( iTarget<0 ){ + errorMessage("line %d of %s: bad client number: %d", + prevLine, zFilename, iTarget); + }else{ + zTask = sqlite3_mprintf("%.*s", iEnd, zScript+ii+len); + if( nArg>1 ){ + zTName = sqlite3_mprintf("%s", azArg[1]); + }else{ + zTName = sqlite3_mprintf("%s:%d", filenameTail(zFilename), prevLine); + } + startClient(iTarget); + runSql("INSERT INTO task(client,script,name)" + " VALUES(%d,'%q',%Q)", iTarget, zTask, zTName); + sqlite3_free(zTask); + sqlite3_free(zTName); + } + iEnd += tokenLength(zScript+ii+len+iEnd, &lineno); + len += iEnd; + iBegin = ii+len; + }else + + /* + ** --breakpoint + ** + ** This command calls "test_breakpoint()" which is a routine provided + ** as a convenient place to set a debugger breakpoint. + */ + if( strcmp(zCmd, "breakpoint")==0 ){ + test_breakpoint(); + }else + + /* + ** --show-sql-errors BOOLEAN + ** + ** Turn display of SQL errors on and off. + */ + if( strcmp(zCmd, "show-sql-errors")==0 ){ + g.bIgnoreSqlErrors = nArg>=1 ? !booleanValue(azArg[0]) : 1; + }else + + + /* error */{ + errorMessage("line %d of %s: unknown command --%s", + prevLine, zFilename, zCmd); + } + ii += len; + } + if( iBegin= nArg ) break; + z = azArg[i]; + if( z[0]!='-' ) continue; + z++; + if( z[0]=='-' ){ + if( z[1]==0 ) break; + z++; + } + if( strcmp(z,zOption)==0 ){ + if( hasArg && i==nArg-1 ){ + fatalError("command-line option \"--%s\" requires an argument", z); + } + if( hasArg ){ + zReturn = azArg[i+1]; + }else{ + zReturn = azArg[i]; + } + j = i+1+(hasArg!=0); + while( j0 ){ + printf("BEGIN: %s", argv[0]); + for(i=1; i5 ? "still " : "", g.zDbFile); + rc = unlink(g.zDbFile); + if( rc && errno==ENOENT ) rc = 0; + }while( rc!=0 && (++nTry)<60 && sqlite3_sleep(1000)>0 ); + if( rc!=0 ){ + fatalError("unable to unlink '%s' after %d attempts\n", + g.zDbFile, nTry); + } + openFlags |= SQLITE_OPEN_CREATE; + } + rc = sqlite3_open_v2(g.zDbFile, &g.db, openFlags, g.zVfs); + if( rc ) fatalError("cannot open [%s]", g.zDbFile); + if( iTmout>0 ) sqlite3_busy_timeout(g.db, iTmout); + + if( zJMode ){ +#if defined(_WIN32) + if( sqlite3_stricmp(zJMode,"persist")==0 + || sqlite3_stricmp(zJMode,"truncate")==0 + ){ + printf("Changing journal mode to DELETE from %s", zJMode); + zJMode = "DELETE"; + } +#endif + runSql("PRAGMA journal_mode=%Q;", zJMode); + } + if( !g.bSync ) trySql("PRAGMA synchronous=OFF"); + sqlite3_enable_load_extension(g.db, 1); + sqlite3_busy_handler(g.db, busyHandler, 0); + sqlite3_create_function(g.db, "vfsname", 0, SQLITE_UTF8, 0, + vfsNameFunc, 0, 0); + sqlite3_create_function(g.db, "eval", 1, SQLITE_UTF8, 0, + evalFunc, 0, 0); + g.iTimeout = DEFAULT_TIMEOUT; + if( g.bSqlTrace ) sqlite3_trace(g.db, sqlTraceCallback, 0); + if( iClient>0 ){ + if( n>0 ) unrecognizedArguments(argv[0], n, argv+2); + if( g.iTrace ) logMessage("start-client"); + while(1){ + char *zTaskName = 0; + rc = startScript(iClient, &zScript, &taskId, &zTaskName); + if( rc==SQLITE_DONE ) break; + if( g.iTrace ) logMessage("begin %s (%d)", zTaskName, taskId); + runScript(iClient, taskId, zScript, zTaskName); + if( g.iTrace ) logMessage("end %s (%d)", zTaskName, taskId); + finishScript(iClient, taskId, 0); + sqlite3_free(zTaskName); + sqlite3_sleep(10); + } + if( g.iTrace ) logMessage("end-client"); + }else{ + sqlite3_stmt *pStmt; + int iTimeout; + if( n==0 ){ + fatalError("missing script filename"); + } + if( n>1 ) unrecognizedArguments(argv[0], n, argv+2); + runSql( + "DROP TABLE IF EXISTS task;\n" + "DROP TABLE IF EXISTS counters;\n" + "DROP TABLE IF EXISTS client;\n" + "CREATE TABLE task(\n" + " id INTEGER PRIMARY KEY,\n" + " name TEXT,\n" + " client INTEGER,\n" + " starttime DATE,\n" + " endtime DATE,\n" + " script TEXT\n" + ");" + "CREATE INDEX task_i1 ON task(client, starttime);\n" + "CREATE INDEX task_i2 ON task(client, endtime);\n" + "CREATE TABLE counters(nError,nTest);\n" + "INSERT INTO counters VALUES(0,0);\n" + "CREATE TABLE client(id INTEGER PRIMARY KEY, wantHalt);\n" + ); + zScript = readFile(argv[2]); + for(iRep=1; iRep<=nRep; iRep++){ + if( g.iTrace ) logMessage("begin script [%s] cycle %d\n", argv[2], iRep); + runScript(0, 0, zScript, argv[2]); + if( g.iTrace ) logMessage("end script [%s] cycle %d\n", argv[2], iRep); + } + sqlite3_free(zScript); + waitForClient(0, 2000, "during shutdown...\n"); + trySql("UPDATE client SET wantHalt=1"); + sqlite3_sleep(10); + g.iTimeout = 0; + iTimeout = 1000; + while( ((rc = trySql("SELECT 1 FROM client"))==SQLITE_BUSY + || rc==SQLITE_ROW) && iTimeout>0 ){ + sqlite3_sleep(10); + iTimeout -= 10; + } + sqlite3_sleep(100); + pStmt = prepareSql("SELECT nError, nTest FROM counters"); + iTimeout = 1000; + while( (rc = sqlite3_step(pStmt))==SQLITE_BUSY && iTimeout>0 ){ + sqlite3_sleep(10); + iTimeout -= 10; + } + if( rc==SQLITE_ROW ){ + g.nError += sqlite3_column_int(pStmt, 0); + g.nTest += sqlite3_column_int(pStmt, 1); + } + sqlite3_finalize(pStmt); + } + sqlite3_close(g.db); + maybeClose(g.pLog); + maybeClose(g.pErrLog); + if( iClient==0 ){ + printf("Summary: %d errors out of %d tests\n", g.nError, g.nTest); + printf("END: %s", argv[0]); + for(i=1; i0; +} diff --git a/packages/wa-sqlite-driver/test/src/mptest/multiwrite01.test b/packages/wa-sqlite-driver/test/src/mptest/multiwrite01.test new file mode 100644 index 0000000..7062ae0 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest/multiwrite01.test @@ -0,0 +1,415 @@ +/* +** This script sets up five different tasks all writing and updating +** the database at the same time, but each in its own table. +*/ +--task 1 build-t1 + DROP TABLE IF EXISTS t1; + CREATE TABLE t1(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t1 VALUES(1, randomblob(2000)); + INSERT INTO t1 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t1 SELECT a+2, randomblob(1500) FROM t1; + INSERT INTO t1 SELECT a+4, randomblob(1500) FROM t1; + INSERT INTO t1 SELECT a+8, randomblob(1500) FROM t1; + --sleep 1 + INSERT INTO t1 SELECT a+16, randomblob(1500) FROM t1; + --sleep 1 + INSERT INTO t1 SELECT a+32, randomblob(1500) FROM t1; + SELECT count(*) FROM t1; + --match 64 + SELECT avg(length(b)) FROM t1; + --match 1500.0 + --sleep 2 + UPDATE t1 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t1; + --match 247 + SELECT a FROM t1 WHERE b='x17y'; + --match 17 + CREATE INDEX t1b ON t1(b); + SELECT a FROM t1 WHERE b='x17y'; + --match 17 + SELECT a FROM t1 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + + +--task 2 build-t2 + DROP TABLE IF EXISTS t2; + CREATE TABLE t2(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t2 VALUES(1, randomblob(2000)); + INSERT INTO t2 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t2 SELECT a+2, randomblob(1500) FROM t2; + INSERT INTO t2 SELECT a+4, randomblob(1500) FROM t2; + INSERT INTO t2 SELECT a+8, randomblob(1500) FROM t2; + --sleep 1 + INSERT INTO t2 SELECT a+16, randomblob(1500) FROM t2; + --sleep 1 + INSERT INTO t2 SELECT a+32, randomblob(1500) FROM t2; + SELECT count(*) FROM t2; + --match 64 + SELECT avg(length(b)) FROM t2; + --match 1500.0 + --sleep 2 + UPDATE t2 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t2; + --match 247 + SELECT a FROM t2 WHERE b='x17y'; + --match 17 + CREATE INDEX t2b ON t2(b); + SELECT a FROM t2 WHERE b='x17y'; + --match 17 + SELECT a FROM t2 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + +--task 3 build-t3 + DROP TABLE IF EXISTS t3; + CREATE TABLE t3(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t3 VALUES(1, randomblob(2000)); + INSERT INTO t3 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t3 SELECT a+2, randomblob(1500) FROM t3; + INSERT INTO t3 SELECT a+4, randomblob(1500) FROM t3; + INSERT INTO t3 SELECT a+8, randomblob(1500) FROM t3; + --sleep 1 + INSERT INTO t3 SELECT a+16, randomblob(1500) FROM t3; + --sleep 1 + INSERT INTO t3 SELECT a+32, randomblob(1500) FROM t3; + SELECT count(*) FROM t3; + --match 64 + SELECT avg(length(b)) FROM t3; + --match 1500.0 + --sleep 2 + UPDATE t3 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t3; + --match 247 + SELECT a FROM t3 WHERE b='x17y'; + --match 17 + CREATE INDEX t3b ON t3(b); + SELECT a FROM t3 WHERE b='x17y'; + --match 17 + SELECT a FROM t3 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + +--task 4 build-t4 + DROP TABLE IF EXISTS t4; + CREATE TABLE t4(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t4 VALUES(1, randomblob(2000)); + INSERT INTO t4 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t4 SELECT a+2, randomblob(1500) FROM t4; + INSERT INTO t4 SELECT a+4, randomblob(1500) FROM t4; + INSERT INTO t4 SELECT a+8, randomblob(1500) FROM t4; + --sleep 1 + INSERT INTO t4 SELECT a+16, randomblob(1500) FROM t4; + --sleep 1 + INSERT INTO t4 SELECT a+32, randomblob(1500) FROM t4; + SELECT count(*) FROM t4; + --match 64 + SELECT avg(length(b)) FROM t4; + --match 1500.0 + --sleep 2 + UPDATE t4 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t4; + --match 247 + SELECT a FROM t4 WHERE b='x17y'; + --match 17 + CREATE INDEX t4b ON t4(b); + SELECT a FROM t4 WHERE b='x17y'; + --match 17 + SELECT a FROM t4 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + +--task 5 build-t5 + DROP TABLE IF EXISTS t5; + CREATE TABLE t5(a INTEGER PRIMARY KEY, b); + --sleep 1 + INSERT INTO t5 VALUES(1, randomblob(2000)); + INSERT INTO t5 VALUES(2, randomblob(1000)); + --sleep 1 + INSERT INTO t5 SELECT a+2, randomblob(1500) FROM t5; + INSERT INTO t5 SELECT a+4, randomblob(1500) FROM t5; + INSERT INTO t5 SELECT a+8, randomblob(1500) FROM t5; + --sleep 1 + INSERT INTO t5 SELECT a+16, randomblob(1500) FROM t5; + --sleep 1 + INSERT INTO t5 SELECT a+32, randomblob(1500) FROM t5; + SELECT count(*) FROM t5; + --match 64 + SELECT avg(length(b)) FROM t5; + --match 1500.0 + --sleep 2 + UPDATE t5 SET b='x'||a||'y'; + SELECT sum(length(b)) FROM t5; + --match 247 + SELECT a FROM t5 WHERE b='x17y'; + --match 17 + CREATE INDEX t5b ON t5(b); + SELECT a FROM t5 WHERE b='x17y'; + --match 17 + SELECT a FROM t5 WHERE b GLOB 'x2?y' ORDER BY b DESC LIMIT 5; + --match 29 28 27 26 25 +--end + +--wait all +SELECT count(*), sum(length(b)) FROM t1; +--match 64 247 +SELECT count(*), sum(length(b)) FROM t2; +--match 64 247 +SELECT count(*), sum(length(b)) FROM t3; +--match 64 247 +SELECT count(*), sum(length(b)) FROM t4; +--match 64 247 +SELECT count(*), sum(length(b)) FROM t5; +--match 64 247 + +--task 1 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 5 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 3 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 2 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 4 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--wait all + +--task 5 + DROP INDEX t5b; + --sleep 5 + PRAGMA integrity_check(10); + --match ok + CREATE INDEX t5b ON t5(b DESC); +--end +--task 3 + DROP INDEX t3b; + --sleep 5 + PRAGMA integrity_check(10); + --match ok + CREATE INDEX t3b ON t3(b DESC); +--end +--task 1 + DROP INDEX t1b; + --sleep 5 + PRAGMA integrity_check(10); + --match ok + CREATE INDEX t1b ON t1(b DESC); +--end +--task 2 + DROP INDEX t2b; + --sleep 5 + PRAGMA integrity_check(10); + --match ok + CREATE INDEX t2b ON t2(b DESC); +--end +--task 4 + DROP INDEX t4b; + --sleep 5 + PRAGMA integrity_check(10); + --match ok + CREATE INDEX t4b ON t4(b DESC); +--end +--wait all + +--task 1 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 5 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 3 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 2 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--task 4 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 +--end +--wait all + +VACUUM; +PRAGMA integrity_check(10); +--match ok + +--task 1 + UPDATE t1 SET b=randomblob(20000); + --sleep 5 + UPDATE t1 SET b='x'||a||'y'; + SELECT a FROM t1 WHERE b='x63y'; + --match 63 +--end +--task 2 + UPDATE t2 SET b=randomblob(20000); + --sleep 5 + UPDATE t2 SET b='x'||a||'y'; + SELECT a FROM t2 WHERE b='x63y'; + --match 63 +--end +--task 3 + UPDATE t3 SET b=randomblob(20000); + --sleep 5 + UPDATE t3 SET b='x'||a||'y'; + SELECT a FROM t3 WHERE b='x63y'; + --match 63 +--end +--task 4 + UPDATE t4 SET b=randomblob(20000); + --sleep 5 + UPDATE t4 SET b='x'||a||'y'; + SELECT a FROM t4 WHERE b='x63y'; + --match 63 +--end +--task 5 + UPDATE t5 SET b=randomblob(20000); + --sleep 5 + UPDATE t5 SET b='x'||a||'y'; + SELECT a FROM t5 WHERE b='x63y'; + --match 63 +--end +--wait all + +--task 1 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 + PRAGMA integrity_check; + --match ok +--end +--task 5 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 + PRAGMA integrity_check; + --match ok +--end +--task 3 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 + PRAGMA integrity_check; + --match ok +--end +--task 2 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 + PRAGMA integrity_check; + --match ok +--end +--task 4 + SELECT t1.a FROM t1, t2 + WHERE t2.b GLOB 'x3?y' AND t1.b=('x'||(t2.a+3)||'y') + ORDER BY t1.a LIMIT 4 + --match 33 34 35 36 + SELECT t3.a FROM t3, t4 + WHERE t4.b GLOB 'x4?y' AND t3.b=('x'||(t4.a+5)||'y') + ORDER BY t3.a LIMIT 7 + --match 45 46 47 48 49 50 51 + PRAGMA integrity_check; + --match ok +--end +--wait all From f3b5709ce92cee97d3620e8fc8e5dfa00f3dfbd4 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 20:54:32 -0600 Subject: [PATCH 18/25] First mptest passing. --- .../wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts | 39 ++-- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 2 - .../test/src/mptest-runner.test.ts | 170 ++++++++++++++++-- 3 files changed, 180 insertions(+), 31 deletions(-) diff --git a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts index ed0ef11..e7a6fe1 100644 --- a/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts +++ b/packages/wa-sqlite-driver/src/OPFSCoopSyncVFS2.ts @@ -92,6 +92,15 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { return (this as unknown as WithModule)._module; } + get #accessHandleOptions() { + return { mode: this.readonly ? 'read-only' : 'readwrite' }; + } + + get #lockOptions() { + return { mode: this.readonly ? 'shared' : 'exclusive' } as const; + // return { mode: 'shared' as const }; + } + async #initialize(nTemporaryFiles) { // Delete temporary directories no longer in use. const root = await navigator.storage.getDirectory(); @@ -130,9 +139,9 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { // Populate temporary directory. for (let i = 0; i < nTemporaryFiles; i++) { const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true }); - const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle({ - mode: this.readonly ? 'read-only' : 'readwrite' - }); + const tmpAccessHandle = await (tmpFile as any).createSyncAccessHandle( + this.#accessHandleOptions + ); this.unboundAccessHandles.add(tmpAccessHandle); } } @@ -531,9 +540,7 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { if (subPersistentFile) { subPersistentFile.accessHandle = await ( subPersistentFile.fileHandle as any - ).createSyncAccessHandle({ - mode: this.readonly ? 'read-only' : 'readwrite' - }); + ).createSyncAccessHandle(this.#accessHandleOptions); } }) ); @@ -574,18 +581,14 @@ export class OPFSCoopSyncVFS2 extends FacadeVFS { setTimeout(notify); this.log?.(`lock requested: ${lockName}`); - navigator.locks.request( - lockName, - { mode: this.readonly ? 'shared' : 'exclusive' }, - (lock) => { - // We have the lock. Stop asking other connections for it. - this.log?.(`lock acquired: ${lockName}`, lock); - clearInterval(notifyId); - return new Promise<() => void>((res) => { - resolve(res as () => void); - }); - } - ); + navigator.locks.request(lockName, this.#lockOptions, (lock) => { + // We have the lock. Stop asking other connections for it. + this.log?.(`lock acquired: ${lockName}`, lock); + clearInterval(notifyId); + return new Promise<() => void>((res) => { + resolve(res as () => void); + }); + }); }); } } diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index b2abb86..b26321a 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -190,7 +190,6 @@ class StatementImpl implements SqliteDriverStatement { } async _finalize() { - console.log('finalizing...', this.con.path); // Wait for these to complete, but ignore any errors. // TODO: also wait for run/step to complete await this.preparePromise; @@ -278,7 +277,6 @@ export class WaSqliteConnection implements SqliteDriverConnection { async close() { await m.runExclusive(async () => { - console.log('closing...', this.path); for (let statement of this.statements) { if (statement.options.persist) { statement.finalize(); diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts index a9c30bf..66b89c8 100644 --- a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -8,7 +8,7 @@ import type { import { waSqliteSingleWorker } from '../../lib/index.js'; -const scriptModules = (import.meta as any).glob('./mptest/**/*test', { +const scriptModules = import.meta.glob('./mptest/**/*', { as: 'raw', eager: true }); @@ -68,6 +68,13 @@ interface ClientContext { pending: Set>; } +interface TaskControl { + promise: Promise; + resolve: () => void; + reject: (err: unknown) => void; + isResolved: () => boolean; +} + class MptestRunner { private clients = new Map(); private pendingTasks = new Set>(); @@ -142,6 +149,32 @@ class MptestRunner { }); } + private createTaskControl(): TaskControl { + let resolved = false; + let resolveFn: () => void = () => {}; + let rejectFn: (err: unknown) => void = () => {}; + const promise = new Promise((resolve, reject) => { + resolveFn = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; + rejectFn = (err) => { + if (!resolved) { + resolved = true; + reject(err); + } + }; + }); + return { + promise, + resolve: resolveFn, + reject: rejectFn, + isResolved: () => resolved + }; + } + private async scheduleTask( parent: ScriptContext, clientId: number, @@ -150,6 +183,8 @@ class MptestRunner { taskLabel: string ): Promise { const client = await this.ensureClient(clientId); + const taskControl = this.createTaskControl(); + this.trackTask(client, taskControl.promise); const run = async () => { await this.runScriptInternal( { @@ -159,12 +194,19 @@ class MptestRunner { displayName: `${parent.displayName}#client${clientId}:${taskLabel}` }, script, - startLine + startLine, + taskControl ); + taskControl.resolve(); }; - const task = client.queue.then(run); + const task = client.queue.then(run).catch((err) => { + if (taskControl.isResolved()) { + return; + } + taskControl.reject(err); + throw err; + }); client.queue = task.catch(() => {}); - this.trackTask(client, task); } private async waitForAll(timeoutMs: number): Promise { @@ -221,7 +263,8 @@ class MptestRunner { private async runScriptInternal( context: ScriptContext, script: string, - initialLine: number + initialLine: number, + taskControl?: TaskControl ): Promise { const result = new ResultBuffer(); let index = 0; @@ -265,6 +308,10 @@ class MptestRunner { throw new Error( `${context.displayName}:${prevLine} expected [${expectedRaw}] but got [${result.toString()}]` ); + } else { + console.log( + `${context.displayName}:${prevLine} expected [${expectedRaw}] matched [${result.toString()}]` + ); } result.reset(); break; @@ -295,6 +342,10 @@ class MptestRunner { ); break; } + case 'finish': { + taskControl?.resolve(); + break; + } case 'wait': { const target = command.args[0] ?? 'all'; const timeout = command.args[1] ? Number(command.args[1]) : 10000; @@ -323,7 +374,8 @@ class MptestRunner { displayName: baseName(resolved) }, subScript, - 1 + 1, + taskControl ); break; } @@ -374,8 +426,8 @@ class MptestRunner { if (!rows || rows.length === 0) { return false; } - const row = rows[0] as SqliteRowRaw; - const value = row[0]; + const rawRows = rows as SqliteRowRaw[]; + const value = rawRows[0]?.[0]; if (value == null) { return false; } @@ -404,12 +456,16 @@ class MptestRunner { if (!sql.trim()) { continue; } + if (!hasNonCommentContent(sql)) { + continue; + } const statement = context.connection.prepare(sql, { rawResults: true }); try { const { rows } = await statement.step(); if (rows) { - for (const row of rows) { - for (const value of row as SqliteRowRaw) { + const rawRows = rows as SqliteRowRaw[]; + for (const row of rawRows) { + for (const value of row) { result.append(value); } } @@ -428,7 +484,9 @@ class MptestRunner { } describe('mptest scripts', () => { - for (const script of topLevelScripts) { + // const scripts = topLevelScripts; + const scripts = ['mptest/multiwrite01.test']; + for (const script of scripts) { test(script, async () => { const dbPath = `mptest-${sanitizeForFilename(script)}-${Math.random() .toString(36) @@ -861,6 +919,96 @@ function toHex(data: Uint8Array): string { return hex; } +function hasNonCommentContent(sql: string): boolean { + let i = 0; + let inSingle = false; + let inDouble = false; + let inBracket = false; + + const advance = (step = 1) => { + i += step; + }; + + while (i < sql.length) { + const c = sql[i]; + const next = sql[i + 1]; + + if (inSingle) { + if (c === "'") { + if (next === "'") { + advance(2); + continue; + } + inSingle = false; + advance(); + continue; + } + advance(); + continue; + } + if (inDouble) { + if (c === '"') { + if (next === '"') { + advance(2); + continue; + } + inDouble = false; + advance(); + continue; + } + advance(); + continue; + } + if (inBracket) { + if (c === ']') { + inBracket = false; + } + advance(); + continue; + } + + if (c === "'") { + inSingle = true; + advance(); + continue; + } + if (c === '"') { + inDouble = true; + advance(); + continue; + } + if (c === '[') { + inBracket = true; + advance(); + continue; + } + + if (c === '-' && next === '-') { + advance(2); + while (i < sql.length && sql[i] !== '\n') { + advance(); + } + continue; + } + if (c === '/' && next === '*') { + advance(2); + while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { + advance(); + } + if (i < sql.length) { + advance(2); + } + continue; + } + if (/\s/.test(c ?? '')) { + advance(); + continue; + } + return true; + } + return false; +} + function sleepMs(ms: number): Promise { if (ms <= 0) { return Promise.resolve(); From 428dd6c47719441d26340b994e2f809a2fd392fe Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 21:56:51 -0600 Subject: [PATCH 19/25] More logging; working tests. --- packages/wa-sqlite-driver/package.json | 11 +- packages/wa-sqlite-driver/src/pool.ts | 12 +- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 36 +++- .../test/src/mptest-runner.test.ts | 170 +++++++++++++++++- .../test/src/mptest/config02.test | 40 ++--- pnpm-lock.yaml | 12 +- 6 files changed, 233 insertions(+), 48 deletions(-) diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index a50fc67..527718d 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -17,17 +17,18 @@ "dependencies": { "@journeyapps/wa-sqlite": "^1.3.2", "@sqlite-js/driver": "workspace:^", - "async-mutex": "^0.5.0" + "async-mutex": "^0.5.0", + "wa-sqlite": "^1.0.0" }, "devDependencies": { - "vite-plugin-top-level-await": "^1.4.2", - "vite-plugin-wasm": "^3.3.0", "@sqlite-js/driver-tests": "workspace:^", "@types/node": "^24.9.1", - "typescript": "^5.9.3", "@vitest/browser": "^2.0.5", - "vitest": "^2.0.5", "playwright": "^1.45.3", + "typescript": "^5.9.3", + "vite-plugin-top-level-await": "^1.4.2", + "vite-plugin-wasm": "^3.3.0", + "vitest": "^2.0.5", "webdriverio": "^8.39.1" } } diff --git a/packages/wa-sqlite-driver/src/pool.ts b/packages/wa-sqlite-driver/src/pool.ts index 942ab41..f35dc55 100644 --- a/packages/wa-sqlite-driver/src/pool.ts +++ b/packages/wa-sqlite-driver/src/pool.ts @@ -12,13 +12,17 @@ import { WorkerDriverConnection } from './worker_threads'; export function waSqliteSingleWorker(path: string): SqliteDriverConnectionPool { return new LazyConnectionPool(async () => { - const connection = new WorkerDriverConnection( - new Worker(new URL('./wa-sqlite-worker.js', import.meta.url), { + const worker = new Worker( + new URL('./wa-sqlite-worker.js', import.meta.url), + { type: 'module' - }), - { path } + } ); + const connection = new WorkerDriverConnection(worker, { path }); await connection.open(); + (connection as any).terminate = () => { + worker.terminate(); + }; return connection; }); } diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index e12a386..0c0a688 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -1,22 +1,42 @@ import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; import { sqlite3, module, WaSqliteConnection } from './wa-sqlite-driver'; import { setupDriverWorker } from './worker_threads'; - -let vfs: OPFSCoopSyncVFS2 | null = null; +import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; +import { OPFSAdaptiveVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSAdaptiveVFS.js'; +import { OPFSPermutedVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSPermutedVFS.js'; +let vfs: any | null = null; setupDriverWorker({ async openConnection(options) { // Register a custom file system. if (vfs != null) { throw new Error('Can only open one connection'); } - vfs = await OPFSCoopSyncVFS2.create( - 'test.db', - module, - options.readonly ?? false - ); + // vfs = await OPFSCoopSyncVFS2.create( + // 'test.db', + // module, + // options.readonly ?? false + // ); + // IDBBatchAtomicVFS - breaks hard (database disk image is malformed) + // vfs = await (IDBBatchAtomicVFS as any).create('test.db', module); + // OPFSAdaptiveVFS - works great + // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { + // ifAvailable: true, + // mode: 'shared' + // }); + // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { + // ifAvailable: true, + // mode: 'shared' + // }); + + // database disk image is malformed + vfs = await (OPFSPermutedVFS as any).create('test.db', module); + // @ts-ignore sqlite3.vfs_register(vfs as any, true); - return await WaSqliteConnection.open(options.path, vfs); + const con = await WaSqliteConnection.open(options.path, vfs); + using stmt = await con.prepare('PRAGMA busy_timeout = 10000'); + await stmt.step(); + return con; } }); diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts index 66b89c8..d51477c 100644 --- a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -203,6 +203,7 @@ class MptestRunner { if (taskControl.isResolved()) { return; } + this.logError(`task failure client=${clientId}`, err); taskControl.reject(err); throw err; }); @@ -236,13 +237,15 @@ class MptestRunner { } const waitPromise = Promise.all(promises).then(() => undefined); if (timeoutMs <= 0) { - await waitPromise; + await this.withProgress(waitPromise, label); return; } let handle: ReturnType | undefined; + let progressHandle: ReturnType | undefined; try { + progressHandle = this.createProgressLogger(label, timeoutMs); await Promise.race([ - waitPromise, + this.withProgress(waitPromise, label, progressHandle), new Promise((_, reject) => { handle = setTimeout( () => @@ -257,9 +260,57 @@ class MptestRunner { if (handle) { clearTimeout(handle); } + if (progressHandle) { + clearInterval(progressHandle); + } + } + } + + private createProgressLogger( + label: string, + timeoutMs: number + ): ReturnType { + const start = Date.now(); + return setInterval( + () => { + const elapsed = Date.now() - start; + const pending = this.describePending(); + console.log( + `[wait] ${label} elapsed=${elapsed}ms timeout=${timeoutMs}ms pending=${pending}` + ); + }, + Math.min(1000, Math.max(250, timeoutMs / 10)) + ); + } + + private async withProgress( + promise: Promise, + label: string, + progressHandle?: ReturnType + ): Promise { + try { + await promise; + } finally { + if (progressHandle) { + clearInterval(progressHandle); + } } } + private describePending(): string { + const entries: string[] = []; + for (const [clientId, client] of this.clients.entries()) { + const size = client.pending.size; + if (size > 0) { + entries.push(`client${clientId}:${size}`); + } + } + if (entries.length === 0) { + return 'none'; + } + return entries.join(','); + } + private async runScriptInternal( context: ScriptContext, script: string, @@ -292,10 +343,12 @@ class MptestRunner { } if (index > begin) { const sqlChunk = script.slice(begin, index); + this.logSqlChunk(context, prevLine, sqlChunk); await this.executeSql(context, sqlChunk, result); } let consumed = token.length; const command = parseCommand(script, index, token.length); + this.logCommand(context, prevLine, command.name, command.payload); switch (command.name) { case 'sleep': await sleepMs(Number(command.args[0] ?? '0')); @@ -308,14 +361,17 @@ class MptestRunner { throw new Error( `${context.displayName}:${prevLine} expected [${expectedRaw}] but got [${result.toString()}]` ); - } else { - console.log( - `${context.displayName}:${prevLine} expected [${expectedRaw}] matched [${result.toString()}]` - ); } result.reset(); break; } + case 'print': { + const message = command.payload.replace(/^\s*/, ''); + if (message.length > 0) { + this.logMessage(context, message); + } + break; + } case 'task': { const clientId = Number(command.args[0]); if (!Number.isInteger(clientId) || clientId < 0) { @@ -346,9 +402,15 @@ class MptestRunner { taskControl?.resolve(); break; } + case 'exit': { + const code = Number(command.args[0] ?? '0'); + taskControl?.resolve(); + await this.terminateClient(context, code); + return; + } case 'wait': { const target = command.args[0] ?? 'all'; - const timeout = command.args[1] ? Number(command.args[1]) : 10000; + const timeout = command.args[1] ? Number(command.args[1]) : 30_000; if (target === 'all') { await this.waitForAll(timeout); } else { @@ -411,6 +473,7 @@ class MptestRunner { } if (begin < script.length) { const remainder = script.slice(begin); + this.logSqlChunk(context, line, remainder); await this.executeSql(context, remainder, result); } } @@ -464,6 +527,7 @@ class MptestRunner { const { rows } = await statement.step(); if (rows) { const rawRows = rows as SqliteRowRaw[]; + this.logSqlExecution(context, sql, rawRows); for (const row of rawRows) { for (const value of row) { result.append(value); @@ -472,8 +536,10 @@ class MptestRunner { } } catch (err) { if (isSqliteError(err)) { + this.logError('sqlite error', err); result.appendError(err); } else { + this.logError('sql execution error', err); throw err; } } finally { @@ -481,11 +547,97 @@ class MptestRunner { } } } + + private logMessage(context: ScriptContext, message: string): void { + const trimmed = message.replace(/\s+$/, ''); + if (!trimmed) { + return; + } + console.log(`${context.displayName} ${trimmed}`); + } + + private logCommand( + context: ScriptContext, + line: number, + command: string, + payload: string + ): void { + const detail = payload ? ` ${payload.trim()}` : ''; + console.log(`${context.displayName}:${line} --${command}${detail}`); + } + + private logSqlChunk( + context: ScriptContext, + line: number, + chunk: string + ): void { + const summary = chunk.trim().replace(/\s+/g, ' '); + if (!summary) { + return; + } + console.log(`${context.displayName}:${line} SQL ${summary}`); + } + + private logError(prefix: string, err: unknown): void { + if (err instanceof Error) { + console.error(`[error] ${prefix}: ${err.message}\n${err.stack}`); + } else { + console.error(`[error] ${prefix}: ${String(err)}`); + } + } + + private logSqlExecution( + context: ScriptContext, + placeholder: string, + values: SqliteRowRaw[] + ): void { + console.log( + `${context.displayName} SQL values ${placeholder} => ${JSON.stringify(values)}` + ); + } + + private async terminateClient( + context: ScriptContext, + exitCode: number + ): Promise { + const { clientId, displayName } = context; + const client = this.clients.get(clientId); + if (!client) { + return; + } + this.clients.delete(clientId); + try { + await client.reserved.release(); + } catch (err) { + this.logError(`release error client=${clientId}`, err); + if (exitCode === 0) { + throw err; + } + } + try { + await client.pool.close(); + } catch (err) { + this.logError(`close error client=${clientId}`, err); + if (exitCode === 0) { + throw err; + } + } + if (exitCode !== 0) { + console.log(`${displayName} exited with code ${exitCode}`); + } + } } -describe('mptest scripts', () => { +describe('mptest scripts', { timeout: 60_000 }, () => { // const scripts = topLevelScripts; - const scripts = ['mptest/multiwrite01.test']; + // const scripts = ['mptest/multiwrite01.test']; + // const scripts = ['mptest/config02.test']; + // const scripts = ['mptest/crash01.test']; + const scripts = [ + // 'mptest/multiwrite01.test', + 'mptest/config02.test' + // 'mptest/crash01.test' + ]; for (const script of scripts) { test(script, async () => { const dbPath = `mptest-${sanitizeForFilename(script)}-${Math.random() diff --git a/packages/wa-sqlite-driver/test/src/mptest/config02.test b/packages/wa-sqlite-driver/test/src/mptest/config02.test index 7d4b278..150f285 100644 --- a/packages/wa-sqlite-driver/test/src/mptest/config02.test +++ b/packages/wa-sqlite-driver/test/src/mptest/config02.test @@ -23,19 +23,19 @@ PRAGMA page_size=1024; VACUUM; CREATE TABLE pgsz(taskid, sz INTEGER); --task 1 - INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(1, (select page_size from pragma_page_size())); --end --task 2 - INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(2, (select page_size from pragma_page_size())); --end --task 3 - INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(3, (select page_size from pragma_page_size())); --end --task 4 - INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(4, (select page_size from pragma_page_size())); --end --task 5 - INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(5, (select page_size from pragma_page_size())); --end --source multiwrite01.test --source crash02.subtest @@ -46,19 +46,19 @@ PRAGMA page_size=2048; VACUUM; DELETE FROM pgsz; --task 1 - INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(1, (select page_size from pragma_page_size())); --end --task 2 - INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(2, (select page_size from pragma_page_size())); --end --task 3 - INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(3, (select page_size from pragma_page_size())); --end --task 4 - INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(4, (select page_size from pragma_page_size())); --end --task 5 - INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(5, (select page_size from pragma_page_size())); --end --source multiwrite01.test --source crash02.subtest @@ -69,19 +69,19 @@ PRAGMA page_size=8192; VACUUM; DELETE FROM pgsz; --task 1 - INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(1, (select page_size from pragma_page_size())); --end --task 2 - INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(2, (select page_size from pragma_page_size())); --end --task 3 - INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(3, (select page_size from pragma_page_size())); --end --task 4 - INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(4, (select page_size from pragma_page_size())); --end --task 5 - INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(5, (select page_size from pragma_page_size())); --end --source multiwrite01.test --source crash02.subtest @@ -92,19 +92,19 @@ PRAGMA page_size=16384; VACUUM; DELETE FROM pgsz; --task 1 - INSERT INTO pgsz VALUES(1, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(1, (select page_size from pragma_page_size())); --end --task 2 - INSERT INTO pgsz VALUES(2, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(2, (select page_size from pragma_page_size())); --end --task 3 - INSERT INTO pgsz VALUES(3, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(3, (select page_size from pragma_page_size())); --end --task 4 - INSERT INTO pgsz VALUES(4, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(4, (select page_size from pragma_page_size())); --end --task 5 - INSERT INTO pgsz VALUES(5, eval('PRAGMA page_size')); + INSERT INTO pgsz VALUES(5, (select page_size from pragma_page_size())); --end --source multiwrite01.test --source crash02.subtest diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 830f7e5..7903d8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: async-mutex: specifier: ^0.5.0 version: 0.5.0 + wa-sqlite: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: '@sqlite-js/driver-tests': specifier: workspace:^ @@ -1204,7 +1207,7 @@ packages: engines: {node: '>= 14'} concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -2642,6 +2645,9 @@ packages: jsdom: optional: true + wa-sqlite@1.0.0: + resolution: {integrity: sha512-Kyybo5/BaJp76z7gDWGk2J6Hthl4NIPsE+swgraEjy3IY6r5zIR02wAs1OJH4XtJp1y3puj3Onp5eMGS0z7nUA==} + wait-port@1.1.0: resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} engines: {node: '>=10'} @@ -2758,7 +2764,7 @@ packages: engines: {node: '>=12'} yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + resolution: {integrity: sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=} yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} @@ -5279,6 +5285,8 @@ snapshots: - supports-color - terser + wa-sqlite@1.0.0: {} + wait-port@1.1.0: dependencies: chalk: 4.1.2 From fe737c4d84fead6cb88beec13555a4b7fb1c223d Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 22:14:33 -0600 Subject: [PATCH 20/25] Tweaks. --- packages/wa-sqlite-driver/package.json | 2 +- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 20 +++++++++---------- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 8 +++++--- pnpm-lock.yaml | 11 +++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index 527718d..6fb8eec 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -18,7 +18,7 @@ "@journeyapps/wa-sqlite": "^1.3.2", "@sqlite-js/driver": "workspace:^", "async-mutex": "^0.5.0", - "wa-sqlite": "^1.0.0" + "wa-sqlite": "git@github.com:rhashimoto/wa-sqlite.git#v1.0.9" }, "devDependencies": { "@sqlite-js/driver-tests": "workspace:^", diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index b26321a..682bdf8 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -1,5 +1,7 @@ import * as SQLite from '@journeyapps/wa-sqlite'; import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; +// import * as SQLite from 'wa-sqlite'; +// import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; import { PrepareOptions, ResetOptions, @@ -14,10 +16,10 @@ import { UpdateListener } from '@sqlite-js/driver'; import * as mutex from 'async-mutex'; -import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; // Initialize SQLite. export const module = await SQLiteESMFactory(); +console.log('module', module); export const sqlite3 = SQLite.Factory(module); const m = new mutex.Mutex(); @@ -233,7 +235,8 @@ class StatementImpl implements SqliteDriverStatement { while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {} const changes = sqlite3.changes(this.db); - const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + // const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + const lastInsertRowId = 0n; return { changes, lastInsertRowId }; } catch (e: any) { @@ -253,26 +256,20 @@ class StatementImpl implements SqliteDriverStatement { export class WaSqliteConnection implements SqliteDriverConnection { db: number; - vfs: OPFSCoopSyncVFS2; statements = new Set(); - static async open( - filename: string, - vfs: OPFSCoopSyncVFS2 - ): Promise { + static async open(filename: string): Promise { // Open the database. const db = await sqlite3.open_v2(filename); - return new WaSqliteConnection(db, vfs, filename); + return new WaSqliteConnection(db, filename); } constructor( db: number, - vfs: OPFSCoopSyncVFS2, public path: string ) { this.db = db; - this.vfs = vfs; } async close() { @@ -290,7 +287,8 @@ export class WaSqliteConnection implements SqliteDriverConnection { async getLastChanges(): Promise { const changes = sqlite3.changes(this.db); - const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + // const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); + const lastInsertRowId = 0n; return { changes, lastInsertRowId }; } diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index 0c0a688..b0d46aa 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -4,6 +4,8 @@ import { setupDriverWorker } from './worker_threads'; import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; import { OPFSAdaptiveVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSAdaptiveVFS.js'; import { OPFSPermutedVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSPermutedVFS.js'; +import { IDBBatchAtomicVFS as IDBBatchAtomicVFS0 } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; + let vfs: any | null = null; setupDriverWorker({ async openConnection(options) { @@ -17,7 +19,7 @@ setupDriverWorker({ // options.readonly ?? false // ); // IDBBatchAtomicVFS - breaks hard (database disk image is malformed) - // vfs = await (IDBBatchAtomicVFS as any).create('test.db', module); + vfs = await (IDBBatchAtomicVFS0 as any).create('test.db', module); // OPFSAdaptiveVFS - works great // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { // ifAvailable: true, @@ -29,12 +31,12 @@ setupDriverWorker({ // }); // database disk image is malformed - vfs = await (OPFSPermutedVFS as any).create('test.db', module); + // vfs = await (OPFSPermutedVFS as any).create('test.db', module); // @ts-ignore sqlite3.vfs_register(vfs as any, true); - const con = await WaSqliteConnection.open(options.path, vfs); + const con = await WaSqliteConnection.open(options.path); using stmt = await con.prepare('PRAGMA busy_timeout = 10000'); await stmt.step(); return con; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7903d8b..da73f70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,8 +154,8 @@ importers: specifier: ^0.5.0 version: 0.5.0 wa-sqlite: - specifier: ^1.0.0 - version: 1.0.0 + specifier: git@github.com:rhashimoto/wa-sqlite.git#v1.0.9 + version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53 devDependencies: '@sqlite-js/driver-tests': specifier: workspace:^ @@ -2645,8 +2645,9 @@ packages: jsdom: optional: true - wa-sqlite@1.0.0: - resolution: {integrity: sha512-Kyybo5/BaJp76z7gDWGk2J6Hthl4NIPsE+swgraEjy3IY6r5zIR02wAs1OJH4XtJp1y3puj3Onp5eMGS0z7nUA==} + wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53: + resolution: {tarball: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53} + version: 1.0.9 wait-port@1.1.0: resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} @@ -5285,7 +5286,7 @@ snapshots: - supports-color - terser - wa-sqlite@1.0.0: {} + wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53: {} wait-port@1.1.0: dependencies: From 7b7c0e8f470e646a594aee9626f29e5958d276b2 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 22:25:21 -0600 Subject: [PATCH 21/25] Functioning test again. --- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 1 + .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 20 ++++++++----------- .../test/src/mptest-runner.test.ts | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index 682bdf8..3364d58 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -1,5 +1,6 @@ import * as SQLite from '@journeyapps/wa-sqlite'; import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; +// import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'; // import * as SQLite from 'wa-sqlite'; // import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; import { diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index b0d46aa..d5274b0 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -13,22 +13,18 @@ setupDriverWorker({ if (vfs != null) { throw new Error('Can only open one connection'); } - // vfs = await OPFSCoopSyncVFS2.create( - // 'test.db', - // module, - // options.readonly ?? false - // ); + vfs = await OPFSCoopSyncVFS2.create( + 'test.db', + module, + options.readonly ?? false + ); // IDBBatchAtomicVFS - breaks hard (database disk image is malformed) - vfs = await (IDBBatchAtomicVFS0 as any).create('test.db', module); + // vfs = await (IDBBatchAtomicVFS0 as any).create('test.db', module); // OPFSAdaptiveVFS - works great // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { // ifAvailable: true, // mode: 'shared' // }); - // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { - // ifAvailable: true, - // mode: 'shared' - // }); // database disk image is malformed // vfs = await (OPFSPermutedVFS as any).create('test.db', module); @@ -37,8 +33,8 @@ setupDriverWorker({ sqlite3.vfs_register(vfs as any, true); const con = await WaSqliteConnection.open(options.path); - using stmt = await con.prepare('PRAGMA busy_timeout = 10000'); - await stmt.step(); + // using stmt = await con.prepare('PRAGMA busy_timeout = 10000'); + // await stmt.step(); return con; } }); diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts index d51477c..281bc39 100644 --- a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -410,7 +410,7 @@ class MptestRunner { } case 'wait': { const target = command.args[0] ?? 'all'; - const timeout = command.args[1] ? Number(command.args[1]) : 30_000; + const timeout = command.args[1] ? Number(command.args[1]) : 120_000; if (target === 'all') { await this.waitForAll(timeout); } else { From ff079fed398455de68f71158d41cdeae0a325d3a Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Thu, 23 Oct 2025 22:26:15 -0600 Subject: [PATCH 22/25] sync wasm --- packages/wa-sqlite-driver/src/wa-sqlite-driver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index 3364d58..a072725 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -1,6 +1,6 @@ import * as SQLite from '@journeyapps/wa-sqlite'; -import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; -// import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'; +// import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; +import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'; // import * as SQLite from 'wa-sqlite'; // import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; import { From 47a9539862c1dd7d686a7a8153c632e52def95f0 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 26 Oct 2025 18:48:22 -0600 Subject: [PATCH 23/25] Use upstream wa-sqlite. --- .node-version | 2 +- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 7 ++----- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 19 +++++++++---------- packages/wa-sqlite-driver/vitest.config.ts | 3 +-- pnpm-lock.yaml | 12 ++++++------ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/.node-version b/.node-version index 3d3475a..c004e35 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v22.5.1 +v22.20.0 diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index a072725..67c0021 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -1,8 +1,5 @@ -import * as SQLite from '@journeyapps/wa-sqlite'; -// import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs'; -import SQLiteESMFactory from '@journeyapps/wa-sqlite/dist/wa-sqlite.mjs'; -// import * as SQLite from 'wa-sqlite'; -// import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; +import * as SQLite from 'wa-sqlite'; +import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; import { PrepareOptions, ResetOptions, diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index d5274b0..18fefb7 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -1,10 +1,9 @@ import { OPFSCoopSyncVFS2 } from './OPFSCoopSyncVFS2'; import { sqlite3, module, WaSqliteConnection } from './wa-sqlite-driver'; import { setupDriverWorker } from './worker_threads'; -import { IDBBatchAtomicVFS } from '@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; -import { OPFSAdaptiveVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSAdaptiveVFS.js'; -import { OPFSPermutedVFS } from '@journeyapps/wa-sqlite/src/examples/OPFSPermutedVFS.js'; -import { IDBBatchAtomicVFS as IDBBatchAtomicVFS0 } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; +import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js'; +import { OPFSAdaptiveVFS } from 'wa-sqlite/src/examples/OPFSAdaptiveVFS.js'; +import { OPFSPermutedVFS } from 'wa-sqlite/src/examples/OPFSPermutedVFS.js'; let vfs: any | null = null; setupDriverWorker({ @@ -13,13 +12,13 @@ setupDriverWorker({ if (vfs != null) { throw new Error('Can only open one connection'); } - vfs = await OPFSCoopSyncVFS2.create( - 'test.db', - module, - options.readonly ?? false - ); + // vfs = await OPFSCoopSyncVFS2.create( + // 'test.db', + // module, + // options.readonly ?? false + // ); // IDBBatchAtomicVFS - breaks hard (database disk image is malformed) - // vfs = await (IDBBatchAtomicVFS0 as any).create('test.db', module); + vfs = await (IDBBatchAtomicVFS as any).create('test.db', module); // OPFSAdaptiveVFS - works great // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { // ifAvailable: true, diff --git a/packages/wa-sqlite-driver/vitest.config.ts b/packages/wa-sqlite-driver/vitest.config.ts index 4c88f04..9772161 100644 --- a/packages/wa-sqlite-driver/vitest.config.ts +++ b/packages/wa-sqlite-driver/vitest.config.ts @@ -1,6 +1,5 @@ import { defineConfig } from 'vitest/config'; import wasm from 'vite-plugin-wasm'; -import topLevelAwait from 'vite-plugin-top-level-await'; export default defineConfig({ esbuild: { target: 'es2022' }, @@ -8,7 +7,7 @@ export default defineConfig({ optimizeDeps: { // Don't optimise these packages as they contain web workers and WASM files. // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 - exclude: ['@journeyapps/wa-sqlite'], + exclude: ['wa-sqlite'], include: [] }, define: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da73f70..ac6041c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,8 +154,8 @@ importers: specifier: ^0.5.0 version: 0.5.0 wa-sqlite: - specifier: git@github.com:rhashimoto/wa-sqlite.git#v1.0.9 - version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53 + specifier: git@github.com:rhashimoto/wa-sqlite.git#idbbatchatomic-skip-writes + version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/c7375512180a15783434d54c35bdc6313bd3b866 devDependencies: '@sqlite-js/driver-tests': specifier: workspace:^ @@ -2645,9 +2645,9 @@ packages: jsdom: optional: true - wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53: - resolution: {tarball: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53} - version: 1.0.9 + wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/c7375512180a15783434d54c35bdc6313bd3b866: + resolution: {tarball: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/c7375512180a15783434d54c35bdc6313bd3b866} + version: 1.0.6 wait-port@1.1.0: resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} @@ -5286,7 +5286,7 @@ snapshots: - supports-color - terser - wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/a3b1324ed5a57928141b02eb3204421e1164ed53: {} + wa-sqlite@https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/c7375512180a15783434d54c35bdc6313bd3b866: {} wait-port@1.1.0: dependencies: From 9c90f90454b2423d8b8604a79c66c65761612fa0 Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 26 Oct 2025 22:06:05 -0600 Subject: [PATCH 24/25] Port wa-sqlite driver to new api; other tweaks. --- .../src/implementations/better-sqlite3.ts | 5 +- benchmarks/src/implementations/node-sqlite.ts | 5 +- .../src/implementations/node-sqlite3.ts | 5 +- benchmarks/src/implementations/sjp-json.ts | 5 +- .../src/implementations/sjp-optimized.ts | 5 +- benchmarks/src/implementations/sjp.ts | 5 +- benchmarks/src/util.ts | 24 + benchmarks/tsconfig.json | 5 +- packages/api/package.json | 6 +- packages/better-sqlite3-driver/package.json | 6 +- packages/driver/package.json | 34 +- .../src/worker/connection-adapter.ts} | 68 +-- packages/driver/src/worker/index.ts | 2 + .../src/worker/protocol.ts} | 77 +-- .../src/worker_threads/WorkerDriverAdapter.ts | 135 +----- .../src/worker_threads/async-commands.ts | 106 +---- packages/wa-sqlite-driver/package.json | 8 +- .../wa-sqlite-driver/src/wa-sqlite-driver.ts | 443 ++++++++++-------- .../wa-sqlite-driver/src/wa-sqlite-worker.ts | 12 +- .../src/worker_threads/index.ts | 2 +- .../src/worker_threads/setup.ts | 7 +- .../src/worker_threads/worker-driver.ts | 211 +++++---- .../test/src/concurrency.test.ts | 16 +- .../test/src/import-meta.d.ts | 6 + .../test/src/mptest-runner.test.ts | 30 +- packages/wa-sqlite-driver/test/tsconfig.json | 3 +- 26 files changed, 532 insertions(+), 699 deletions(-) rename packages/{wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts => driver/src/worker/connection-adapter.ts} (65%) create mode 100644 packages/driver/src/worker/index.ts rename packages/{wa-sqlite-driver/src/worker_threads/async-commands.ts => driver/src/worker/protocol.ts} (60%) create mode 100644 packages/wa-sqlite-driver/test/src/import-meta.d.ts diff --git a/benchmarks/src/implementations/better-sqlite3.ts b/benchmarks/src/implementations/better-sqlite3.ts index beb9849..7caffe1 100644 --- a/benchmarks/src/implementations/better-sqlite3.ts +++ b/benchmarks/src/implementations/better-sqlite3.ts @@ -3,14 +3,13 @@ import DatabaseConstructor from 'better-sqlite3'; import { promises as fs } from 'fs'; import assert from 'node:assert'; import { join } from 'path'; -import Prando from 'prando'; import { Benchmark } from '../Benchmark.js'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; export class BetterSqlite3Impl extends Benchmark { private db!: bsqlite.Database; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/implementations/node-sqlite.ts b/benchmarks/src/implementations/node-sqlite.ts index c71ae87..10d9f7c 100644 --- a/benchmarks/src/implementations/node-sqlite.ts +++ b/benchmarks/src/implementations/node-sqlite.ts @@ -1,9 +1,8 @@ import { promises as fs } from 'fs'; import assert from 'node:assert'; import { join } from 'path'; -import Prando from 'prando'; import { Benchmark } from '../Benchmark.js'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; //@ts-ignore import * as sqlite from 'node:sqlite'; @@ -11,7 +10,7 @@ import * as sqlite from 'node:sqlite'; export class NodeSqliteImpl extends Benchmark { private db!: sqlite.DatabaseSync; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/implementations/node-sqlite3.ts b/benchmarks/src/implementations/node-sqlite3.ts index d7fae3d..ddceade 100644 --- a/benchmarks/src/implementations/node-sqlite3.ts +++ b/benchmarks/src/implementations/node-sqlite3.ts @@ -1,16 +1,15 @@ import { Benchmark } from '../Benchmark.js'; import { join } from 'path'; import { promises as fs } from 'fs'; -import Prando from 'prando'; import assert from 'node:assert'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; import sqlite3 from 'sqlite3'; import { open, Database } from 'sqlite'; export class NodeSqlite3Impl extends Benchmark { private db!: Database; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/implementations/sjp-json.ts b/benchmarks/src/implementations/sjp-json.ts index 6ba7200..ea84e14 100644 --- a/benchmarks/src/implementations/sjp-json.ts +++ b/benchmarks/src/implementations/sjp-json.ts @@ -1,15 +1,14 @@ import { promises as fs } from 'fs'; import assert from 'node:assert'; import { join } from 'path'; -import Prando from 'prando'; import { SqliteConnectionPool } from '@sqlite-js/api'; import { Benchmark } from '../Benchmark.js'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; export class JSPJsonImpl extends Benchmark { private db!: SqliteConnectionPool; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/implementations/sjp-optimized.ts b/benchmarks/src/implementations/sjp-optimized.ts index e752e97..e7310e3 100644 --- a/benchmarks/src/implementations/sjp-optimized.ts +++ b/benchmarks/src/implementations/sjp-optimized.ts @@ -1,15 +1,14 @@ import { promises as fs } from 'fs'; import assert from 'node:assert'; import { join } from 'path'; -import Prando from 'prando'; import { SqliteConnectionPool } from '@sqlite-js/api'; import { Benchmark } from '../Benchmark.js'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; export class JSPOptimizedImpl extends Benchmark { private db!: SqliteConnectionPool; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/implementations/sjp.ts b/benchmarks/src/implementations/sjp.ts index e12e2aa..3137008 100644 --- a/benchmarks/src/implementations/sjp.ts +++ b/benchmarks/src/implementations/sjp.ts @@ -1,15 +1,14 @@ import { promises as fs } from 'fs'; import assert from 'node:assert'; import { join } from 'path'; -import Prando from 'prando'; import { SqliteConnectionPool } from '@sqlite-js/api'; import { Benchmark } from '../Benchmark.js'; -import { numberName } from '../util.js'; +import { numberName, createRandom } from '../util.js'; export class JSPImpl extends Benchmark { private db!: SqliteConnectionPool; private dir: string; - private random = new Prando.default(0); + private random = createRandom(0); constructor( public name: string, diff --git a/benchmarks/src/util.ts b/benchmarks/src/util.ts index 5456c77..7251bf0 100644 --- a/benchmarks/src/util.ts +++ b/benchmarks/src/util.ts @@ -1,3 +1,5 @@ +import * as Prando from 'prando'; + const digits = [ '', 'one', @@ -34,6 +36,28 @@ const names100 = [ ...digits.map((digit) => `ninety${digit != '' ? '-' + digit : ''}`) ]; +type PrandoInstance = import('prando').default; +type PrandoConstructor = new (seed?: number | string) => PrandoInstance; + +function resolvePrandoConstructor(): PrandoConstructor { + const mod = Prando as unknown as { + default?: PrandoConstructor; + }; + if (typeof (Prando as unknown) === 'function') { + return Prando as unknown as PrandoConstructor; + } + if (mod.default) { + return mod.default; + } + throw new Error('Unable to locate Prando constructor'); +} + +const PRANDO_CTOR = resolvePrandoConstructor(); + +export function createRandom(seed?: number | string): PrandoInstance { + return new PRANDO_CTOR(seed); +} + export function numberName(n: number): string { if (n == 0) { return 'zero'; diff --git a/benchmarks/tsconfig.json b/benchmarks/tsconfig.json index 971a02f..d80b224 100644 --- a/benchmarks/tsconfig.json +++ b/benchmarks/tsconfig.json @@ -5,11 +5,14 @@ "target": "ES2022", "strict": true, "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "lib", "sourceMap": true, "declaration": true, "rootDir": "src", - "typeRoots": ["./src/types", "./node_modules/@types"] + "typeRoots": ["./src/types", "./node_modules/@types"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true }, "include": ["src"], "references": [{ "path": "../packages/api" }] diff --git a/packages/api/package.json b/packages/api/package.json index e9557c0..8f3bbc9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -8,8 +8,12 @@ "test": "vitest", "clean": "tsc -b --clean && rm -rf lib" }, + "types": "./lib/index.d.ts", "exports": { - ".": "./lib/index.js" + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } }, "keywords": [], "author": "", diff --git a/packages/better-sqlite3-driver/package.json b/packages/better-sqlite3-driver/package.json index db4d9be..aaa0523 100644 --- a/packages/better-sqlite3-driver/package.json +++ b/packages/better-sqlite3-driver/package.json @@ -8,8 +8,12 @@ "test": "pnpm build && vitest", "clean": "tsc -b --clean && tsc -b ./test/tsconfig.json --clean && rm -rf lib test/lib" }, + "types": "./lib/index.d.ts", "exports": { - ".": "./lib/index.js" + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } }, "keywords": [], "author": "", diff --git a/packages/driver/package.json b/packages/driver/package.json index fd78c52..775049f 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -8,12 +8,36 @@ "build": "tsc -b", "clean": "tsc -b --clean && rm -rf lib" }, + "types": "./lib/index.d.ts", "exports": { - ".": "./lib/index.js", - "./worker_threads": "./lib/worker_threads/index.js", - "./worker_threads/setup": "./lib/worker_threads/setup.js", - "./util": "./lib/util/index.js", - "./node": "./lib/node/index.js" + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./worker": { + "types": "./lib/worker/index.d.ts", + "default": "./lib/worker/index.js" + }, + "./worker/*": { + "types": "./lib/worker/*.d.ts", + "default": "./lib/worker/*.js" + }, + "./worker_threads": { + "types": "./lib/worker_threads/index.d.ts", + "default": "./lib/worker_threads/index.js" + }, + "./worker_threads/setup": { + "types": "./lib/worker_threads/setup.d.ts", + "default": "./lib/worker_threads/setup.js" + }, + "./util": { + "types": "./lib/util/index.d.ts", + "default": "./lib/util/index.js" + }, + "./node": { + "types": "./lib/node/index.d.ts", + "default": "./lib/node/index.js" + } }, "keywords": [], "author": "", diff --git a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/driver/src/worker/connection-adapter.ts similarity index 65% rename from packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts rename to packages/driver/src/worker/connection-adapter.ts index 0fbe4f7..cb8533a 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/WorkerDriverAdapter.ts +++ b/packages/driver/src/worker/connection-adapter.ts @@ -2,13 +2,11 @@ import { SqliteChanges, SqliteDriverConnection, SqliteDriverStatement, - SqliteStepResult, UpdateListener -} from '@sqlite-js/driver'; -import { mapError } from '@sqlite-js/driver/util'; +} from '../driver-api.js'; +import { mapError } from '../util/errors.js'; import { InferBatchResult, - SqliteBind, SqliteCommand, SqliteCommandResponse, SqliteCommandType, @@ -16,22 +14,19 @@ import { SqliteParse, SqliteParseResult, SqlitePrepare, - SqliteReset, + SqliteQuery, + SqliteQueryResult, SqliteRun, - SqliteStep, WorkerDriver -} from './async-commands.js'; +} from './protocol.js'; export class WorkerConnectionAdapter implements WorkerDriver { constructor(public connnection: SqliteDriverConnection) {} statements = new Map(); - private promise: Promise = Promise.resolve(); async close() { - const p = this.promise.then(() => this.connnection.close()); - this.promise = p; - return p; + await this.connnection.close(); } private requireStatement(id: number) { @@ -53,9 +48,7 @@ export class WorkerConnectionAdapter implements WorkerDriver { } const statement = this.connnection.prepare(sql, { - bigint: command.bigint, - persist: command.persist, - rawResults: command.rawResults + autoFinalize: command.autoFinalize }); this.statements.set(id, statement); } @@ -66,28 +59,25 @@ export class WorkerConnectionAdapter implements WorkerDriver { return { columns: await statement.getColumns() }; } - private _bind(command: SqliteBind): void { - const { id, parameters } = command; - const statement = this.requireStatement(id); - statement.bind(parameters); - } - - private _step(command: SqliteStep): Promise { - const { id, n, requireTransaction } = command; - const statement = this.requireStatement(id); - return statement.step(n, { requireTransaction }); - } - private _run(command: SqliteRun): Promise { const { id } = command; const statement = this.requireStatement(id); - return statement.run(command); + return statement.run(command.parameters, command.options); } - private _reset(command: SqliteReset): void { + private async _query(command: SqliteQuery): Promise { const { id } = command; const statement = this.requireStatement(id); - statement.reset(command); + if (command.array) { + const results = await statement.allArray( + command.parameters, + command.options + ); + return { rows: results }; + } else { + const results = await statement.all(command.parameters, command.options); + return { rows: results }; + } } private _finalize(command: SqliteFinalize): void { @@ -101,35 +91,21 @@ export class WorkerConnectionAdapter implements WorkerDriver { switch (command.type) { case SqliteCommandType.prepare: return this._prepare(command); - case SqliteCommandType.bind: - return this._bind(command); - case SqliteCommandType.step: - return this._step(command); + case SqliteCommandType.query: + return this._query(command); case SqliteCommandType.run: return this._run(command); - case SqliteCommandType.reset: - return this._reset(command); case SqliteCommandType.finalize: return this._finalize(command); case SqliteCommandType.parse: return this._parse(command); - case SqliteCommandType.changes: - return this.connnection.getLastChanges(); default: - throw new Error(`Unknown command: ${command.type}`); + throw new Error(`Unknown command: ${(command as SqliteCommand).type}`); } } async execute( commands: T - ): Promise> { - const p = this.promise.then(() => this._execute(commands)); - this.promise = p.then(() => {}); - return p; - } - - async _execute( - commands: T ): Promise> { let results: SqliteCommandResponse[] = []; diff --git a/packages/driver/src/worker/index.ts b/packages/driver/src/worker/index.ts new file mode 100644 index 0000000..969b12b --- /dev/null +++ b/packages/driver/src/worker/index.ts @@ -0,0 +1,2 @@ +export * from './protocol.js'; +export * from './connection-adapter.js'; diff --git a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts b/packages/driver/src/worker/protocol.ts similarity index 60% rename from packages/wa-sqlite-driver/src/worker_threads/async-commands.ts rename to packages/driver/src/worker/protocol.ts index 0b4de11..cb9a964 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/async-commands.ts +++ b/packages/driver/src/worker/protocol.ts @@ -1,22 +1,19 @@ import { - SqliteParameterBinding, + QueryOptions, + SqliteArrayRow, SqliteChanges, - SqliteStepResult -} from '@sqlite-js/driver'; -import { SerializedDriverError } from '@sqlite-js/driver'; + SqliteObjectRow, + SqliteParameterBinding, + StreamQueryOptions +} from '../driver-api.js'; +import { SerializedDriverError } from '../sqlite-error.js'; -export enum SqliteCommandType { +export const enum SqliteCommandType { prepare = 1, - bind = 2, - step = 3, - reset = 4, finalize = 5, - sync = 6, parse = 7, run = 8, - changes = 9, - lock = 10, - release = 11 + query = 9 } export type SqliteDriverError = SerializedDriverError; @@ -39,43 +36,35 @@ export interface SqlitePrepare extends SqliteBaseCommand { type: SqliteCommandType.prepare; id: number; sql: string; - bigint?: boolean; - persist?: boolean; - rawResults?: boolean; + autoFinalize?: boolean; } export interface SqliteParseResult { columns: string[]; } -export interface SqliteBind extends SqliteBaseCommand { - type: SqliteCommandType.bind; - id: number; - parameters: SqliteParameterBinding; -} - export interface SqliteParse extends SqliteBaseCommand { type: SqliteCommandType.parse; id: number; } -export interface SqliteStep extends SqliteBaseCommand { - type: SqliteCommandType.step; - id: number; - n?: number; - requireTransaction?: boolean; -} - export interface SqliteRun extends SqliteBaseCommand { type: SqliteCommandType.run; id: number; - requireTransaction?: boolean; + parameters?: SqliteParameterBinding; + options?: QueryOptions; +} + +export interface SqliteQueryResult { + rows: SqliteArrayRow[] | SqliteObjectRow[]; } -export interface SqliteReset extends SqliteBaseCommand { - type: SqliteCommandType.reset; +export interface SqliteQuery extends SqliteBaseCommand { + type: SqliteCommandType.query; id: number; - clearBindings?: boolean; + parameters?: SqliteParameterBinding; + options?: StreamQueryOptions; + array?: boolean; } export interface SqliteFinalize extends SqliteBaseCommand { @@ -83,34 +72,20 @@ export interface SqliteFinalize extends SqliteBaseCommand { id: number; } -export interface SqliteSync { - type: SqliteCommandType.sync; -} - -export interface SqliteGetChanges { - type: SqliteCommandType.changes; -} - export type SqliteCommand = | SqlitePrepare - | SqliteBind - | SqliteStep | SqliteRun - | SqliteReset | SqliteFinalize - | SqliteSync - | SqliteParse - | SqliteGetChanges; + | SqliteQuery + | SqliteParse; export type InferCommandResult = T extends SqliteRun ? SqliteChanges - : T extends SqliteStep - ? SqliteStepResult + : T extends SqliteQuery + ? SqliteQueryResult : T extends SqliteParse ? SqliteParseResult - : T extends SqliteGetChanges - ? SqliteChanges - : void; + : void; export type InferBatchResult = { [i in keyof T]: diff --git a/packages/driver/src/worker_threads/WorkerDriverAdapter.ts b/packages/driver/src/worker_threads/WorkerDriverAdapter.ts index c17ce25..a5bc912 100644 --- a/packages/driver/src/worker_threads/WorkerDriverAdapter.ts +++ b/packages/driver/src/worker_threads/WorkerDriverAdapter.ts @@ -1,134 +1 @@ -import { - SqliteChanges, - SqliteDriverConnection, - SqliteDriverStatement, - UpdateListener -} from '../driver-api.js'; -import { mapError } from '../util/errors.js'; -import { - InferBatchResult, - SqliteCommand, - SqliteCommandResponse, - SqliteCommandType, - SqliteFinalize, - SqliteParse, - SqliteParseResult, - SqlitePrepare, - SqliteQuery, - SqliteQueryResult, - SqliteRun, - WorkerDriver -} from './async-commands.js'; - -export class WorkerConnectionAdapter implements WorkerDriver { - constructor(public connnection: SqliteDriverConnection) {} - - statements = new Map(); - - async close() { - await this.connnection.close(); - } - - private requireStatement(id: number) { - const statement = this.statements.get(id); - if (statement == null) { - throw new Error(`statement not found: ${id}`); - } - return statement; - } - - private _prepare(command: SqlitePrepare): void { - const { id, sql } = command; - - const existing = this.statements.get(id); - if (existing != null) { - throw new Error( - `Replacing statement ${id} without finalizing the previous one` - ); - } - - const statement = this.connnection.prepare(sql, { - autoFinalize: command.autoFinalize - }); - this.statements.set(id, statement); - } - - private async _parse(command: SqliteParse): Promise { - const { id } = command; - const statement = this.requireStatement(id); - return { columns: await statement.getColumns() }; - } - - private _run(command: SqliteRun): Promise { - const { id } = command; - const statement = this.requireStatement(id); - return statement.run(command.parameters, command.options); - } - - private async _query(command: SqliteQuery): Promise { - const { id } = command; - const statement = this.requireStatement(id); - if (command.array) { - const results = await statement.allArray( - command.parameters, - command.options - ); - return { rows: results }; - } else { - const results = await statement.all(command.parameters, command.options); - return { rows: results }; - } - } - - private _finalize(command: SqliteFinalize): void { - const { id } = command; - const statement = this.requireStatement(id); - statement.finalize(); - this.statements.delete(id); - } - - private async _executeCommand(command: SqliteCommand): Promise { - switch (command.type) { - case SqliteCommandType.prepare: - return this._prepare(command); - case SqliteCommandType.query: - return this._query(command); - case SqliteCommandType.run: - return this._run(command); - case SqliteCommandType.finalize: - return this._finalize(command); - case SqliteCommandType.parse: - return this._parse(command); - default: - throw new Error(`Unknown command: ${(command as SqliteCommand).type}`); - } - } - - async execute( - commands: T - ): Promise> { - let results: SqliteCommandResponse[] = []; - - for (let command of commands) { - try { - const result = await this._executeCommand(command); - results.push({ value: result }); - } catch (e: any) { - const err = mapError(e); - results.push({ - error: { message: err.message, stack: err.stack, code: err.code } - }); - } - } - return results as InferBatchResult; - } - - onUpdate( - listener: UpdateListener, - options?: - | { tables?: string[] | undefined; batchLimit?: number | undefined } - | undefined - ): () => void { - throw new Error('Not implemented yet'); - } -} +export { WorkerConnectionAdapter } from '../worker/connection-adapter.js'; diff --git a/packages/driver/src/worker_threads/async-commands.ts b/packages/driver/src/worker_threads/async-commands.ts index e9450f6..4665e70 100644 --- a/packages/driver/src/worker_threads/async-commands.ts +++ b/packages/driver/src/worker_threads/async-commands.ts @@ -1,105 +1 @@ -import { - SqliteParameterBinding, - SqliteChanges, - QueryOptions, - StreamQueryOptions, - SqliteArrayRow, - SqliteObjectRow -} from '../driver-api.js'; -import { SerializedDriverError } from '../sqlite-error.js'; - -export const enum SqliteCommandType { - prepare = 1, - finalize = 5, - parse = 7, - run = 8, - query = 9 -} - -export type SqliteDriverError = SerializedDriverError; - -export type SqliteCommandResponse = SqliteErrorResponse | SqliteValueResponse; - -export interface SqliteErrorResponse { - error: SqliteDriverError; -} - -export interface SqliteValueResponse { - value: T; -} - -export interface SqliteBaseCommand { - type: SqliteCommandType; -} - -export interface SqlitePrepare extends SqliteBaseCommand { - type: SqliteCommandType.prepare; - id: number; - sql: string; - autoFinalize?: boolean; -} - -export interface SqliteParseResult { - columns: string[]; -} - -export interface SqliteParse extends SqliteBaseCommand { - type: SqliteCommandType.parse; - id: number; -} - -export interface SqliteRun extends SqliteBaseCommand { - type: SqliteCommandType.run; - id: number; - parameters?: SqliteParameterBinding; - options?: QueryOptions; -} - -export interface SqliteQueryResult { - rows: SqliteArrayRow[] | SqliteObjectRow[]; -} - -export interface SqliteQuery extends SqliteBaseCommand { - type: SqliteCommandType.query; - id: number; - parameters?: SqliteParameterBinding; - options?: StreamQueryOptions; - array?: boolean; -} - -export interface SqliteFinalize extends SqliteBaseCommand { - type: SqliteCommandType.finalize; - id: number; -} - -export type SqliteCommand = - | SqlitePrepare - | SqliteRun - | SqliteFinalize - | SqliteQuery - | SqliteParse; - -export type InferCommandResult = T extends SqliteRun - ? SqliteChanges - : T extends SqliteQuery - ? SqliteQueryResult - : T extends SqliteParse - ? SqliteParseResult - : void; - -export type InferBatchResult = { - [i in keyof T]: - | SqliteErrorResponse - | SqliteValueResponse>; -}; - -export function isErrorResponse( - response: SqliteCommandResponse -): response is SqliteErrorResponse { - return (response as SqliteErrorResponse).error != null; -} - -export interface WorkerDriver { - execute(commands: SqliteCommand[]): Promise; - close(): Promise; -} +export * from '../worker/protocol.js'; diff --git a/packages/wa-sqlite-driver/package.json b/packages/wa-sqlite-driver/package.json index 6fb8eec..bf02eb3 100644 --- a/packages/wa-sqlite-driver/package.json +++ b/packages/wa-sqlite-driver/package.json @@ -4,12 +4,16 @@ "description": "", "type": "module", "scripts": { - "build": "tsc -b", + "build": "tsc -b && tsc -b test/tsconfig.json", "test": "pnpm build && vitest", "clean": "tsc -b --clean && tsc -b ./test/tsconfig.json --clean && rm -rf lib test/lib" }, + "types": "./lib/index.d.ts", "exports": { - ".": "./lib/index.js" + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } }, "keywords": [], "author": "", diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts index 67c0021..12d0611 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-driver.ts @@ -2,249 +2,308 @@ import * as SQLite from 'wa-sqlite'; import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs'; import { PrepareOptions, - ResetOptions, + QueryOptions, + SqliteArrayRow, SqliteChanges, SqliteDriverConnection, SqliteDriverStatement, - SqliteError, + SqliteObjectRow, SqliteParameterBinding, - SqliteRow, - SqliteStepResult, - StepOptions, + StreamQueryOptions, UpdateListener } from '@sqlite-js/driver'; +import { SqliteError } from '@sqlite-js/driver'; import * as mutex from 'async-mutex'; -// Initialize SQLite. export const module = await SQLiteESMFactory(); -console.log('module', module); export const sqlite3 = SQLite.Factory(module); -const m = new mutex.Mutex(); +const globalMutex = new mutex.Mutex(); + +async function withMutex(fn: () => Promise | T): Promise { + return globalMutex.runExclusive(fn); +} + +function toSqliteError(error: any): SqliteError { + return new SqliteError({ + code: 'SQLITE_ERROR', + message: error?.message ?? String(error) + }); +} class StatementImpl implements SqliteDriverStatement { - private preparePromise: Promise<{ error: SqliteError | null }>; - private bindPromise?: Promise<{ error: SqliteError | null }>; + private statementRef?: number; private columns: string[] = []; + private finalized = false; + private prepared = false; - private statementRef?: number; - private done = false; + readonly persisted: boolean; constructor( private db: number, - private con: WaSqliteConnection, + private connection: WaSqliteConnection, public source: string, - public options: PrepareOptions + options: PrepareOptions ) { - this.preparePromise = this.prepare(); - } - - async prepare() { - return await m.runExclusive(() => this._prepare()); + this.persisted = options.autoFinalize ?? false; } - private async getStatement() { + private async prepareStatementIfNeeded(): Promise { + if (this.prepared || this.finalized) { + if (this.statementRef == null) { + throw new SqliteError({ + code: 'SQLITE_ERROR', + message: 'Statement has been finalized' + }); + } + return; + } const statementsIter = sqlite3.statements(this.db, this.source, { unscoped: true }); - for await (let statement of statementsIter) { - return statement; - } - throw new Error(`No SQL statements in: ${this.source}`); - } - - async _prepare() { try { - const statement = await this.getStatement(); - this.statementRef = statement; - this.columns = sqlite3.column_names(statement); - return { error: null }; - } catch (e: any) { - return { - error: new SqliteError({ - code: 'SQLITE_ERROR', - message: e.message - }) - }; + for await (let statement of statementsIter) { + this.statementRef = statement; + this.columns = sqlite3.column_names(statement); + this.prepared = true; + return; + } + } catch (error) { + throw toSqliteError(error); } + throw new SqliteError({ + code: 'SQLITE_ERROR', + message: `No SQL statements in: ${this.source}` + }); } - private async _waitForPrepare() { - const { error } = await (this.bindPromise ?? this.preparePromise); - if (error) { - throw error; + private resetStatement(): void { + if (this.statementRef == null) { + return; } + sqlite3.reset(this.statementRef); } - async getColumns(): Promise { - await this._waitForPrepare(); - return sqlite3.column_names(this.statementRef!); + private clearBindings(): void { + if (this.statementRef == null) { + return; + } + const count = sqlite3.bind_parameter_count(this.statementRef); + for (let i = 0; i < count; i++) { + sqlite3.bind_null(this.statementRef, i + 1); + } } - bind(parameters: SqliteParameterBinding): void { - this.bindPromise = this.preparePromise.then(async (result) => { - if (result.error) { - return result; - } - await m.runExclusive(() => this.bindImpl(parameters)); - return { error: null }; - }); - } + private bindParameters(parameters: SqliteParameterBinding | undefined): void { + if (this.statementRef == null || parameters == null) { + return; + } - bindImpl(parameters: SqliteParameterBinding): void { if (Array.isArray(parameters)) { - const count = sqlite3.bind_parameter_count(this.statementRef!); - let pi = 0; + const count = sqlite3.bind_parameter_count(this.statementRef); + // Bind any named parameters that correspond to positional indices for (let i = 0; i < count; i++) { - const name = sqlite3.bind_parameter_name(this.statementRef!, i + 1); - if (name == '') { - const value = parameters[pi]; - pi++; - if (typeof value != 'undefined') { - sqlite3.bind(this.statementRef!, i + 1, value); + const name = sqlite3.bind_parameter_name(this.statementRef, i + 1); + if (name === '') { + const value = parameters[i]; + if (typeof value !== 'undefined') { + sqlite3.bind(this.statementRef, i + 1, value); } } } - for (let i = 0; i < parameters.length; i++) { const value = parameters[i]; if (typeof value !== 'undefined') { - sqlite3.bind(this.statementRef!, i + 1, value); + sqlite3.bind(this.statementRef, i + 1, value); } } - } else if (parameters != null) { - const count = sqlite3.bind_parameter_count(this.statementRef!); + } else { + const count = sqlite3.bind_parameter_count(this.statementRef); for (let i = 0; i < count; i++) { - const name = sqlite3.bind_parameter_name(this.statementRef!, i + 1); - if (name != '') { - if (name in parameters) { - const value = parameters[name]; - sqlite3.bind(this.statementRef!, i + 1, value); - } else if (name.substring(1) in parameters) { - // Removes the prefix of ? : @ $ - const value = parameters[name.substring(1)]; - sqlite3.bind(this.statementRef!, i + 1, value); - } + const name = sqlite3.bind_parameter_name(this.statementRef, i + 1); + if (name === '') { + continue; + } + let key = name; + if (!(key in parameters) && name.length > 1) { + key = name.substring(1); + } + const value = (parameters as Record)[key]; + if (typeof value !== 'undefined') { + sqlite3.bind(this.statementRef, i + 1, value); } } } } - async step(n?: number, options?: StepOptions): Promise { - await this._waitForPrepare(); - - return await m.runExclusive(() => this._step(n, options)); - } - - async _step(n?: number, options?: StepOptions): Promise { - try { - if (this.done) { - return { done: true }; + private mapValue( + value: unknown, + options?: QueryOptions | StreamQueryOptions + ): unknown { + const useBigint = options?.bigint ?? false; + if (typeof value === 'number') { + if (useBigint && Number.isInteger(value)) { + return BigInt(value); } + return value; + } + if (typeof value === 'bigint' && !useBigint) { + const num = Number(value); + // if (!Number.isSafeInteger(num)) { + // return value; + // } + return num; + } + return value; + } - const stmt = this.statementRef!; - - let rows: SqliteRow[] = []; + private mapRow( + row: any[], + options: QueryOptions | StreamQueryOptions | undefined, + asArray: boolean + ): SqliteObjectRow | SqliteArrayRow { + if (asArray) { + return row.map((value) => this.mapValue(value, options)) as SqliteArrayRow; + } + const entries = this.columns.map((column, index) => [ + column, + this.mapValue(row[index], options) + ]); + return Object.fromEntries(entries) as SqliteObjectRow; + } - const mapValue = (value: any) => { - if (typeof value == 'number') { - return this.options.bigint ? BigInt(value) : value; - } else if (typeof value == 'bigint') { - return this.options.bigint ? value : Number(value); - } else { - return value; - } - }; - const mapRow = this.options.rawResults - ? (row: any) => row.map(mapValue) - : (row: any[]) => { - return Object.fromEntries( - this.columns.map((c, i) => [c, mapValue(row[i])]) - ); - }; - if (n == null) { + async all( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { + return withMutex(async () => { + try { + await this.prepareStatementIfNeeded(); + const stmt = this.statementRef!; + this.resetStatement(); + this.clearBindings(); + this.bindParameters(parameters); + const rows: SqliteObjectRow[] = []; while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) { const row = sqlite3.row(stmt); - rows.push(mapRow(row)); + rows.push(this.mapRow(row, options, false) as SqliteObjectRow); } - this.done = true; - return { rows: rows, done: true }; - } else { - while ( - rows.length < n && - (await sqlite3.step(stmt)) === SQLite.SQLITE_ROW - ) { + return rows; + } catch (error) { + throw toSqliteError(error); + } finally { + this.resetStatement(); + } + }); + } + + async allArray( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { + return withMutex(async () => { + try { + await this.prepareStatementIfNeeded(); + const stmt = this.statementRef!; + this.resetStatement(); + this.clearBindings(); + this.bindParameters(parameters); + const rows: SqliteArrayRow[] = []; + while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) { const row = sqlite3.row(stmt); - rows.push(mapRow(row)); + rows.push(this.mapRow(row, options, true) as SqliteArrayRow); } - const done = rows.length < n; - this.done = done; - return { rows: rows, done: done }; + return rows; + } catch (error) { + throw toSqliteError(error); + } finally { + this.resetStatement(); } - } catch (e: any) { - throw new SqliteError({ - code: 'SQLITE_ERROR', - message: e.message - }); - } + }); } - async _finalize() { - // Wait for these to complete, but ignore any errors. - // TODO: also wait for run/step to complete - await this.preparePromise; - await this.bindPromise; - - if (this.statementRef) { - sqlite3.finalize(this.statementRef); - this.statementRef = undefined; + async *stream( + parameters?: SqliteParameterBinding, + options?: StreamQueryOptions + ): AsyncIterableIterator { + const rows = await this.all(parameters, options); + if (rows.length === 0) { + return; + } + const chunkSize = options?.chunkMaxRows ?? rows.length; + const effectiveChunk = chunkSize > 0 ? chunkSize : rows.length; + for (let i = 0; i < rows.length; i += effectiveChunk) { + yield rows.slice(i, i + effectiveChunk); } } - finalize(): void { - m.runExclusive(() => this._finalize()); + async *streamArray( + parameters?: SqliteParameterBinding, + options?: StreamQueryOptions + ): AsyncIterableIterator { + const rows = await this.allArray(parameters, options); + if (rows.length === 0) { + return; + } + const chunkSize = options?.chunkMaxRows ?? rows.length; + const effectiveChunk = chunkSize > 0 ? chunkSize : rows.length; + for (let i = 0; i < rows.length; i += effectiveChunk) { + yield rows.slice(i, i + effectiveChunk); + } } - reset(options?: ResetOptions): void { - this.preparePromise.finally(() => { - this.done = false; - sqlite3.reset(this.statementRef!); + async getColumns(): Promise { + return withMutex(async () => { + await this.prepareStatementIfNeeded(); + return this.columns; + }); + } - if (options?.clearBindings) { - // No native clear_bidings? - const count = sqlite3.bind_parameter_count(this.statementRef!); - for (let i = 0; i < count; i++) { - sqlite3.bind_null(this.statementRef!, i + 1); + async run( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { + return withMutex(async () => { + try { + await this.prepareStatementIfNeeded(); + const stmt = this.statementRef!; + this.resetStatement(); + this.clearBindings(); + this.bindParameters(parameters); + while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) { + // Exhaust results } + const changes = sqlite3.changes(this.db); + const lastInsertRowId = 0n; + return { changes, lastInsertRowId }; + } catch (error) { + throw toSqliteError(error); + } finally { + this.resetStatement(); } }); } - async run(options?: StepOptions): Promise { - return await m.runExclusive(() => this._run(options)); + finalize(): void { + void withMutex(() => { + this.finalizeInternal(); + }); } - async _run(options?: StepOptions): Promise { - await this.preparePromise; + finalizeForClose(): void { + this.finalizeInternal(); + } - try { - this.reset(); - const stmt = this.statementRef!; - while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {} - - const changes = sqlite3.changes(this.db); - // const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); - const lastInsertRowId = 0n; - - return { changes, lastInsertRowId }; - } catch (e: any) { - throw new SqliteError({ - code: 'SQLITE_ERROR', - message: e.message - }); - } finally { - this.reset(); + private finalizeInternal() { + if (this.finalized) { + return; + } + this.finalized = true; + if (this.statementRef != null) { + sqlite3.finalize(this.statementRef); + this.statementRef = undefined; } + this.connection.unregisterStatement(this); } [Symbol.dispose](): void { @@ -253,58 +312,46 @@ class StatementImpl implements SqliteDriverStatement { } export class WaSqliteConnection implements SqliteDriverConnection { - db: number; + private statements = new Set(); - statements = new Set(); + constructor( + private db: number, + public path: string + ) {} static async open(filename: string): Promise { - // Open the database. const db = await sqlite3.open_v2(filename); return new WaSqliteConnection(db, filename); } - constructor( - db: number, - public path: string - ) { - this.db = db; + registerStatement(statement: StatementImpl) { + this.statements.add(statement); + } + + unregisterStatement(statement: StatementImpl) { + this.statements.delete(statement); } async close() { - await m.runExclusive(async () => { - for (let statement of this.statements) { - if (statement.options.persist) { - statement.finalize(); - } + await withMutex(async () => { + for (let statement of Array.from(this.statements)) { + statement.finalizeForClose(); } - + this.statements.clear(); await new Promise((resolve) => setTimeout(resolve, 100)); await sqlite3.close(this.db); }); } - async getLastChanges(): Promise { - const changes = sqlite3.changes(this.db); - // const lastInsertRowId = BigInt(sqlite3.last_insert_id(this.db)); - const lastInsertRowId = 0n; - - return { changes, lastInsertRowId }; - } - prepare(sql: string, options?: PrepareOptions): StatementImpl { - const st = new StatementImpl(this.db, this, sql, options ?? {}); - // TODO: cleanup on finalize - this.statements.add(st); - return st; - } - - dispose(): void { - // No-op + const statement = new StatementImpl(this.db, this, sql, options ?? {}); + this.registerStatement(statement); + return statement; } onUpdate( - listener: UpdateListener, - options?: + _listener: UpdateListener, + _options?: | { tables?: string[] | undefined; batchLimit?: number | undefined } | undefined ): () => void { diff --git a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts index 18fefb7..ff431f8 100644 --- a/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts +++ b/packages/wa-sqlite-driver/src/wa-sqlite-worker.ts @@ -12,13 +12,13 @@ setupDriverWorker({ if (vfs != null) { throw new Error('Can only open one connection'); } - // vfs = await OPFSCoopSyncVFS2.create( - // 'test.db', - // module, - // options.readonly ?? false - // ); + vfs = await OPFSCoopSyncVFS2.create( + 'test.db', + module, + options.readonly ?? false + ); // IDBBatchAtomicVFS - breaks hard (database disk image is malformed) - vfs = await (IDBBatchAtomicVFS as any).create('test.db', module); + // vfs = await (IDBBatchAtomicVFS as any).create('test.db', module); // OPFSAdaptiveVFS - works great // vfs = await (OPFSAdaptiveVFS as any).create('test.db', module, { // ifAvailable: true, diff --git a/packages/wa-sqlite-driver/src/worker_threads/index.ts b/packages/wa-sqlite-driver/src/worker_threads/index.ts index 7d1c108..5678a3d 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/index.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/index.ts @@ -1,3 +1,3 @@ -export * from './async-commands.js'; +export * from '@sqlite-js/driver/worker/protocol'; export * from './worker-driver.js'; export * from './setup.js'; diff --git a/packages/wa-sqlite-driver/src/worker_threads/setup.ts b/packages/wa-sqlite-driver/src/worker_threads/setup.ts index 587ddac..2617dcb 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/setup.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/setup.ts @@ -1,9 +1,12 @@ import { Deferred } from './deferred.js'; -import { isErrorResponse, WorkerDriver } from './async-commands.js'; +import { + isErrorResponse, + WorkerDriver +} from '@sqlite-js/driver/worker/protocol'; import type { WorkerDriverConnectionOptions } from './worker-driver.js'; import { SqliteDriverConnection } from '@sqlite-js/driver'; -import { WorkerConnectionAdapter } from './WorkerDriverAdapter.js'; +import { WorkerConnectionAdapter } from '@sqlite-js/driver/worker/connection-adapter'; export type { WorkerDriverConnectionOptions }; diff --git a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts index e8cf541..d58e1a3 100644 --- a/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts +++ b/packages/wa-sqlite-driver/src/worker_threads/worker-driver.ts @@ -1,12 +1,13 @@ import { PrepareOptions, - ResetOptions, + QueryOptions, + SqliteArrayRow, + SqliteChanges, SqliteDriverConnection, SqliteDriverStatement, + SqliteObjectRow, SqliteParameterBinding, - SqliteChanges, - SqliteStepResult, - StepOptions, + StreamQueryOptions, UpdateListener } from '@sqlite-js/driver'; @@ -19,7 +20,7 @@ import { SqliteCommand, SqliteCommandType, SqliteDriverError -} from './async-commands.js'; +} from '@sqlite-js/driver/worker/protocol'; export interface WorkerDriverConnectionOptions { path: string; @@ -28,8 +29,14 @@ export interface WorkerDriverConnectionOptions { workerOptions?: WorkerOptions; } +interface CommandQueueItem { + cmd: SqliteCommand; + resolve?: (r: any) => void; + reject?: (e: SqliteDriverError) => void; +} + /** - * Driver connection using worker_threads. + * Driver connection using Web Workers. */ export class WorkerDriverConnection implements SqliteDriverConnection { worker: Worker; @@ -40,18 +47,21 @@ export class WorkerDriverConnection implements SqliteDriverConnection { private nextId = 1; private options: WorkerDriverConnectionOptions; - buffer: CommandQueueItem[] = []; + private buffer: CommandQueueItem[] = []; + private inProgress = 0; constructor(worker: Worker, options: WorkerDriverConnectionOptions) { this.worker = worker; this.options = options; + worker.addEventListener('error', (err) => { console.error('worker error', err.message, err); }); + this.ready = new Promise((resolve) => { worker.addEventListener('message', (event) => { const { id, value } = event.data; - if (id == 0) { + if (id === 0) { resolve(); return; } @@ -63,7 +73,6 @@ export class WorkerDriverConnection implements SqliteDriverConnection { callback(value); }); }); - this.worker = worker; } open() { @@ -76,32 +85,32 @@ export class WorkerDriverConnection implements SqliteDriverConnection { cmd: { type: SqliteCommandType.prepare, id, - bigint: options?.bigint, - persist: options?.persist, - rawResults: options?.rawResults, - sql + sql, + autoFinalize: options?.autoFinalize } }); + this._maybeFlush(); return new WorkerDriverStatement(this, id); } - async getLastChanges(): Promise { - return await this._push({ - type: SqliteCommandType.changes - }); - } - - async sync(): Promise { - await this._push({ - type: SqliteCommandType.sync - }); + async close() { + if (this.closing) { + return; + } + this.closing = true; + await this._flush(); + const r: any = await this.post('close', {}); + if (r?.error) { + throw r.error; + } + await this.worker.terminate(); } _push(cmd: T): Promise> { - const d = new Deferred(); - this.buffer.push({ cmd, resolve: d.resolve, reject: d.reject }); + const deferred = new Deferred(); + this.buffer.push({ cmd, resolve: deferred.resolve, reject: deferred.reject }); this._maybeFlush(); - return d.promise as Promise>; + return deferred.promise as Promise>; } _send(cmd: SqliteCommand): void { @@ -109,10 +118,6 @@ export class WorkerDriverConnection implements SqliteDriverConnection { this._maybeFlush(); } - log(...args: any[]) { - console.log(this.options.path, this.options.connectionName, ...args); - } - private registerCallback(callback: (value: any) => void) { const id = this.nextCallbackId++; this.callbacks.set(id, callback); @@ -136,55 +141,41 @@ export class WorkerDriverConnection implements SqliteDriverConnection { return result; } - async close() { - if (this.closing) { - return; - } - this.closing = true; - await this._flush(); - const r: any = await this.post('close', {}); - if (r?.error) { - throw r.error; - } - await this.worker.terminate(); - } - - private inProgress = 0; - - async _flush() { + private async _flush() { const commands = this.buffer; - if (commands.length == 0) { + if (commands.length === 0) { return; } this.buffer = []; - const r = await this._execute(commands.map((c) => c.cmd)); + const responses = await this._execute(commands.map((c) => c.cmd)); for (let i = 0; i < commands.length; i++) { - const c = commands[i]; - const rr = r[i]; - if (rr == null) { - c.reject?.({ message: 'no result received', code: '' }); - } else if (isErrorResponse(rr)) { - c.reject?.(rr.error); - } else if (c.resolve) { - c.resolve!(rr.value); + const entry = commands[i]; + const response = responses[i]; + if (response == null) { + entry.reject?.({ message: 'no result received', code: '' }); + } else if (isErrorResponse(response)) { + entry.reject?.(response.error); + } else if (entry.resolve) { + entry.resolve(response.value); } } } - async _maybeFlush() { - if (this.inProgress <= 2) { - this.inProgress += 1; - try { - while (this.buffer.length > 0) { - await this._flush(); - } - } finally { - this.inProgress -= 1; + private async _maybeFlush() { + if (this.inProgress > 2) { + return; + } + this.inProgress += 1; + try { + while (this.buffer.length > 0) { + await this._flush(); } + } finally { + this.inProgress -= 1; } } - async _execute( + private async _execute( commands: T ): Promise> { return await this.post('execute', commands); @@ -192,9 +183,7 @@ export class WorkerDriverConnection implements SqliteDriverConnection { onUpdate( listener: UpdateListener, - options?: - | { tables?: string[] | undefined; batchLimit?: number | undefined } - | undefined + options?: { tables?: string[] | undefined; batchLimit?: number | undefined } ): () => void { throw new Error('Not implemented'); } @@ -207,42 +196,72 @@ class WorkerDriverStatement implements SqliteDriverStatement { private driver: WorkerDriverConnection, private id: number ) { - if (typeof Symbol.dispose != 'undefined') { + if (typeof Symbol.dispose !== 'undefined') { this[Symbol.dispose] = () => this.finalize(); } } - async getColumns(): Promise { + async all( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { return this.driver ._push({ - type: SqliteCommandType.parse, - id: this.id + type: SqliteCommandType.query, + id: this.id, + parameters, + options }) - .then((r) => r.columns); + .then((result) => result.rows as SqliteObjectRow[]); } - bind(parameters: SqliteParameterBinding): void { - this.driver._send({ - type: SqliteCommandType.bind, - id: this.id, - parameters: parameters - }); + async allArray( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { + return this.driver + ._push({ + type: SqliteCommandType.query, + id: this.id, + parameters, + options, + array: true + }) + .then((result) => result.rows as SqliteArrayRow[]); } - async step(n?: number, options?: StepOptions): Promise { - return this.driver._push({ - type: SqliteCommandType.step, - id: this.id, - n: n, - requireTransaction: options?.requireTransaction - }); + stream( + _parameters?: SqliteParameterBinding, + _options?: StreamQueryOptions + ): AsyncIterableIterator { + throw new Error('Method not implemented.'); + } + + streamArray( + _parameters?: SqliteParameterBinding, + _options?: StreamQueryOptions + ): AsyncIterableIterator { + throw new Error('Method not implemented.'); + } + + async getColumns(): Promise { + return this.driver + ._push({ + type: SqliteCommandType.parse, + id: this.id + }) + .then((result) => result.columns); } - async run(options?: StepOptions): Promise { + async run( + parameters?: SqliteParameterBinding, + options?: QueryOptions + ): Promise { return this.driver._push({ type: SqliteCommandType.run, id: this.id, - requireTransaction: options?.requireTransaction + parameters, + options }); } @@ -252,18 +271,4 @@ class WorkerDriverStatement implements SqliteDriverStatement { id: this.id }); } - - reset(options?: ResetOptions): void { - this.driver._send({ - type: SqliteCommandType.reset, - id: this.id, - clearBindings: options?.clearBindings - }); - } -} - -interface CommandQueueItem { - cmd: SqliteCommand; - resolve?: (r: any) => void; - reject?: (e: SqliteDriverError) => void; } diff --git a/packages/wa-sqlite-driver/test/src/concurrency.test.ts b/packages/wa-sqlite-driver/test/src/concurrency.test.ts index b5fd39b..8364cc0 100644 --- a/packages/wa-sqlite-driver/test/src/concurrency.test.ts +++ b/packages/wa-sqlite-driver/test/src/concurrency.test.ts @@ -30,11 +30,11 @@ describe('concurrency tests', () => { using s1 = connection.prepare( 'create table test_data(id integer primary key, data text)' ); - await s1.step(); + await s1.run(); using s2 = connection.prepare( "insert into test_data(data) values('test')" ); - await s2.step(); + await s2.run(); } let promises: Promise[] = []; @@ -47,15 +47,15 @@ describe('concurrency tests', () => { }); using b = connection.prepare('begin immediate'); - await b.step(); + await b.run(); using s = connection.prepare('select * from test_data'); - const { rows } = await s.step(); + const rows = await s.all(); expect(rows).toEqual([{ id: 1, data: 'test' }]); await new Promise((resolve) => setTimeout(resolve, 500)); using e = connection.prepare('commit'); - await e.step(); + await e.run(); console.log('tx done in', Date.now() - start); })(); promises.push(p); @@ -70,15 +70,15 @@ describe('concurrency tests', () => { }); using b = connection.prepare('begin immediate'); - await b.step(); + await b.run(); using s = connection.prepare('select * from test_data'); - const { rows } = await s.step(); + const rows = await s.all(); expect(rows).toEqual([{ id: 1, data: 'test' }]); await new Promise((resolve) => setTimeout(resolve, 500)); using e = connection.prepare('commit'); - await e.step(); + await e.run(); console.log('tx done in', Date.now() - start); })(); promises.push(p); diff --git a/packages/wa-sqlite-driver/test/src/import-meta.d.ts b/packages/wa-sqlite-driver/test/src/import-meta.d.ts new file mode 100644 index 0000000..2b2ed12 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/import-meta.d.ts @@ -0,0 +1,6 @@ +interface ImportMeta { + glob( + pattern: string, + options: { as: 'raw'; eager: true } + ): Record; +} diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts index 281bc39..e8e93ce 100644 --- a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -2,8 +2,8 @@ import { describe, test } from '@sqlite-js/driver-tests'; import { SqliteError, SqliteValue } from '@sqlite-js/driver'; import type { ReservedConnection, - SqliteDriverConnectionPool, - SqliteRowRaw + SqliteArrayRow, + SqliteDriverConnectionPool } from '@sqlite-js/driver'; import { waSqliteSingleWorker } from '../../lib/index.js'; @@ -11,11 +11,11 @@ import { waSqliteSingleWorker } from '../../lib/index.js'; const scriptModules = import.meta.glob('./mptest/**/*', { as: 'raw', eager: true -}); +}) as Record; const scriptMap = new Map(); for (const [key, value] of Object.entries(scriptModules)) { - scriptMap.set(normalizePath(key), value as string); + scriptMap.set(normalizePath(key), value); } const topLevelScripts = [...scriptMap.keys()] @@ -483,14 +483,13 @@ class MptestRunner { expr: string ): Promise { const sql = `SELECT ${expr}`; - const statement = context.connection.prepare(sql, { rawResults: true }); + const statement = context.connection.prepare(sql); try { - const { rows } = await statement.step(); - if (!rows || rows.length === 0) { + const rows = await statement.allArray(); + if (rows.length === 0) { return false; } - const rawRows = rows as SqliteRowRaw[]; - const value = rawRows[0]?.[0]; + const value = rows[0]?.[0]; if (value == null) { return false; } @@ -522,13 +521,12 @@ class MptestRunner { if (!hasNonCommentContent(sql)) { continue; } - const statement = context.connection.prepare(sql, { rawResults: true }); + const statement = context.connection.prepare(sql); try { - const { rows } = await statement.step(); - if (rows) { - const rawRows = rows as SqliteRowRaw[]; - this.logSqlExecution(context, sql, rawRows); - for (const row of rawRows) { + const rows = await statement.allArray(); + if (rows.length > 0) { + this.logSqlExecution(context, sql, rows); + for (const row of rows) { for (const value of row) { result.append(value); } @@ -589,7 +587,7 @@ class MptestRunner { private logSqlExecution( context: ScriptContext, placeholder: string, - values: SqliteRowRaw[] + values: SqliteArrayRow[] ): void { console.log( `${context.displayName} SQL values ${placeholder} => ${JSON.stringify(values)}` diff --git a/packages/wa-sqlite-driver/test/tsconfig.json b/packages/wa-sqlite-driver/test/tsconfig.json index 42305e6..a005889 100644 --- a/packages/wa-sqlite-driver/test/tsconfig.json +++ b/packages/wa-sqlite-driver/test/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../../tsconfig.base.json", "compilerOptions": { "lib": ["ES2022", "DOM"], - "noEmit": true + "noEmit": true, + "outDir": "lib" }, "include": ["src"], "references": [{ "path": "../" }] From a591fce287ae3ee3fe0645738b6d8f317e585b9c Mon Sep 17 00:00:00 2001 From: Ralf Kistner Date: Sun, 26 Oct 2025 22:30:10 -0600 Subject: [PATCH 25/25] Refactor tests. --- .../test/src/mptest-runner.test.ts | 1189 +---------------- .../test/src/mptest-runner.ts | 1147 ++++++++++++++++ 2 files changed, 1167 insertions(+), 1169 deletions(-) create mode 100644 packages/wa-sqlite-driver/test/src/mptest-runner.ts diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts index e8e93ce..7260d9b 100644 --- a/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.test.ts @@ -1,1177 +1,28 @@ import { describe, test } from '@sqlite-js/driver-tests'; -import { SqliteError, SqliteValue } from '@sqlite-js/driver'; -import type { - ReservedConnection, - SqliteArrayRow, - SqliteDriverConnectionPool -} from '@sqlite-js/driver'; +import { MptestRunner, sanitizeForFilename } from './mptest-runner.js'; -import { waSqliteSingleWorker } from '../../lib/index.js'; - -const scriptModules = import.meta.glob('./mptest/**/*', { - as: 'raw', - eager: true -}) as Record; - -const scriptMap = new Map(); -for (const [key, value] of Object.entries(scriptModules)) { - scriptMap.set(normalizePath(key), value); -} - -const topLevelScripts = [...scriptMap.keys()] - .filter((name) => name.endsWith('.test')) - .sort(); - -interface ScriptContext { - clientId: number; - connection: ReservedConnection; - filename: string; - displayName: string; -} - -interface TokenInfo { - length: number; - newlines: number; -} - -class ResultBuffer { - private parts: string[] = []; - - append(value: SqliteValue | string): void { - this.parts.push(formatTerm(value)); - } - - appendError(error: SqliteError): void { - const code = error.code ?? 'SQLITE_ERROR'; - this.parts.push(formatTerm(`error(${code})`)); - this.parts.push(formatTerm(error.message)); - } - - reset(): void { - this.parts = []; - } - - tokens(): string[] { - return [...this.parts]; - } - - toString(): string { - return this.parts.join(' '); - } -} - -interface ClientContext { - id: number; - pool: SqliteDriverConnectionPool; - reserved: ReservedConnection; - queue: Promise; - pending: Set>; -} - -interface TaskControl { - promise: Promise; - resolve: () => void; - reject: (err: unknown) => void; - isResolved: () => boolean; -} - -class MptestRunner { - private clients = new Map(); - private pendingTasks = new Set>(); - - constructor(private readonly dbPath: string) {} - - async runScript(scriptName: string): Promise { - const script = requireScript(scriptName); - const master = await this.ensureClient(0); - await this.runScriptInternal( - { - clientId: 0, - connection: master.reserved, - filename: scriptName, - displayName: baseName(scriptName) - }, - script, - 1 - ); - await this.waitForAll(0); - } - - async close(): Promise { - const errors: Error[] = []; - await this.waitForAll(0).catch((err) => { - errors.push(err instanceof Error ? err : new Error(String(err))); - }); - await Promise.all( - [...this.clients.values()].map(async (client) => { - try { - await client.reserved.release(); - } catch (err) { - errors.push(err instanceof Error ? err : new Error(String(err))); - } - try { - await client.pool.close(); - } catch (err) { - errors.push(err instanceof Error ? err : new Error(String(err))); - } - }) - ); - this.clients.clear(); - if (errors.length > 0) { - throw errors[0]; - } - } - - private async ensureClient(clientId: number): Promise { - let client = this.clients.get(clientId); - if (client) { - return client; - } - const pool = waSqliteSingleWorker(this.dbPath); - const reserved = await pool.reserveConnection(); - client = { - id: clientId, - pool, - reserved, - queue: Promise.resolve(), - pending: new Set() - }; - this.clients.set(clientId, client); - return client; - } - - private trackTask(client: ClientContext, taskPromise: Promise): void { - client.pending.add(taskPromise); - this.pendingTasks.add(taskPromise); - taskPromise.finally(() => { - client.pending.delete(taskPromise); - this.pendingTasks.delete(taskPromise); - }); - } - - private createTaskControl(): TaskControl { - let resolved = false; - let resolveFn: () => void = () => {}; - let rejectFn: (err: unknown) => void = () => {}; - const promise = new Promise((resolve, reject) => { - resolveFn = () => { - if (!resolved) { - resolved = true; - resolve(); - } - }; - rejectFn = (err) => { - if (!resolved) { - resolved = true; - reject(err); - } - }; - }); - return { - promise, - resolve: resolveFn, - reject: rejectFn, - isResolved: () => resolved - }; - } - - private async scheduleTask( - parent: ScriptContext, - clientId: number, - script: string, - startLine: number, - taskLabel: string - ): Promise { - const client = await this.ensureClient(clientId); - const taskControl = this.createTaskControl(); - this.trackTask(client, taskControl.promise); - const run = async () => { - await this.runScriptInternal( - { - clientId, - connection: client.reserved, - filename: parent.filename, - displayName: `${parent.displayName}#client${clientId}:${taskLabel}` - }, - script, - startLine, - taskControl - ); - taskControl.resolve(); - }; - const task = client.queue.then(run).catch((err) => { - if (taskControl.isResolved()) { - return; - } - this.logError(`task failure client=${clientId}`, err); - taskControl.reject(err); - throw err; - }); - client.queue = task.catch(() => {}); - } - - private async waitForAll(timeoutMs: number): Promise { - await this.waitWithTimeout( - [...this.pendingTasks], - 'all clients', - timeoutMs - ); - } - - private async waitForClient( - clientId: number, - timeoutMs: number - ): Promise { - const client = this.clients.get(clientId); - const pending = client ? [...client.pending] : []; - await this.waitWithTimeout(pending, `client ${clientId}`, timeoutMs); - } - - private async waitWithTimeout( - promises: Promise[], - label: string, - timeoutMs: number - ): Promise { - if (promises.length === 0) { - return; - } - const waitPromise = Promise.all(promises).then(() => undefined); - if (timeoutMs <= 0) { - await this.withProgress(waitPromise, label); - return; - } - let handle: ReturnType | undefined; - let progressHandle: ReturnType | undefined; - try { - progressHandle = this.createProgressLogger(label, timeoutMs); - await Promise.race([ - this.withProgress(waitPromise, label, progressHandle), - new Promise((_, reject) => { - handle = setTimeout( - () => - reject( - new Error(`timeout waiting for ${label} (${timeoutMs}ms)`) - ), - timeoutMs - ); - }) - ]); - } finally { - if (handle) { - clearTimeout(handle); - } - if (progressHandle) { - clearInterval(progressHandle); - } - } - } - - private createProgressLogger( - label: string, - timeoutMs: number - ): ReturnType { - const start = Date.now(); - return setInterval( - () => { - const elapsed = Date.now() - start; - const pending = this.describePending(); - console.log( - `[wait] ${label} elapsed=${elapsed}ms timeout=${timeoutMs}ms pending=${pending}` - ); - }, - Math.min(1000, Math.max(250, timeoutMs / 10)) - ); - } - - private async withProgress( - promise: Promise, - label: string, - progressHandle?: ReturnType - ): Promise { - try { - await promise; - } finally { - if (progressHandle) { - clearInterval(progressHandle); - } - } - } - - private describePending(): string { - const entries: string[] = []; - for (const [clientId, client] of this.clients.entries()) { - const size = client.pending.size; - if (size > 0) { - entries.push(`client${clientId}:${size}`); - } - } - if (entries.length === 0) { - return 'none'; - } - return entries.join(','); - } - - private async runScriptInternal( - context: ScriptContext, - script: string, - initialLine: number, - taskControl?: TaskControl - ): Promise { - const result = new ResultBuffer(); - let index = 0; - let begin = 0; - let line = initialLine; - while (index < script.length) { - const prevLine = line; - const token = tokenLength(script, index); - if (token.length <= 0) { - break; - } - line += token.newlines; - const chr = script[index]; - if (isWhitespace(chr) || (chr === '/' && script[index + 1] === '*')) { - index += token.length; - continue; - } - if ( - chr !== '-' || - script[index + 1] !== '-' || - !isAlpha(script[index + 2]) - ) { - index += token.length; - continue; - } - if (index > begin) { - const sqlChunk = script.slice(begin, index); - this.logSqlChunk(context, prevLine, sqlChunk); - await this.executeSql(context, sqlChunk, result); - } - let consumed = token.length; - const command = parseCommand(script, index, token.length); - this.logCommand(context, prevLine, command.name, command.payload); - switch (command.name) { - case 'sleep': - await sleepMs(Number(command.args[0] ?? '0')); - break; - case 'match': { - const expectedRaw = command.payload.trim(); - const expectedTokens = tokenizeMatch(expectedRaw); - const actualTokens = result.tokens(); - if (!tokensEqual(expectedTokens, actualTokens)) { - throw new Error( - `${context.displayName}:${prevLine} expected [${expectedRaw}] but got [${result.toString()}]` - ); - } - result.reset(); - break; - } - case 'print': { - const message = command.payload.replace(/^\s*/, ''); - if (message.length > 0) { - this.logMessage(context, message); - } - break; - } - case 'task': { - const clientId = Number(command.args[0]); - if (!Number.isInteger(clientId) || clientId < 0) { - throw new Error( - `${context.displayName}:${prevLine} invalid client ${command.args[0]}` - ); - } - const lineRef = { value: line }; - const blockStart = index + consumed; - const blockLength = findEnd(script, blockStart, lineRef); - const endToken = tokenLength(script, blockStart + blockLength); - line = lineRef.value + endToken.newlines; - consumed += blockLength + endToken.length; - const taskScript = script.slice(blockStart, blockStart + blockLength); - const taskLabel = command.args[1] - ? command.args[1] - : `${baseName(context.filename)}:${prevLine}`; - await this.scheduleTask( - context, - clientId, - taskScript, - prevLine + 1, - taskLabel - ); - break; - } - case 'finish': { - taskControl?.resolve(); - break; - } - case 'exit': { - const code = Number(command.args[0] ?? '0'); - taskControl?.resolve(); - await this.terminateClient(context, code); - return; - } - case 'wait': { - const target = command.args[0] ?? 'all'; - const timeout = command.args[1] ? Number(command.args[1]) : 120_000; - if (target === 'all') { - await this.waitForAll(timeout); - } else { - await this.waitForClient(Number(target), timeout); - } - break; - } - case 'source': { - const sourceName = command.args[0]; - if (!sourceName) { - throw new Error( - `${context.displayName}:${prevLine} missing source filename` - ); - } - const resolved = resolveSourcePath(context.filename, sourceName); - const subScript = requireScript(resolved); - const master = await this.ensureClient(0); - await this.runScriptInternal( - { - clientId: 0, - connection: master.reserved, - filename: resolved, - displayName: baseName(resolved) - }, - subScript, - 1, - taskControl - ); - break; - } - case 'if': { - const condition = await this.evaluateCondition( - context, - command.payload - ); - if (!condition) { - const lineRef = { value: line }; - const skip = findEndif(script, index + consumed, true, lineRef); - line = lineRef.value; - consumed += skip; - } - break; - } - case 'else': { - const lineRef = { value: line }; - const skip = findEndif(script, index + consumed, false, lineRef); - line = lineRef.value; - consumed += skip; - break; - } - case 'endif': - break; - default: - throw new Error( - `${context.displayName}:${prevLine} unknown command --${command.name}` - ); - } - begin = index + consumed; - index += consumed; - } - if (begin < script.length) { - const remainder = script.slice(begin); - this.logSqlChunk(context, line, remainder); - await this.executeSql(context, remainder, result); - } - } - - private async evaluateCondition( - context: ScriptContext, - expr: string - ): Promise { - const sql = `SELECT ${expr}`; - const statement = context.connection.prepare(sql); - try { - const rows = await statement.allArray(); - if (rows.length === 0) { - return false; - } - const value = rows[0]?.[0]; - if (value == null) { - return false; - } - if (typeof value === 'number') { - return value !== 0; - } - if (typeof value === 'bigint') { - return value !== BigInt(0); - } - if (typeof value === 'string') { - return value.length > 0; - } - return true; - } finally { - statement.finalize(); - } - } - - private async executeSql( - context: ScriptContext, - chunk: string, - result: ResultBuffer - ): Promise { - const statements = splitSql(chunk); - for (const sql of statements) { - if (!sql.trim()) { - continue; - } - if (!hasNonCommentContent(sql)) { - continue; - } - const statement = context.connection.prepare(sql); - try { - const rows = await statement.allArray(); - if (rows.length > 0) { - this.logSqlExecution(context, sql, rows); - for (const row of rows) { - for (const value of row) { - result.append(value); - } - } - } - } catch (err) { - if (isSqliteError(err)) { - this.logError('sqlite error', err); - result.appendError(err); - } else { - this.logError('sql execution error', err); - throw err; - } - } finally { - statement.finalize(); - } - } - } - - private logMessage(context: ScriptContext, message: string): void { - const trimmed = message.replace(/\s+$/, ''); - if (!trimmed) { - return; - } - console.log(`${context.displayName} ${trimmed}`); - } - - private logCommand( - context: ScriptContext, - line: number, - command: string, - payload: string - ): void { - const detail = payload ? ` ${payload.trim()}` : ''; - console.log(`${context.displayName}:${line} --${command}${detail}`); - } - - private logSqlChunk( - context: ScriptContext, - line: number, - chunk: string - ): void { - const summary = chunk.trim().replace(/\s+/g, ' '); - if (!summary) { - return; - } - console.log(`${context.displayName}:${line} SQL ${summary}`); - } - - private logError(prefix: string, err: unknown): void { - if (err instanceof Error) { - console.error(`[error] ${prefix}: ${err.message}\n${err.stack}`); - } else { - console.error(`[error] ${prefix}: ${String(err)}`); - } - } - - private logSqlExecution( - context: ScriptContext, - placeholder: string, - values: SqliteArrayRow[] - ): void { - console.log( - `${context.displayName} SQL values ${placeholder} => ${JSON.stringify(values)}` - ); - } - - private async terminateClient( - context: ScriptContext, - exitCode: number - ): Promise { - const { clientId, displayName } = context; - const client = this.clients.get(clientId); - if (!client) { - return; - } - this.clients.delete(clientId); - try { - await client.reserved.release(); - } catch (err) { - this.logError(`release error client=${clientId}`, err); - if (exitCode === 0) { - throw err; - } - } - try { - await client.pool.close(); - } catch (err) { - this.logError(`close error client=${clientId}`, err); - if (exitCode === 0) { - throw err; - } - } - if (exitCode !== 0) { - console.log(`${displayName} exited with code ${exitCode}`); - } +async function runMptestScript(script: string): Promise { + const dbPath = `mptest-${sanitizeForFilename(script)}-${Math.random() + .toString(36) + .slice(2)}.db`; + const runner = new MptestRunner(dbPath); + try { + await runner.runScript(script); + } finally { + await runner.close(); } } describe('mptest scripts', { timeout: 60_000 }, () => { - // const scripts = topLevelScripts; - // const scripts = ['mptest/multiwrite01.test']; - // const scripts = ['mptest/config02.test']; - // const scripts = ['mptest/crash01.test']; - const scripts = [ - // 'mptest/multiwrite01.test', - 'mptest/config02.test' - // 'mptest/crash01.test' - ]; - for (const script of scripts) { - test(script, async () => { - const dbPath = `mptest-${sanitizeForFilename(script)}-${Math.random() - .toString(36) - .slice(2)}.db`; - const runner = new MptestRunner(dbPath); - try { - await runner.runScript(script); - } finally { - await runner.close(); - } - }); - } -}); - -function normalizePath(input: string): string { - return input.startsWith('./') ? input.slice(2) : input; -} - -function baseName(file: string): string { - const normalized = file.replace(/\\/g, '/'); - const idx = normalized.lastIndexOf('/'); - return idx >= 0 ? normalized.slice(idx + 1) : normalized; -} - -function sanitizeForFilename(input: string): string { - return baseName(input).replace(/[^a-zA-Z0-9_-]+/g, '_'); -} - -function requireScript(path: string): string { - const normalized = normalizePath(path); - const script = scriptMap.get(normalized); - if (script == null) { - throw new Error(`Unknown script: ${normalized}`); - } - return script; -} - -function resolveSourcePath(currentFile: string, relative: string): string { - if (relative.startsWith('/')) { - return normalizePath(relative.slice(1)); - } - const currentParts = normalizePath(currentFile).split('/'); - currentParts.pop(); - for (const part of relative.split('/')) { - if (part === '' || part === '.') { - continue; - } - if (part === '..') { - if (currentParts.length > 0) { - currentParts.pop(); - } - } else { - currentParts.push(part); - } - } - return currentParts.join('/'); -} - -function isWhitespace(ch: string | undefined): boolean { - if (ch == null) { - return false; - } - return ( - ch === ' ' || - ch === '\n' || - ch === '\r' || - ch === '\t' || - ch === '\f' || - ch === '\v' - ); -} - -function isAlpha(ch: string | undefined): boolean { - if (!ch) { - return false; - } - const code = ch.charCodeAt(0); - return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); -} - -function tokenLength(script: string, start: number): TokenInfo { - let n = 0; - let newlines = 0; - const first = script[start]; - if (first == null) { - return { length: 0, newlines: 0 }; - } - if (isWhitespace(first) || (first === '/' && script[start + 1] === '*')) { - let inComment = first === '/' ? 1 : 0; - if (first === '/') { - n = 2; - } - while (true) { - const c = script[start + n]; - n++; - if (c == null) { - break; - } - if (c === '\n') { - newlines++; - } - if (isWhitespace(c)) { - continue; - } - if (inComment && c === '*' && script[start + n] === '/') { - n++; - inComment = 0; - } else if (!inComment && c === '/' && script[start + n] === '*') { - n++; - inComment = 1; - } else if (!inComment) { - break; - } - } - n--; - return { length: n, newlines }; - } - if (first === '-' && script[start + 1] === '-') { - n = 2; - while (start + n < script.length && script[start + n] !== '\n') { - n++; - } - if (start + n < script.length) { - newlines++; - n++; - } - return { length: n, newlines }; - } - if (first === '"' || first === "'") { - const delim = first; - n = 1; - while (start + n < script.length) { - const c = script[start + n]; - if (c === '\n') { - newlines++; - } - n++; - if (c === delim) { - if (script[start + n] !== delim) { - break; - } - n++; - } - } - return { length: n, newlines }; - } - n = 1; - while (start + n < script.length) { - const c = script[start + n]; - if (!c || isWhitespace(c) || c === '"' || c === "'" || c === ';') { - break; - } - n++; - } - return { length: n, newlines }; -} - -function findEnd( - script: string, - start: number, - lineRef: { value: number } -): number { - let offset = 0; - while (start + offset < script.length) { - if ( - script.startsWith('--end', start + offset) && - isWhitespace(script[start + offset + 5] ?? '\n') - ) { - break; - } - const token = tokenLength(script, start + offset); - lineRef.value += token.newlines; - if (token.length <= 0) { - break; - } - offset += token.length; - } - return offset; -} - -function findEndif( - script: string, - start: number, - stopAtElse: boolean, - lineRef: { value: number } -): number { - let offset = 0; - while (start + offset < script.length) { - const current = start + offset; - const token = tokenLength(script, current); - lineRef.value += token.newlines; - if ( - script.startsWith('--endif', current) && - isWhitespace(script[current + 7] ?? '\n') - ) { - return offset + token.length; - } - if ( - stopAtElse && - script.startsWith('--else', current) && - isWhitespace(script[current + 6] ?? '\n') - ) { - return offset + token.length; - } - if ( - script.startsWith('--if', current) && - isWhitespace(script[current + 4] ?? '\n') - ) { - const inner = findEndif(script, current + token.length, false, lineRef); - offset += token.length + inner; - } else { - if (token.length <= 0) { - break; - } - offset += token.length; - } - } - return offset; -} - -interface ParsedCommand { - name: string; - args: string[]; - payload: string; -} - -function parseCommand( - script: string, - start: number, - length: number -): ParsedCommand { - const segment = script.slice(start, start + length).replace(/\r?\n?$/, ''); - const trimmed = segment.trim(); - const body = trimmed.slice(2).trim(); - const firstSpace = body.search(/\s/); - let name: string; - let payload: string; - if (firstSpace === -1) { - name = body; - payload = ''; - } else { - name = body.slice(0, firstSpace); - payload = body.slice(firstSpace).trimStart(); - } - const args = payload ? payload.split(/\s+/) : []; - return { name, args, payload }; -} - -function splitSql(script: string): string[] { - const statements: string[] = []; - let current = ''; - let inSingle = false; - let inDouble = false; - let inBracket = false; - let inLineComment = false; - let inBlockComment = false; - - for (let i = 0; i < script.length; i++) { - const c = script[i]; - const next = script[i + 1]; - if (inLineComment) { - current += c; - if (c === '\n') { - inLineComment = false; - } - continue; - } - if (inBlockComment) { - current += c; - if (c === '*' && next === '/') { - current += next; - i++; - inBlockComment = false; - } - continue; - } - if (!inSingle && !inDouble) { - if (c === '-' && next === '-') { - inLineComment = true; - current += c; - continue; - } - if (c === '/' && next === '*') { - inBlockComment = true; - current += c; - continue; - } - } - if (c === "'" && !inDouble) { - inSingle = !inSingle; - current += c; - if (inSingle && next === "'") { - current += next; - i++; - } - continue; - } - if (c === '"' && !inSingle) { - inDouble = !inDouble; - current += c; - if (inDouble && next === '"') { - current += next; - i++; - } - continue; - } - if (!inSingle && !inDouble) { - if (c === '[') { - inBracket = true; - } else if (c === ']' && inBracket) { - inBracket = false; - } - } - if (!inSingle && !inDouble && !inBracket && c === ';') { - const statement = current.trim(); - if (statement) { - statements.push(statement); - } - current = ''; - continue; - } - current += c; - } - const trailing = current.trim(); - if (trailing) { - statements.push(trailing); - } - return statements; -} + test('mptest/multiwrite01.test', async () => { + await runMptestScript('mptest/multiwrite01.test'); + }); -function tokenizeMatch(input: string): string[] { - const tokens: string[] = []; - let i = 0; - while (i < input.length) { - while (i < input.length && /\s/.test(input[i]!)) i++; - if (i >= input.length) { - break; - } - if (input[i] === "'") { - const start = i; - i++; - while (i < input.length) { - if (input[i] === "'") { - i++; - if (input[i] === "'") { - i++; - continue; - } - break; - } - i++; - } - tokens.push(input.slice(start, i)); - } else { - const start = i; - while (i < input.length && !/\s/.test(input[i]!)) i++; - tokens.push(input.slice(start, i)); - } - } - return tokens; -} - -function tokensEqual(expected: string[], actual: string[]): boolean { - if (expected.length !== actual.length) { - return false; - } - for (let i = 0; i < expected.length; i++) { - const e = expected[i]; - const a = actual[i]; - if (e === a) { - continue; - } - if (normalizeNumeric(e) === normalizeNumeric(a)) { - continue; - } - return false; - } - return true; -} + test('mptest/config02.test', async () => { + await runMptestScript('mptest/config02.test'); + }); -function normalizeNumeric(token: string): string { - if (!/^-?\d+(?:\.\d+)?$/.test(token)) { - return token; - } - if (!token.includes('.')) { - return token; - } - const negative = token.startsWith('-'); - let body = negative ? token.slice(1) : token; - body = body.replace(/\.0+$/, ''); - body = body.replace(/(\.\d*?[1-9])0+$/, '$1'); - if (body.endsWith('.')) { - body = body.slice(0, -1); - } - if (!body) { - body = '0'; - } - return negative ? `-${body}` : body; -} - -function formatTerm(value: SqliteValue | string): string { - if (value == null) { - return 'nil'; - } - if (typeof value === 'string') { - return formatString(value); - } - if (typeof value === 'number' || typeof value === 'bigint') { - return String(value); - } - if (value instanceof Uint8Array) { - return `x'${toHex(value)}'`; - } - return formatString(String(value)); -} - -function formatString(value: string): string { - if (!/\s/.test(value) && !value.includes("'")) { - return value; - } - return `'${value.replace(/'/g, "''")}'`; -} - -function toHex(data: Uint8Array): string { - let hex = ''; - for (let i = 0; i < data.length; i++) { - const byte = data[i]; - hex += byte.toString(16).padStart(2, '0'); - } - return hex; -} - -function hasNonCommentContent(sql: string): boolean { - let i = 0; - let inSingle = false; - let inDouble = false; - let inBracket = false; - - const advance = (step = 1) => { - i += step; - }; - - while (i < sql.length) { - const c = sql[i]; - const next = sql[i + 1]; - - if (inSingle) { - if (c === "'") { - if (next === "'") { - advance(2); - continue; - } - inSingle = false; - advance(); - continue; - } - advance(); - continue; - } - if (inDouble) { - if (c === '"') { - if (next === '"') { - advance(2); - continue; - } - inDouble = false; - advance(); - continue; - } - advance(); - continue; - } - if (inBracket) { - if (c === ']') { - inBracket = false; - } - advance(); - continue; - } - - if (c === "'") { - inSingle = true; - advance(); - continue; - } - if (c === '"') { - inDouble = true; - advance(); - continue; - } - if (c === '[') { - inBracket = true; - advance(); - continue; - } - - if (c === '-' && next === '-') { - advance(2); - while (i < sql.length && sql[i] !== '\n') { - advance(); - } - continue; - } - if (c === '/' && next === '*') { - advance(2); - while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { - advance(); - } - if (i < sql.length) { - advance(2); - } - continue; - } - if (/\s/.test(c ?? '')) { - advance(); - continue; - } - return true; - } - return false; -} - -function sleepMs(ms: number): Promise { - if (ms <= 0) { - return Promise.resolve(); - } - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function isSqliteError(err: unknown): err is SqliteError { - return ( - err instanceof SqliteError || - (typeof err === 'object' && - err != null && - 'code' in err && - 'message' in err) - ); -} + test('mptest/crash01.test', async () => { + await runMptestScript('mptest/crash01.test'); + }); +}); diff --git a/packages/wa-sqlite-driver/test/src/mptest-runner.ts b/packages/wa-sqlite-driver/test/src/mptest-runner.ts new file mode 100644 index 0000000..0577ca6 --- /dev/null +++ b/packages/wa-sqlite-driver/test/src/mptest-runner.ts @@ -0,0 +1,1147 @@ +import { SqliteError, SqliteValue } from '@sqlite-js/driver'; +import type { + ReservedConnection, + SqliteDriverConnectionPool, + SqliteArrayRow +} from '@sqlite-js/driver'; + +import { waSqliteSingleWorker } from '../../lib/index.js'; + +const scriptModules = import.meta.glob('./mptest/**/*', { + as: 'raw', + eager: true +}); + +const scriptMap = new Map(); +for (const [key, value] of Object.entries(scriptModules)) { + scriptMap.set(normalizePath(key), value as string); +} + +interface ScriptContext { + clientId: number; + connection: ReservedConnection; + filename: string; + displayName: string; +} + +interface TokenInfo { + length: number; + newlines: number; +} + +class ResultBuffer { + private parts: string[] = []; + + append(value: SqliteValue | string): void { + this.parts.push(formatTerm(value)); + } + + appendError(error: SqliteError): void { + const code = error.code ?? 'SQLITE_ERROR'; + this.parts.push(formatTerm(`error(${code})`)); + this.parts.push(formatTerm(error.message)); + } + + reset(): void { + this.parts = []; + } + + tokens(): string[] { + return [...this.parts]; + } + + toString(): string { + return this.parts.join(' '); + } +} + +interface ClientContext { + id: number; + pool: SqliteDriverConnectionPool; + reserved: ReservedConnection; + queue: Promise; + pending: Set>; +} + +interface TaskControl { + promise: Promise; + resolve: () => void; + reject: (err: unknown) => void; + isResolved: () => boolean; +} + +export class MptestRunner { + private clients = new Map(); + private pendingTasks = new Set>(); + + constructor(private readonly dbPath: string) {} + + async runScript(scriptName: string): Promise { + const script = requireScript(scriptName); + const master = await this.ensureClient(0); + await this.runScriptInternal( + { + clientId: 0, + connection: master.reserved, + filename: scriptName, + displayName: baseName(scriptName) + }, + script, + 1 + ); + await this.waitForAll(0); + } + + async close(): Promise { + const errors: Error[] = []; + await this.waitForAll(0).catch((err) => { + errors.push(err instanceof Error ? err : new Error(String(err))); + }); + await Promise.all( + [...this.clients.values()].map(async (client) => { + try { + await client.reserved.release(); + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); + } + try { + await client.pool.close(); + } catch (err) { + errors.push(err instanceof Error ? err : new Error(String(err))); + } + }) + ); + this.clients.clear(); + if (errors.length > 0) { + throw errors[0]; + } + } + + private async ensureClient(clientId: number): Promise { + let client = this.clients.get(clientId); + if (client) { + return client; + } + const pool = waSqliteSingleWorker(this.dbPath); + const reserved = await pool.reserveConnection(); + client = { + id: clientId, + pool, + reserved, + queue: Promise.resolve(), + pending: new Set() + }; + this.clients.set(clientId, client); + return client; + } + + private trackTask(client: ClientContext, taskPromise: Promise): void { + client.pending.add(taskPromise); + this.pendingTasks.add(taskPromise); + taskPromise.finally(() => { + client.pending.delete(taskPromise); + this.pendingTasks.delete(taskPromise); + }); + } + + private createTaskControl(): TaskControl { + let resolved = false; + let resolveFn: () => void = () => {}; + let rejectFn: (err: unknown) => void = () => {}; + const promise = new Promise((resolve, reject) => { + resolveFn = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; + rejectFn = (err) => { + if (!resolved) { + resolved = true; + reject(err); + } + }; + }); + return { + promise, + resolve: resolveFn, + reject: rejectFn, + isResolved: () => resolved + }; + } + + private async scheduleTask( + parent: ScriptContext, + clientId: number, + script: string, + startLine: number, + taskLabel: string + ): Promise { + const client = await this.ensureClient(clientId); + const taskControl = this.createTaskControl(); + this.trackTask(client, taskControl.promise); + const run = async () => { + await this.runScriptInternal( + { + clientId, + connection: client.reserved, + filename: parent.filename, + displayName: `${parent.displayName}#client${clientId}:${taskLabel}` + }, + script, + startLine, + taskControl + ); + taskControl.resolve(); + }; + const task = client.queue.then(run).catch((err) => { + if (taskControl.isResolved()) { + return; + } + this.logError(`task failure client=${clientId}`, err); + taskControl.reject(err); + throw err; + }); + client.queue = task.catch(() => {}); + } + + private async waitForAll(timeoutMs: number): Promise { + await this.waitWithTimeout( + [...this.pendingTasks], + 'all clients', + timeoutMs + ); + } + + private async waitForClient( + clientId: number, + timeoutMs: number + ): Promise { + const client = this.clients.get(clientId); + const pending = client ? [...client.pending] : []; + await this.waitWithTimeout(pending, `client ${clientId}`, timeoutMs); + } + + private async waitWithTimeout( + promises: Promise[], + label: string, + timeoutMs: number + ): Promise { + if (promises.length === 0) { + return; + } + const waitPromise = Promise.all(promises).then(() => undefined); + if (timeoutMs <= 0) { + await this.withProgress(waitPromise, label); + return; + } + let handle: ReturnType | undefined; + let progressHandle: ReturnType | undefined; + try { + progressHandle = this.createProgressLogger(label, timeoutMs); + await Promise.race([ + this.withProgress(waitPromise, label, progressHandle), + new Promise((_, reject) => { + handle = setTimeout( + () => + reject( + new Error(`timeout waiting for ${label} (${timeoutMs}ms)`) + ), + timeoutMs + ); + }) + ]); + } finally { + if (handle) { + clearTimeout(handle); + } + if (progressHandle) { + clearInterval(progressHandle); + } + } + } + + private createProgressLogger( + label: string, + timeoutMs: number + ): ReturnType { + const start = Date.now(); + return setInterval( + () => { + const elapsed = Date.now() - start; + const pending = this.describePending(); + console.log( + `[wait] ${label} elapsed=${elapsed}ms timeout=${timeoutMs}ms pending=${pending}` + ); + }, + Math.min(1000, Math.max(250, timeoutMs / 10)) + ); + } + + private async withProgress( + promise: Promise, + label: string, + progressHandle?: ReturnType + ): Promise { + try { + await promise; + } finally { + if (progressHandle) { + clearInterval(progressHandle); + } + } + } + + private describePending(): string { + const entries: string[] = []; + for (const [clientId, client] of this.clients.entries()) { + const size = client.pending.size; + if (size > 0) { + entries.push(`client${clientId}:${size}`); + } + } + if (entries.length === 0) { + return 'none'; + } + return entries.join(','); + } + + private async runScriptInternal( + context: ScriptContext, + script: string, + initialLine: number, + taskControl?: TaskControl + ): Promise { + const result = new ResultBuffer(); + let index = 0; + let begin = 0; + let line = initialLine; + while (index < script.length) { + const prevLine = line; + const token = tokenLength(script, index); + if (token.length <= 0) { + break; + } + line += token.newlines; + const chr = script[index]; + if (isWhitespace(chr) || (chr === '/' && script[index + 1] === '*')) { + index += token.length; + continue; + } + if ( + chr !== '-' || + script[index + 1] !== '-' || + !isAlpha(script[index + 2]) + ) { + index += token.length; + continue; + } + if (index > begin) { + const sqlChunk = script.slice(begin, index); + this.logSqlChunk(context, prevLine, sqlChunk); + await this.executeSql(context, sqlChunk, result); + } + let consumed = token.length; + const command = parseCommand(script, index, token.length); + this.logCommand(context, prevLine, command.name, command.payload); + switch (command.name) { + case 'sleep': + await sleepMs(Number(command.args[0] ?? '0')); + break; + case 'match': { + const expectedRaw = command.payload.trim(); + const expectedTokens = tokenizeMatch(expectedRaw); + const actualTokens = result.tokens(); + if (!tokensEqual(expectedTokens, actualTokens)) { + throw new Error( + `${context.displayName}:${prevLine} expected [${expectedRaw}] but got [${result.toString()}]` + ); + } + result.reset(); + break; + } + case 'print': { + const message = command.payload.replace(/^\s*/, ''); + if (message.length > 0) { + this.logMessage(context, message); + } + break; + } + case 'task': { + const clientId = Number(command.args[0]); + if (!Number.isInteger(clientId) || clientId < 0) { + throw new Error( + `${context.displayName}:${prevLine} invalid client ${command.args[0]}` + ); + } + const lineRef = { value: line }; + const blockStart = index + consumed; + const blockLength = findEnd(script, blockStart, lineRef); + const endToken = tokenLength(script, blockStart + blockLength); + line = lineRef.value + endToken.newlines; + consumed += blockLength + endToken.length; + const taskScript = script.slice(blockStart, blockStart + blockLength); + const taskLabel = command.args[1] + ? command.args[1] + : `${baseName(context.filename)}:${prevLine}`; + await this.scheduleTask( + context, + clientId, + taskScript, + prevLine + 1, + taskLabel + ); + break; + } + case 'finish': { + taskControl?.resolve(); + break; + } + case 'exit': { + const code = Number(command.args[0] ?? '0'); + taskControl?.resolve(); + await this.terminateClient(context, code); + return; + } + case 'wait': { + const target = command.args[0] ?? 'all'; + const timeout = command.args[1] ? Number(command.args[1]) : 120_000; + if (target === 'all') { + await this.waitForAll(timeout); + } else { + await this.waitForClient(Number(target), timeout); + } + break; + } + case 'source': { + const sourceName = command.args[0]; + if (!sourceName) { + throw new Error( + `${context.displayName}:${prevLine} missing source filename` + ); + } + const resolved = resolveSourcePath(context.filename, sourceName); + const subScript = requireScript(resolved); + const master = await this.ensureClient(0); + await this.runScriptInternal( + { + clientId: 0, + connection: master.reserved, + filename: resolved, + displayName: baseName(resolved) + }, + subScript, + 1, + taskControl + ); + break; + } + case 'if': { + const condition = await this.evaluateCondition( + context, + command.payload + ); + if (!condition) { + const lineRef = { value: line }; + const skip = findEndif(script, index + consumed, true, lineRef); + line = lineRef.value; + consumed += skip; + } + break; + } + case 'else': { + const lineRef = { value: line }; + const skip = findEndif(script, index + consumed, false, lineRef); + line = lineRef.value; + consumed += skip; + break; + } + case 'endif': + break; + default: + throw new Error( + `${context.displayName}:${prevLine} unknown command --${command.name}` + ); + } + begin = index + consumed; + index += consumed; + } + if (begin < script.length) { + const remainder = script.slice(begin); + this.logSqlChunk(context, line, remainder); + await this.executeSql(context, remainder, result); + } + } + + private async evaluateCondition( + context: ScriptContext, + expr: string + ): Promise { + const sql = `SELECT ${expr}`; + const statement = context.connection.prepare(sql); + try { + const rows = await statement.allArray(); + if (!rows || rows.length === 0) { + return false; + } + const value = rows[0]?.[0]; + if (value == null) { + return false; + } + if (typeof value === 'number') { + return value !== 0; + } + if (typeof value === 'bigint') { + return value !== BigInt(0); + } + if (typeof value === 'string') { + return value.length > 0; + } + return true; + } finally { + statement.finalize(); + } + } + + private async executeSql( + context: ScriptContext, + chunk: string, + result: ResultBuffer + ): Promise { + const statements = splitSql(chunk); + for (const sql of statements) { + if (!sql.trim()) { + continue; + } + if (!hasNonCommentContent(sql)) { + continue; + } + const statement = context.connection.prepare(sql); + try { + const rows = await statement.allArray(); + if (rows.length > 0) { + this.logSqlExecution(context, sql, rows); + for (const row of rows) { + for (const value of row) { + result.append(value); + } + } + } + } catch (err) { + if (isSqliteError(err)) { + this.logError('sqlite error', err); + result.appendError(err); + } else { + this.logError('sql execution error', err); + throw err; + } + } finally { + statement.finalize(); + } + } + } + + private logMessage(context: ScriptContext, message: string): void { + const trimmed = message.replace(/\s+$/, ''); + if (!trimmed) { + return; + } + console.log(`${context.displayName} ${trimmed}`); + } + + private logCommand( + context: ScriptContext, + line: number, + command: string, + payload: string + ): void { + const detail = payload ? ` ${payload.trim()}` : ''; + console.log(`${context.displayName}:${line} --${command}${detail}`); + } + + private logSqlChunk( + context: ScriptContext, + line: number, + chunk: string + ): void { + const summary = chunk.trim().replace(/\s+/g, ' '); + if (!summary) { + return; + } + console.log(`${context.displayName}:${line} SQL ${summary}`); + } + + private logError(prefix: string, err: unknown): void { + if (err instanceof Error) { + console.error(`[error] ${prefix}: ${err.message}\n${err.stack}`); + } else { + console.error(`[error] ${prefix}: ${String(err)}`); + } + } + + private logSqlExecution( + context: ScriptContext, + placeholder: string, + values: SqliteArrayRow[] + ): void { + console.log( + `${context.displayName} SQL values ${placeholder} => ${JSON.stringify(values)}` + ); + } + + private async terminateClient( + context: ScriptContext, + exitCode: number + ): Promise { + const { clientId, displayName } = context; + const client = this.clients.get(clientId); + if (!client) { + return; + } + this.clients.delete(clientId); + try { + await client.reserved.release(); + } catch (err) { + this.logError(`release error client=${clientId}`, err); + if (exitCode === 0) { + throw err; + } + } + try { + await client.pool.close(); + } catch (err) { + this.logError(`close error client=${clientId}`, err); + if (exitCode === 0) { + throw err; + } + } + if (exitCode !== 0) { + console.log(`${displayName} exited with code ${exitCode}`); + } + } +} + +function normalizePath(input: string): string { + return input.startsWith('./') ? input.slice(2) : input; +} + +function baseName(file: string): string { + const normalized = file.replace(/\\/g, '/'); + const idx = normalized.lastIndexOf('/'); + return idx >= 0 ? normalized.slice(idx + 1) : normalized; +} + +export function sanitizeForFilename(input: string): string { + return baseName(input).replace(/[^a-zA-Z0-9_-]+/g, '_'); +} + +function requireScript(path: string): string { + const normalized = normalizePath(path); + const script = scriptMap.get(normalized); + if (script == null) { + throw new Error(`Unknown script: ${normalized}`); + } + return script; +} + +function resolveSourcePath(currentFile: string, relative: string): string { + if (relative.startsWith('/')) { + return normalizePath(relative.slice(1)); + } + const currentParts = normalizePath(currentFile).split('/'); + currentParts.pop(); + for (const part of relative.split('/')) { + if (part === '' || part === '.') { + continue; + } + if (part === '..') { + if (currentParts.length > 0) { + currentParts.pop(); + } + } else { + currentParts.push(part); + } + } + return currentParts.join('/'); +} + +function isWhitespace(ch: string | undefined): boolean { + if (ch == null) { + return false; + } + return ( + ch === ' ' || + ch === '\n' || + ch === '\r' || + ch === '\t' || + ch === '\f' || + ch === '\v' + ); +} + +function isAlpha(ch: string | undefined): boolean { + if (!ch) { + return false; + } + const code = ch.charCodeAt(0); + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122); +} + +function tokenLength(script: string, start: number): TokenInfo { + let n = 0; + let newlines = 0; + const first = script[start]; + if (first == null) { + return { length: 0, newlines: 0 }; + } + if (isWhitespace(first) || (first === '/' && script[start + 1] === '*')) { + let inComment = first === '/' ? 1 : 0; + if (first === '/') { + n = 2; + } + while (true) { + const c = script[start + n]; + n++; + if (c == null) { + break; + } + if (c === '\n') { + newlines++; + } + if (isWhitespace(c)) { + continue; + } + if (inComment && c === '*' && script[start + n] === '/') { + n++; + inComment = 0; + } else if (!inComment && c === '/' && script[start + n] === '*') { + n++; + inComment = 1; + } else if (!inComment) { + break; + } + } + n--; + return { length: n, newlines }; + } + if (first === '-' && script[start + 1] === '-') { + n = 2; + while (start + n < script.length && script[start + n] !== '\n') { + n++; + } + if (start + n < script.length) { + newlines++; + n++; + } + return { length: n, newlines }; + } + if (first === '"' || first === "'") { + const delim = first; + n = 1; + while (start + n < script.length) { + const c = script[start + n]; + if (c === '\n') { + newlines++; + } + n++; + if (c === delim) { + if (script[start + n] !== delim) { + break; + } + n++; + } + } + return { length: n, newlines }; + } + n = 1; + while (start + n < script.length) { + const c = script[start + n]; + if (!c || isWhitespace(c) || c === '"' || c === "'" || c === ';') { + break; + } + n++; + } + return { length: n, newlines }; +} + +function findEnd( + script: string, + start: number, + lineRef: { value: number } +): number { + let offset = 0; + while (start + offset < script.length) { + if ( + script.startsWith('--end', start + offset) && + isWhitespace(script[start + offset + 5] ?? '\n') + ) { + break; + } + const token = tokenLength(script, start + offset); + lineRef.value += token.newlines; + if (token.length <= 0) { + break; + } + offset += token.length; + } + return offset; +} + +function findEndif( + script: string, + start: number, + stopAtElse: boolean, + lineRef: { value: number } +): number { + let offset = 0; + while (start + offset < script.length) { + const current = start + offset; + const token = tokenLength(script, current); + lineRef.value += token.newlines; + if ( + script.startsWith('--endif', current) && + isWhitespace(script[current + 7] ?? '\n') + ) { + return offset + token.length; + } + if ( + stopAtElse && + script.startsWith('--else', current) && + isWhitespace(script[current + 6] ?? '\n') + ) { + return offset + token.length; + } + if ( + script.startsWith('--if', current) && + isWhitespace(script[current + 4] ?? '\n') + ) { + const inner = findEndif(script, current + token.length, false, lineRef); + offset += token.length + inner; + } else { + if (token.length <= 0) { + break; + } + offset += token.length; + } + } + return offset; +} + +interface ParsedCommand { + name: string; + args: string[]; + payload: string; +} + +function parseCommand( + script: string, + start: number, + length: number +): ParsedCommand { + const segment = script.slice(start, start + length).replace(/\r?\n?$/, ''); + const trimmed = segment.trim(); + const body = trimmed.slice(2).trim(); + const firstSpace = body.search(/\s/); + let name: string; + let payload: string; + if (firstSpace === -1) { + name = body; + payload = ''; + } else { + name = body.slice(0, firstSpace); + payload = body.slice(firstSpace).trimStart(); + } + const args = payload ? payload.split(/\s+/) : []; + return { name, args, payload }; +} + +function splitSql(script: string): string[] { + const statements: string[] = []; + let current = ''; + let inSingle = false; + let inDouble = false; + let inBracket = false; + let inLineComment = false; + let inBlockComment = false; + + for (let i = 0; i < script.length; i++) { + const c = script[i]; + const next = script[i + 1]; + if (inLineComment) { + current += c; + if (c === '\n') { + inLineComment = false; + } + continue; + } + if (inBlockComment) { + current += c; + if (c === '*' && next === '/') { + current += next; + i++; + inBlockComment = false; + } + continue; + } + if (!inSingle && !inDouble) { + if (c === '-' && next === '-') { + inLineComment = true; + current += c; + continue; + } + if (c === '/' && next === '*') { + inBlockComment = true; + current += c; + continue; + } + } + if (c === "'" && !inDouble) { + inSingle = !inSingle; + current += c; + if (inSingle && next === "'") { + current += next; + i++; + } + continue; + } + if (c === '"' && !inSingle) { + inDouble = !inDouble; + current += c; + if (inDouble && next === '"') { + current += next; + i++; + } + continue; + } + if (!inSingle && !inDouble) { + if (c === '[') { + inBracket = true; + } else if (c === ']' && inBracket) { + inBracket = false; + } + } + if (!inSingle && !inDouble && !inBracket && c === ';') { + const statement = current.trim(); + if (statement) { + statements.push(statement); + } + current = ''; + continue; + } + current += c; + } + const trailing = current.trim(); + if (trailing) { + statements.push(trailing); + } + return statements; +} + +function tokenizeMatch(input: string): string[] { + const tokens: string[] = []; + let i = 0; + while (i < input.length) { + while (i < input.length && /\s/.test(input[i]!)) i++; + if (i >= input.length) { + break; + } + if (input[i] === "'") { + const start = i; + i++; + while (i < input.length) { + if (input[i] === "'") { + i++; + if (input[i] === "'") { + i++; + continue; + } + break; + } + i++; + } + tokens.push(input.slice(start, i)); + } else { + const start = i; + while (i < input.length && !/\s/.test(input[i]!)) i++; + tokens.push(input.slice(start, i)); + } + } + return tokens; +} + +function tokensEqual(expected: string[], actual: string[]): boolean { + if (expected.length !== actual.length) { + return false; + } + for (let i = 0; i < expected.length; i++) { + const e = expected[i]; + const a = actual[i]; + if (e === a) { + continue; + } + if (normalizeNumeric(e) === normalizeNumeric(a)) { + continue; + } + return false; + } + return true; +} + +function normalizeNumeric(token: string): string { + if (!/^-?\d+(?:\.\d+)?$/.test(token)) { + return token; + } + if (!token.includes('.')) { + return token; + } + const negative = token.startsWith('-'); + let body = negative ? token.slice(1) : token; + body = body.replace(/\.0+$/, ''); + body = body.replace(/(\.\d*?[1-9])0+$/, '$1'); + if (body.endsWith('.')) { + body = body.slice(0, -1); + } + if (!body) { + body = '0'; + } + return negative ? `-${body}` : body; +} + +function formatTerm(value: SqliteValue | string): string { + if (value == null) { + return 'nil'; + } + if (typeof value === 'string') { + return formatString(value); + } + if (typeof value === 'number' || typeof value === 'bigint') { + return String(value); + } + if (value instanceof Uint8Array) { + return `x'${toHex(value)}'`; + } + return formatString(String(value)); +} + +function formatString(value: string): string { + if (!/\s/.test(value) && !value.includes("'")) { + return value; + } + return `'${value.replace(/'/g, "''")}'`; +} + +function toHex(data: Uint8Array): string { + let hex = ''; + for (let i = 0; i < data.length; i++) { + const byte = data[i]; + hex += byte.toString(16).padStart(2, '0'); + } + return hex; +} + +function hasNonCommentContent(sql: string): boolean { + let i = 0; + let inSingle = false; + let inDouble = false; + let inBracket = false; + + const advance = (step = 1) => { + i += step; + }; + + while (i < sql.length) { + const c = sql[i]; + const next = sql[i + 1]; + + if (inSingle) { + if (c === "'") { + if (next === "'") { + advance(2); + continue; + } + inSingle = false; + advance(); + continue; + } + advance(); + continue; + } + if (inDouble) { + if (c === '"') { + if (next === '"') { + advance(2); + continue; + } + inDouble = false; + advance(); + continue; + } + advance(); + continue; + } + if (inBracket) { + if (c === ']') { + inBracket = false; + } + advance(); + continue; + } + + if (c === "'") { + inSingle = true; + advance(); + continue; + } + if (c === '"') { + inDouble = true; + advance(); + continue; + } + if (c === '[') { + inBracket = true; + advance(); + continue; + } + + if (c === '-' && next === '-') { + advance(2); + while (i < sql.length && sql[i] !== '\n') { + advance(); + } + continue; + } + if (c === '/' && next === '*') { + advance(2); + while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) { + advance(); + } + if (i < sql.length) { + advance(2); + } + continue; + } + if (/\s/.test(c ?? '')) { + advance(); + continue; + } + return true; + } + return false; +} + +function sleepMs(ms: number): Promise { + if (ms <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isSqliteError(err: unknown): err is SqliteError { + return ( + err instanceof SqliteError || + (typeof err === 'object' && + err != null && + 'code' in err && + 'message' in err) + ); +}