From ac2c11073e51231969854a7d07d94c3159b5fb86 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Mon, 11 Aug 2025 14:43:36 +0300 Subject: [PATCH] Add async connect() to promise API --- index.d.ts | 1 + index.js | 3 +- integration-tests/tests/async.test.js | 6 +- integration-tests/tests/concurrency.test.js | 6 +- promise.js | 27 ++- src/lib.rs | 185 ++++++++++---------- 6 files changed, 123 insertions(+), 105 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6dc7609..2384f77 100644 --- a/index.d.ts +++ b/index.d.ts @@ -14,6 +14,7 @@ export interface Options { encryptionKey?: string remoteEncryptionKey?: string } +export declare function connect(path: string, opts?: Options | undefined | null): Promise /** Result of a database sync operation. */ export interface SyncResult { /** The number of frames synced. */ diff --git a/index.js b/index.js index 95b3e00..d3843a7 100644 --- a/index.js +++ b/index.js @@ -310,9 +310,10 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Database, databasePrepareSync, databaseSyncSync, databaseExecSync, Statement, statementGetSync, statementRunSync, statementIterateSync, RowsIterator, iteratorNextSync, Record } = nativeBinding +const { Database, connect, databasePrepareSync, databaseSyncSync, databaseExecSync, Statement, statementGetSync, statementRunSync, statementIterateSync, RowsIterator, iteratorNextSync, Record } = nativeBinding module.exports.Database = Database +module.exports.connect = connect module.exports.databasePrepareSync = databasePrepareSync module.exports.databaseSyncSync = databaseSyncSync module.exports.databaseExecSync = databaseExecSync diff --git a/integration-tests/tests/async.test.js b/integration-tests/tests/async.test.js index 84f77a2..c92f873 100644 --- a/integration-tests/tests/async.test.js +++ b/integration-tests/tests/async.test.js @@ -414,9 +414,9 @@ const connect = async (path_opt, options = {}) => { const path = path_opt ?? "hello.db"; const provider = process.env.PROVIDER; const database = process.env.LIBSQL_DATABASE ?? path; - const x = await import("libsql/promise"); - const db = new x.default(database, options); - return [db, x.SqliteError]; + const libsql = await import("libsql/promise"); + const db = await libsql.connect(database, options); + return [db, libsql.SqliteError]; }; /// Generate a unique database filename diff --git a/integration-tests/tests/concurrency.test.js b/integration-tests/tests/concurrency.test.js index d4d1bac..12d5003 100644 --- a/integration-tests/tests/concurrency.test.js +++ b/integration-tests/tests/concurrency.test.js @@ -148,9 +148,9 @@ test("Concurrent operations with timeout should handle busy database", async (t) const connect = async (path_opt, options = {}) => { const path = path_opt ?? `test-${crypto.randomBytes(8).toString('hex')}.db`; - const x = await import("libsql/promise"); - const db = new x.default(process.env.LIBSQL_DATABASE ?? path, options); - return [db, x.SqliteError, path]; + const libsql = await import("libsql/promise"); + const db = await libsql.connect(process.env.LIBSQL_DATABASE ?? path, options); + return [db, libsql.SqliteError, path]; }; const cleanup = async (context) => { diff --git a/promise.js b/promise.js index 37b12e0..aa48e19 100644 --- a/promise.js +++ b/promise.js @@ -1,6 +1,6 @@ "use strict"; -const { Database: NativeDb } = require("./index.js"); +const { Database: NativeDb, connect: nativeConnect } = require("./index.js"); const SqliteError = require("./sqlite-error.js"); const Authorization = require("./auth"); @@ -28,6 +28,17 @@ function convertError(err) { return err; } +/** + * Creates a new database connection. + * + * @param {string} path - Path to the database file. + * @param {object} opts - Options. + */ +const connect = async (path, opts) => { + const db = await nativeConnect(path, opts); + return new Database(db); +}; + /** * Database represents a connection that can prepare and execute SQL statements. */ @@ -38,10 +49,9 @@ class Database { * @constructor * @param {string} path - Path to the database file. */ - constructor(path, opts) { - this.db = new NativeDb(path, opts); + constructor(db) { + this.db = db; this.memory = this.db.memory - const db = this.db; Object.defineProperties(this, { inTransaction: { get() { @@ -346,6 +356,9 @@ class Statement { } } -module.exports = Database; -module.exports.SqliteError = SqliteError; -module.exports.Authorization = Authorization; +module.exports = { + Database, + connect, + SqliteError, + Authorization, +}; diff --git a/src/lib.rs b/src/lib.rs index 332b055..4b18c89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,111 +233,114 @@ impl Drop for Database { } #[napi] -impl Database { - /// Creates a new database instance. - /// - /// # Arguments - /// - /// * `path` - The path to the database file. - /// * `opts` - The database options. - #[napi(constructor)] - pub fn new(path: String, opts: Option) -> Result { - ensure_logger(); - let rt = runtime()?; - let remote = is_remote_path(&path); - let db = if remote { - let auth_token = opts +pub async fn connect(path: String, opts: Option) -> Result { + let remote = is_remote_path(&path); + let db = if remote { + let auth_token = opts + .as_ref() + .and_then(|o| o.authToken.as_ref()) + .cloned() + .unwrap_or_default(); + let mut builder = libsql::Builder::new_remote(path.clone(), auth_token); + if let Some(encryption_key) = opts + .as_ref() + .and_then(|o| o.remoteEncryptionKey.as_ref()) + .cloned() + { + let encryption_context = libsql::EncryptionContext { + key: libsql::EncryptionKey::Base64Encoded(encryption_key), + }; + builder = builder.remote_encryption(encryption_context); + } + builder.build().await.map_err(Error::from)? + } else if let Some(options) = &opts { + if let Some(sync_url) = &options.syncUrl { + let auth_token = options.authToken.as_ref().cloned().unwrap_or_default(); + + let encryption_cipher: String = opts .as_ref() - .and_then(|o| o.authToken.as_ref()) + .and_then(|o| o.encryptionCipher.as_ref()) .cloned() - .unwrap_or_default(); - let mut builder = libsql::Builder::new_remote(path.clone(), auth_token); - if let Some(encryption_key) = opts + .unwrap_or("aes256cbc".to_string()); + let cipher = libsql::Cipher::from_str(&encryption_cipher).map_err(|_| { + throw_sqlite_error( + "Invalid encryption cipher".to_string(), + "SQLITE_INVALID_ENCRYPTION_CIPHER".to_string(), + 0, + ) + })?; + let encryption_key = opts .as_ref() - .and_then(|o| o.remoteEncryptionKey.as_ref()) + .and_then(|o| o.encryptionKey.as_ref()) .cloned() - { + .unwrap_or("".to_string()); + + let mut builder = + libsql::Builder::new_remote_replica(path.clone(), sync_url.clone(), auth_token); + + let read_your_writes = options.readYourWrites.unwrap_or(true); + builder = builder.read_your_writes(read_your_writes); + + if encryption_key.len() > 0 { + let encryption_config = + libsql::EncryptionConfig::new(cipher, encryption_key.into()); + builder = builder.encryption_config(encryption_config); + } + + if let Some(remote_encryption_key) = &options.remoteEncryptionKey { let encryption_context = libsql::EncryptionContext { - key: libsql::EncryptionKey::Base64Encoded(encryption_key), + key: libsql::EncryptionKey::Base64Encoded(remote_encryption_key.to_string()), }; builder = builder.remote_encryption(encryption_context); } - rt.block_on(builder.build()).map_err(Error::from)? - } else if let Some(options) = &opts { - if let Some(sync_url) = &options.syncUrl { - let auth_token = options.authToken.as_ref().cloned().unwrap_or_default(); - - let encryption_cipher: String = opts - .as_ref() - .and_then(|o| o.encryptionCipher.as_ref()) - .cloned() - .unwrap_or("aes256cbc".to_string()); - let cipher = libsql::Cipher::from_str(&encryption_cipher).map_err(|_| { - throw_sqlite_error( - "Invalid encryption cipher".to_string(), - "SQLITE_INVALID_ENCRYPTION_CIPHER".to_string(), - 0, - ) - })?; - let encryption_key = opts - .as_ref() - .and_then(|o| o.encryptionKey.as_ref()) - .cloned() - .unwrap_or("".to_string()); - - let mut builder = - libsql::Builder::new_remote_replica(path.clone(), sync_url.clone(), auth_token); - - let read_your_writes = options.readYourWrites.unwrap_or(true); - builder = builder.read_your_writes(read_your_writes); - - if encryption_key.len() > 0 { - let encryption_config = - libsql::EncryptionConfig::new(cipher, encryption_key.into()); - builder = builder.encryption_config(encryption_config); - } - - if let Some(remote_encryption_key) = &options.remoteEncryptionKey { - let encryption_context = libsql::EncryptionContext { - key: libsql::EncryptionKey::Base64Encoded( - remote_encryption_key.to_string(), - ), - }; - builder = builder.remote_encryption(encryption_context); - } - if let Some(period) = options.syncPeriod { - if period > 0.0 { - builder = builder.sync_interval(std::time::Duration::from_secs_f64(period)); - } + if let Some(period) = options.syncPeriod { + if period > 0.0 { + builder = builder.sync_interval(std::time::Duration::from_secs_f64(period)); } - - rt.block_on(builder.build()).map_err(Error::from)? - } else { - let builder = libsql::Builder::new_local(&path); - rt.block_on(builder.build()).map_err(Error::from)? } + + builder.build().await.map_err(Error::from)? } else { let builder = libsql::Builder::new_local(&path); - rt.block_on(builder.build()).map_err(Error::from)? - }; - let conn = db.connect().map_err(Error::from)?; - let default_safe_integers = AtomicBool::new(false); - let memory = path == ":memory:"; - let timeout = match opts { - Some(ref opts) => opts.timeout.unwrap_or(0.0), - None => 0.0, - }; - if timeout > 0.0 { - conn.busy_timeout(Duration::from_millis(timeout as u64)) - .map_err(Error::from)? + builder.build().await.map_err(Error::from)? } - Ok(Database { - db, - conn: Some(Arc::new(conn)), - default_safe_integers, - memory, - }) + } else { + let builder = libsql::Builder::new_local(&path); + builder.build().await.map_err(Error::from)? + }; + let conn = db.connect().map_err(Error::from)?; + let default_safe_integers = AtomicBool::new(false); + let memory = path == ":memory:"; + let timeout = match opts { + Some(ref opts) => opts.timeout.unwrap_or(0.0), + None => 0.0, + }; + if timeout > 0.0 { + conn.busy_timeout(Duration::from_millis(timeout as u64)) + .map_err(Error::from)? + } + Ok(Database { + db, + conn: Some(Arc::new(conn)), + default_safe_integers, + memory, + }) +} + +#[napi] +impl Database { + /// Creates a new database instance. + /// + /// # Arguments + /// + /// * `path` - The path to the database file. + /// * `opts` - The database options. + #[napi(constructor)] + pub fn new(path: String, opts: Option) -> Result { + ensure_logger(); + let rt = runtime()?; + rt.block_on(connect(path, opts)) } /// Returns whether the database is in memory-only mode.