diff --git a/bindings/javascript/sync/packages/wasm/cp-entrypoint.sh b/bindings/javascript/sync/packages/wasm/cp-entrypoint.sh new file mode 100644 index 000000000..7fcf98ad6 --- /dev/null +++ b/bindings/javascript/sync/packages/wasm/cp-entrypoint.sh @@ -0,0 +1,3 @@ +sed 's/index-default.js/index-bundle.js/g' promise-default.ts > promise-bundle.ts +sed 's/index-default.js/index-turbopack-hack.js/g' promise-default.ts > promise-turbopack-hack.ts +sed 's/index-default.js/index-vite-dev-hack.js/g' promise-default.ts > promise-vite-dev-hack.ts diff --git a/bindings/javascript/sync/packages/wasm/promise-bundle.ts b/bindings/javascript/sync/packages/wasm/promise-bundle.ts index 7a818f323..8080d7c53 100644 --- a/bindings/javascript/sync/packages/wasm/promise-bundle.ts +++ b/bindings/javascript/sync/packages/wasm/promise-bundle.ts @@ -1,7 +1,7 @@ import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-common"; -import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker } from "./index-bundle.js"; +import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-bundle.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { @@ -45,6 +45,12 @@ class Database extends DatabasePromise { #guards: SyncEngineGuards; #worker: Worker | null; constructor(opts: DatabaseOpts) { + if (opts.url == null) { + super(new NativeDatabase(opts.path, { tracing: opts.tracing }) as any); + this.#engine = null; + return; + } + const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, @@ -52,23 +58,31 @@ class Database extends DatabasePromise { protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, + bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, + remoteEncryption: opts.remoteEncryption?.cipher, }); super(engine.db() as unknown as any); - - let headers = typeof opts.authToken === "function" ? () => ({ - ...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }) : { - ...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }; + let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); + if (typeof opts.authToken == "function") { + const authToken = opts.authToken; + headers = async () => ({ + ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }); + } else { + const authToken = opts.authToken; + headers = { + ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }; + } this.#runOpts = { url: opts.url, headers: headers, @@ -83,18 +97,23 @@ class Database extends DatabasePromise { * connect database and initialize it in case of clean start */ override async connect() { - if (this.connected) { return; } - if (!this.memory) { - this.#worker = await init(); - await Promise.all([ - registerFileAtWorker(this.#worker, this.name), - registerFileAtWorker(this.#worker, `${this.name}-wal`), - registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), - registerFileAtWorker(this.#worker, `${this.name}-info`), - registerFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.connected) { + return; + } else if (this.#engine == null) { + await super.connect(); + } else { + if (!this.memory) { + this.#worker = await init(); + await Promise.all([ + registerFileAtWorker(this.#worker, this.name), + registerFileAtWorker(this.#worker, `${this.name}-wal`), + registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), + registerFileAtWorker(this.#worker, `${this.name}-info`), + registerFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); } - await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); this.connected = true; } /** @@ -103,6 +122,9 @@ class Database extends DatabasePromise { * @returns true if new changes were pulled from the remote */ async pull() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait())); if (changes.empty()) { return false; @@ -115,35 +137,46 @@ class Database extends DatabasePromise { * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats())); } /** * close the database and relevant files */ async close() { - if (this.name != null && this.#worker != null) { - await Promise.all([ - unregisterFileAtWorker(this.#worker, this.name), - unregisterFileAtWorker(this.#worker, `${this.name}-wal`), - unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), - unregisterFileAtWorker(this.#worker, `${this.name}-info`), - unregisterFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.#engine != null) { + if (this.name != null && this.#worker != null) { + await Promise.all([ + unregisterFileAtWorker(this.#worker, this.name), + unregisterFileAtWorker(this.#worker, `${this.name}-wal`), + unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), + unregisterFileAtWorker(this.#worker, `${this.name}-info`), + unregisterFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + this.#engine.close(); } await super.close(); - this.#engine.close(); } } diff --git a/bindings/javascript/sync/packages/wasm/promise-default.ts b/bindings/javascript/sync/packages/wasm/promise-default.ts index 75802be92..156998cfc 100644 --- a/bindings/javascript/sync/packages/wasm/promise-default.ts +++ b/bindings/javascript/sync/packages/wasm/promise-default.ts @@ -1,7 +1,7 @@ import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-common"; -import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker } from "./index-default.js"; +import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-default.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { @@ -45,6 +45,12 @@ class Database extends DatabasePromise { #guards: SyncEngineGuards; #worker: Worker | null; constructor(opts: DatabaseOpts) { + if (opts.url == null) { + super(new NativeDatabase(opts.path, { tracing: opts.tracing }) as any); + this.#engine = null; + return; + } + const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, @@ -52,23 +58,31 @@ class Database extends DatabasePromise { protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, + bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, + remoteEncryption: opts.remoteEncryption?.cipher, }); super(engine.db() as unknown as any); - - let headers = typeof opts.authToken === "function" ? () => ({ - ...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }) : { - ...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }; + let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); + if (typeof opts.authToken == "function") { + const authToken = opts.authToken; + headers = async () => ({ + ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }); + } else { + const authToken = opts.authToken; + headers = { + ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }; + } this.#runOpts = { url: opts.url, headers: headers, @@ -83,18 +97,23 @@ class Database extends DatabasePromise { * connect database and initialize it in case of clean start */ override async connect() { - if (this.connected) { return; } - if (!this.memory) { - this.#worker = await init(); - await Promise.all([ - registerFileAtWorker(this.#worker, this.name), - registerFileAtWorker(this.#worker, `${this.name}-wal`), - registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), - registerFileAtWorker(this.#worker, `${this.name}-info`), - registerFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.connected) { + return; + } else if (this.#engine == null) { + await super.connect(); + } else { + if (!this.memory) { + this.#worker = await init(); + await Promise.all([ + registerFileAtWorker(this.#worker, this.name), + registerFileAtWorker(this.#worker, `${this.name}-wal`), + registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), + registerFileAtWorker(this.#worker, `${this.name}-info`), + registerFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); } - await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); this.connected = true; } /** @@ -103,6 +122,9 @@ class Database extends DatabasePromise { * @returns true if new changes were pulled from the remote */ async pull() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait())); if (changes.empty()) { return false; @@ -115,35 +137,46 @@ class Database extends DatabasePromise { * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats())); } /** * close the database and relevant files */ async close() { - if (this.name != null && this.#worker != null) { - await Promise.all([ - unregisterFileAtWorker(this.#worker, this.name), - unregisterFileAtWorker(this.#worker, `${this.name}-wal`), - unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), - unregisterFileAtWorker(this.#worker, `${this.name}-info`), - unregisterFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.#engine != null) { + if (this.name != null && this.#worker != null) { + await Promise.all([ + unregisterFileAtWorker(this.#worker, this.name), + unregisterFileAtWorker(this.#worker, `${this.name}-wal`), + unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), + unregisterFileAtWorker(this.#worker, `${this.name}-info`), + unregisterFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + this.#engine.close(); } await super.close(); - this.#engine.close(); } } diff --git a/bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts b/bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts index 273fd21fd..935b8b31d 100644 --- a/bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts +++ b/bindings/javascript/sync/packages/wasm/promise-turbopack-hack.ts @@ -1,7 +1,7 @@ import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-common"; -import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker } from "./index-turbopack-hack.js"; +import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-turbopack-hack.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { @@ -45,6 +45,12 @@ class Database extends DatabasePromise { #guards: SyncEngineGuards; #worker: Worker | null; constructor(opts: DatabaseOpts) { + if (opts.url == null) { + super(new NativeDatabase(opts.path, { tracing: opts.tracing }) as any); + this.#engine = null; + return; + } + const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, @@ -52,23 +58,31 @@ class Database extends DatabasePromise { protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, + bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, + remoteEncryption: opts.remoteEncryption?.cipher, }); super(engine.db() as unknown as any); - - let headers = typeof opts.authToken === "function" ? () => ({ - ...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }) : { - ...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }; + let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); + if (typeof opts.authToken == "function") { + const authToken = opts.authToken; + headers = async () => ({ + ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }); + } else { + const authToken = opts.authToken; + headers = { + ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }; + } this.#runOpts = { url: opts.url, headers: headers, @@ -83,18 +97,23 @@ class Database extends DatabasePromise { * connect database and initialize it in case of clean start */ override async connect() { - if (this.connected) { return; } - if (!this.memory) { - this.#worker = await init(); - await Promise.all([ - registerFileAtWorker(this.#worker, this.name), - registerFileAtWorker(this.#worker, `${this.name}-wal`), - registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), - registerFileAtWorker(this.#worker, `${this.name}-info`), - registerFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.connected) { + return; + } else if (this.#engine == null) { + await super.connect(); + } else { + if (!this.memory) { + this.#worker = await init(); + await Promise.all([ + registerFileAtWorker(this.#worker, this.name), + registerFileAtWorker(this.#worker, `${this.name}-wal`), + registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), + registerFileAtWorker(this.#worker, `${this.name}-info`), + registerFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); } - await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); this.connected = true; } /** @@ -103,6 +122,9 @@ class Database extends DatabasePromise { * @returns true if new changes were pulled from the remote */ async pull() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait())); if (changes.empty()) { return false; @@ -115,35 +137,46 @@ class Database extends DatabasePromise { * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats())); } /** * close the database and relevant files */ async close() { - if (this.name != null && this.#worker != null) { - await Promise.all([ - unregisterFileAtWorker(this.#worker, this.name), - unregisterFileAtWorker(this.#worker, `${this.name}-wal`), - unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), - unregisterFileAtWorker(this.#worker, `${this.name}-info`), - unregisterFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.#engine != null) { + if (this.name != null && this.#worker != null) { + await Promise.all([ + unregisterFileAtWorker(this.#worker, this.name), + unregisterFileAtWorker(this.#worker, `${this.name}-wal`), + unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), + unregisterFileAtWorker(this.#worker, `${this.name}-info`), + unregisterFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + this.#engine.close(); } await super.close(); - this.#engine.close(); } } diff --git a/bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts b/bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts index 152859a7a..fbced4d2c 100644 --- a/bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts +++ b/bindings/javascript/sync/packages/wasm/promise-vite-dev-hack.ts @@ -1,7 +1,7 @@ import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { DatabasePromise } from "@tursodatabase/database-common" import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-common"; -import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker } from "./index-vite-dev-hack.js"; +import { SyncEngine, SyncEngineProtocolVersion, initThreadPool, MainWorker, Database as NativeDatabase } from "./index-vite-dev-hack.js"; let BrowserIO: ProtocolIo = { async read(path: string): Promise { @@ -45,6 +45,12 @@ class Database extends DatabasePromise { #guards: SyncEngineGuards; #worker: Worker | null; constructor(opts: DatabaseOpts) { + if (opts.url == null) { + super(new NativeDatabase(opts.path, { tracing: opts.tracing }) as any); + this.#engine = null; + return; + } + const engine = new SyncEngine({ path: opts.path, clientName: opts.clientName, @@ -52,23 +58,31 @@ class Database extends DatabasePromise { protocolVersion: SyncEngineProtocolVersion.V1, longPollTimeoutMs: opts.longPollTimeoutMs, tracing: opts.tracing, + bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null, + remoteEncryption: opts.remoteEncryption?.cipher, }); super(engine.db() as unknown as any); - - let headers = typeof opts.authToken === "function" ? () => ({ - ...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }) : { - ...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), - ...(opts.encryption != null && { - "x-turso-encryption-key": opts.encryption.key, - "x-turso-encryption-cipher": opts.encryption.cipher, - }) - }; + let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>); + if (typeof opts.authToken == "function") { + const authToken = opts.authToken; + headers = async () => ({ + ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }); + } else { + const authToken = opts.authToken; + headers = { + ...(opts.authToken != null && { "Authorization": `Bearer ${authToken}` }), + ...(opts.remoteEncryption != null && { + "x-turso-encryption-key": opts.remoteEncryption.key, + "x-turso-encryption-cipher": opts.remoteEncryption.cipher, + }) + }; + } this.#runOpts = { url: opts.url, headers: headers, @@ -83,18 +97,23 @@ class Database extends DatabasePromise { * connect database and initialize it in case of clean start */ override async connect() { - if (this.connected) { return; } - if (!this.memory) { - this.#worker = await init(); - await Promise.all([ - registerFileAtWorker(this.#worker, this.name), - registerFileAtWorker(this.#worker, `${this.name}-wal`), - registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), - registerFileAtWorker(this.#worker, `${this.name}-info`), - registerFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.connected) { + return; + } else if (this.#engine == null) { + await super.connect(); + } else { + if (!this.memory) { + this.#worker = await init(); + await Promise.all([ + registerFileAtWorker(this.#worker, this.name), + registerFileAtWorker(this.#worker, `${this.name}-wal`), + registerFileAtWorker(this.#worker, `${this.name}-wal-revert`), + registerFileAtWorker(this.#worker, `${this.name}-info`), + registerFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); } - await run(this.#runOpts, this.#io, this.#engine, this.#engine.connect()); this.connected = true; } /** @@ -103,6 +122,9 @@ class Database extends DatabasePromise { * @returns true if new changes were pulled from the remote */ async pull() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait())); if (changes.empty()) { return false; @@ -115,35 +137,46 @@ class Database extends DatabasePromise { * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote */ async push() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push())); } /** * checkpoint WAL for local database */ async checkpoint() { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint())); } /** * @returns statistic of current local database */ async stats(): Promise { + if (this.#engine == null) { + throw new Error("sync is disabled as database was opened without sync support") + } return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats())); } /** * close the database and relevant files */ async close() { - if (this.name != null && this.#worker != null) { - await Promise.all([ - unregisterFileAtWorker(this.#worker, this.name), - unregisterFileAtWorker(this.#worker, `${this.name}-wal`), - unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), - unregisterFileAtWorker(this.#worker, `${this.name}-info`), - unregisterFileAtWorker(this.#worker, `${this.name}-changes`), - ]); + if (this.#engine != null) { + if (this.name != null && this.#worker != null) { + await Promise.all([ + unregisterFileAtWorker(this.#worker, this.name), + unregisterFileAtWorker(this.#worker, `${this.name}-wal`), + unregisterFileAtWorker(this.#worker, `${this.name}-wal-revert`), + unregisterFileAtWorker(this.#worker, `${this.name}-info`), + unregisterFileAtWorker(this.#worker, `${this.name}-changes`), + ]); + } + this.#engine.close(); } await super.close(); - this.#engine.close(); } } diff --git a/bindings/javascript/sync/packages/wasm/promise.test.ts b/bindings/javascript/sync/packages/wasm/promise.test.ts index c4f7aad4b..e6be4e53a 100644 --- a/bindings/javascript/sync/packages/wasm/promise.test.ts +++ b/bindings/javascript/sync/packages/wasm/promise.test.ts @@ -2,6 +2,7 @@ import { expect, test } from 'vitest' import { Database, connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise-default.js' const localeCompare = (a, b) => a.x.localeCompare(b.x); +const intCompare = (a, b) => a.x - b.x; test('implicit connect', async () => { const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); @@ -11,6 +12,91 @@ test('implicit connect', async () => { expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); }) +test('simple-db', async () => { + const db = new Database({ path: ':memory:' }); + expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]) + await db.exec("CREATE TABLE t(x)"); + await db.exec("INSERT INTO t VALUES (1), (2), (3)"); + expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]) + await expect(async () => await db.pull()).rejects.toThrowError(/sync is disabled as database was opened without sync support/); +}) + +test('implicit connect', async () => { + const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); + const defer = db.prepare("SELECT * FROM not_found"); + await expect(async () => await defer.all()).rejects.toThrowError(/no such table: not_found/); + expect(() => db.prepare("SELECT * FROM not_found")).toThrowError(/no such table: not_found/); + expect(await db.prepare("SELECT 1 as x").all()).toEqual([{ x: 1 }]); +}) + +test('defered sync', async () => { + { + const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); + await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); + await db.exec("DELETE FROM t"); + await db.exec("INSERT INTO t VALUES (100)"); + await db.push(); + await db.close(); + } + + let url = null; + const db = new Database({ path: ':memory:', url: () => url }); + await db.prepare("CREATE TABLE t(x)").run(); + await db.prepare("INSERT INTO t VALUES (1), (2), (3), (42)").run(); + expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); + await expect(async () => await db.pull()).rejects.toThrow(/url is empty - sync is paused/); + url = process.env.VITE_TURSO_DB_URL; + await db.pull(); + expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }, { x: 42 }]); +}) + +test('encryption sync', async () => { + const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; + const URL = 'http://encrypted--a--a.localhost:10000'; + { + const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); + await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); + await db.exec("DELETE FROM t"); + await db.push(); + await db.close(); + } + const db1 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); + const db2 = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); + await db1.exec("INSERT INTO t VALUES (1), (2), (3)"); + await db2.exec("INSERT INTO t VALUES (4), (5), (6)"); + expect(await db1.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); + expect(await db2.prepare("SELECT * FROM t").all()).toEqual([{ x: 4 }, { x: 5 }, { x: 6 }]); + await Promise.all([db1.push(), db2.push()]); + await Promise.all([db1.pull(), db2.pull()]); + const expected = [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }, { x: 6 }]; + expect((await db1.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); + expect((await db2.prepare("SELECT * FROM t").all()).sort(intCompare)).toEqual(expected.sort(intCompare)); +}); + +test('defered encryption sync', async () => { + const URL = 'http://encrypted--a--a.localhost:10000'; + const KEY = 'l/FWopMfZisTLgBX4A42AergrCrYKjiO3BfkJUwv83I='; + let url = null; + { + const db = await connect({ path: ':memory:', url: URL, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); + await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); + await db.exec("DELETE FROM t"); + await db.exec("INSERT INTO t VALUES (100)"); + await db.push(); + await db.close(); + } + const db = await connect({ path: ':memory:', url: () => url, remoteEncryption: { key: KEY, cipher: 'aes256gcm' } }); + await db.exec("CREATE TABLE IF NOT EXISTS t(x)"); + await db.exec("INSERT INTO t VALUES (1), (2), (3)"); + expect(await db.prepare("SELECT * FROM t").all()).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }]); + + url = URL; + await db.pull(); + + const expected = [{ x: 100 }, { x: 1 }, { x: 2 }, { x: 3 }]; + expect((await db.prepare("SELECT * FROM t").all())).toEqual(expected); +}); + test('select-after-push', async () => { { const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });