diff --git a/bindings/javascript/packages/common/promise.ts b/bindings/javascript/packages/common/promise.ts index 8dbcf5058..88ccb802c 100644 --- a/bindings/javascript/packages/common/promise.ts +++ b/bindings/javascript/packages/common/promise.ts @@ -115,22 +115,17 @@ class Database { const db = this; const wrapTxn = (mode) => { return async (...bindParameters) => { - await this.execLock.acquire(); + await db.exec("BEGIN " + mode); + db._inTransaction = true; try { - await db.exec("BEGIN " + mode); - db._inTransaction = true; - try { - const result = await fn(...bindParameters); - await db.exec("COMMIT"); - db._inTransaction = false; - return result; - } catch (err) { - await db.exec("ROLLBACK"); - db._inTransaction = false; - throw err; - } - } finally { - this.execLock.release(); + const result = await fn(...bindParameters); + await db.exec("COMMIT"); + db._inTransaction = false; + return result; + } catch (err) { + await db.exec("ROLLBACK"); + db._inTransaction = false; + throw err; } }; }; @@ -203,18 +198,11 @@ class Database { throw new TypeError("The database connection is not open"); } - await this.execLock.acquire(); + const stmt = this.prepare(sql); try { - const stmt = this.prepare(sql); - try { - await stmt.run(); - } finally { - stmt.close(); - } - } catch (err) { - throw convertError(err); + await stmt.run(); } finally { - this.execLock.release(); + stmt.close(); } } diff --git a/bindings/javascript/packages/native/promise.test.ts b/bindings/javascript/packages/native/promise.test.ts index 5819c8f39..a554154cd 100644 --- a/bindings/javascript/packages/native/promise.test.ts +++ b/bindings/javascript/packages/native/promise.test.ts @@ -22,7 +22,7 @@ test('drizzle-orm', async () => { } }) -test('in-memory db', async () => { +test('in-memory-db-async', async () => { const db = await connect(":memory:"); await db.exec("CREATE TABLE t(x)"); await db.exec("INSERT INTO t VALUES (1), (2), (3)"); diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 3a9970680..ff910b7f2 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -10,9 +10,9 @@ //! - Iterating through query results //! - Managing the I/O event loop -// #[cfg(feature = "browser")] +#[cfg(feature = "browser")] pub mod browser; -// #[cfg(feature = "browser")] +#[cfg(feature = "browser")] use crate::browser::opfs; use napi::bindgen_prelude::*; @@ -62,6 +62,8 @@ pub(crate) fn init_tracing(level_filter: Option) { return; }; let level_filter = match level_filter.as_ref() { + "error" => LevelFilter::ERROR, + "warn" => LevelFilter::WARN, "info" => LevelFilter::INFO, "debug" => LevelFilter::DEBUG, "trace" => LevelFilter::TRACE, @@ -596,7 +598,12 @@ impl Statement { /// Finalizes the statement. #[napi] pub fn finalize(&self) -> Result<()> { - self.stmt.borrow_mut().take(); + match self.stmt.try_borrow_mut() { + Ok(mut stmt) => { + stmt.take(); + } + Err(err) => tracing::error!("borrow error: {:?}", err), + } Ok(()) } } diff --git a/bindings/javascript/sync/packages/browser/a b/bindings/javascript/sync/packages/browser/a deleted file mode 100755 index 8f5630c07..000000000 Binary files a/bindings/javascript/sync/packages/browser/a and /dev/null differ diff --git a/bindings/javascript/sync/packages/browser/a-shm b/bindings/javascript/sync/packages/browser/a-shm deleted file mode 100755 index 7c251555d..000000000 Binary files a/bindings/javascript/sync/packages/browser/a-shm and /dev/null differ diff --git a/bindings/javascript/sync/packages/browser/package.json b/bindings/javascript/sync/packages/browser/package.json index efddc0a70..e09740154 100644 --- a/bindings/javascript/sync/packages/browser/package.json +++ b/bindings/javascript/sync/packages/browser/package.json @@ -42,7 +42,7 @@ "tsc-build": "npm exec tsc && cp sync.wasm32-wasi.wasm ./dist/sync.wasm32-wasi.wasm && WASM_FILE=sync.wasm32-wasi.wasm JS_FILE=./dist/wasm-inline.js node ../../../scripts/inline-wasm-base64.js && npm run bundle", "bundle": "vite build", "build": "npm run napi-build && npm run tsc-build", - "test": "VITE_TURSO_DB_URL=http://c--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=chromium --run && VITE_TURSO_DB_URL=http://c--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=firefox --run" + "test": "VITE_TURSO_DB_URL=http://f--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=chromium --run && VITE_TURSO_DB_URL=http://f--a--a.localhost:10000 CI=1 vitest --testTimeout 30000 --browser=firefox --run" }, "napi": { "binaryName": "sync", diff --git a/bindings/javascript/sync/packages/browser/promise.test.ts b/bindings/javascript/sync/packages/browser/promise.test.ts index fb60ff61b..ff271d0e6 100644 --- a/bindings/javascript/sync/packages/browser/promise.test.ts +++ b/bindings/javascript/sync/packages/browser/promise.test.ts @@ -316,47 +316,72 @@ test('pull-push-concurrent', async () => { console.info(await db.stats()); }) -test('concurrent-updates', async () => { +test('concurrent-updates', { timeout: 60000 }, async () => { { - const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 5000 }); - await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)"); - await db.exec("DELETE FROM q"); + const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, longPollTimeoutMs: 10 }); + await db.exec("CREATE TABLE IF NOT EXISTS three(x TEXT PRIMARY KEY, y, z)"); + await db.exec("DELETE FROM three"); await db.push(); await db.close(); } - const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); - const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); - async function pull(db) { + let stop = false; + const dbs = []; + for (let i = 0; i < 8; i++) { + dbs.push(await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL })); + } + async function pull(db, i) { try { + console.info('pull', i); await db.pull(); } catch (e) { - // ignore + console.error('pull', i, e); } finally { - setTimeout(async () => await pull(db), 0); + if (!stop) { + setTimeout(async () => await pull(db, i), 0); + } } } - async function push(db) { + async function push(db, i) { try { + console.info('push', i); await db.push(); } catch (e) { - // ignore + console.error('push', i, e); } finally { - setTimeout(async () => await push(db), 0); + if (!stop) { + setTimeout(async () => await push(db, i), 0); + } } } - setTimeout(async () => await pull(db1), 0) - setTimeout(async () => await pull(db2), 0) - setTimeout(async () => await push(db1), 0) - setTimeout(async () => await push(db2), 0) + for (let i = 0; i < dbs.length; i++) { + setTimeout(async () => await pull(dbs[i], i), 0) + setTimeout(async () => await push(dbs[i], i), 0) + } for (let i = 0; i < 1000; i++) { try { - await db1.exec(`INSERT INTO q VALUES ('1', 0) ON CONFLICT DO UPDATE SET y = randomblob(128)`); - await db2.exec(`INSERT INTO q VALUES ('2', 0) ON CONFLICT DO UPDATE SET y = randomblob(128)`); + const tasks = []; + for (let s = 0; s < dbs.length; s++) { + tasks.push(dbs[s].exec(`INSERT INTO three VALUES ('${s}', 0, randomblob(128)) ON CONFLICT DO UPDATE SET y = y + 1, z = randomblob(128)`)); + } + await Promise.all(tasks); } catch (e) { // ignore } await new Promise(resolve => setTimeout(resolve, 1)); } + stop = true; + await Promise.all(dbs.map(db => db.push())); + await Promise.all(dbs.map(db => db.pull())); + let results = []; + for (let i = 0; i < dbs.length; i++) { + results.push(await dbs[i].prepare('SELECT x, y FROM three').all()); + } + for (let i = 0; i < dbs.length; i++) { + expect(results[i]).toEqual(results[0]); + for (let s = 0; s < dbs.length; s++) { + expect(results[i][s].y).toBeGreaterThan(500); + } + } }) test('transform', async () => { diff --git a/bindings/javascript/sync/packages/native/index.d.ts b/bindings/javascript/sync/packages/native/index.d.ts index 4d1b45fa9..af73101f3 100644 --- a/bindings/javascript/sync/packages/native/index.d.ts +++ b/bindings/javascript/sync/packages/native/index.d.ts @@ -73,16 +73,6 @@ export declare class Database { ioLoopAsync(): Promise } -export declare class Opfs { - constructor() - connectDb(path: string, opts?: DatabaseOpts | undefined | null): Promise - complete(completionNo: number, result: number): void -} - -export declare class OpfsFile { - -} - /** A prepared statement. */ export declare class Statement { reset(): void @@ -139,12 +129,6 @@ export declare class Statement { export interface DatabaseOpts { tracing?: string } - -/** - * turso-db in the the browser requires explicit thread pool initialization - * so, we just put no-op task on the thread pool and force emnapi to allocate web worker - */ -export declare function initThreadPool(): Promise export declare class GeneratorHolder { resumeSync(error?: string | undefined | null): GeneratorResponse resumeAsync(error?: string | undefined | null): Promise diff --git a/bindings/javascript/sync/packages/native/index.js b/bindings/javascript/sync/packages/native/index.js index 12e351d61..cd543a959 100644 --- a/bindings/javascript/sync/packages/native/index.js +++ b/bindings/javascript/sync/packages/native/index.js @@ -510,10 +510,7 @@ if (!nativeBinding) { const { Database, Statement, GeneratorHolder, JsDataCompletion, JsProtocolIo, JsProtocolRequestBytes, SyncEngine, SyncEngineChanges, DatabaseChangeTypeJs, SyncEngineProtocolVersion } = nativeBinding export { Database } -export { Opfs } -export { OpfsFile } export { Statement } -export { initThreadPool } export { GeneratorHolder } export { JsDataCompletion } export { JsProtocolIo } diff --git a/bindings/javascript/sync/packages/native/package.json b/bindings/javascript/sync/packages/native/package.json index c0c01081c..c7c52414f 100644 --- a/bindings/javascript/sync/packages/native/package.json +++ b/bindings/javascript/sync/packages/native/package.json @@ -31,7 +31,7 @@ "napi-artifacts": "napi artifacts --output-dir .", "tsc-build": "npm exec tsc", "build": "npm run napi-build && npm run tsc-build", - "test": "VITE_TURSO_DB_URL=http://c--a--a.localhost:10000 vitest --run", + "test": "VITE_TURSO_DB_URL=http://d--a--a.localhost:10000 vitest --run", "prepublishOnly": "npm run napi-dirs && npm run napi-artifacts && napi prepublish -t npm" }, "napi": { diff --git a/bindings/javascript/sync/src/lib.rs b/bindings/javascript/sync/src/lib.rs index 13427f501..3223b0795 100644 --- a/bindings/javascript/sync/src/lib.rs +++ b/bindings/javascript/sync/src/lib.rs @@ -6,7 +6,7 @@ pub mod js_protocol_io; use std::{ collections::HashMap, - sync::{Arc, Mutex, MutexGuard, OnceLock, RwLock, RwLockReadGuard}, + sync::{Arc, Mutex, OnceLock, RwLock, RwLockReadGuard}, }; use napi::bindgen_prelude::{AsyncTask, Either5, Null}; @@ -149,6 +149,8 @@ impl SyncEngine { pub fn new(opts: SyncEngineOpts) -> napi::Result { // helpful for local debugging match opts.tracing.as_deref() { + Some("error") => init_tracing(LevelFilter::ERROR), + Some("warn") => init_tracing(LevelFilter::WARN), Some("info") => init_tracing(LevelFilter::INFO), Some("debug") => init_tracing(LevelFilter::DEBUG), Some("trace") => init_tracing(LevelFilter::TRACE),