fix wasm client

This commit is contained in:
Nikita Sivukhin
2025-10-02 16:56:54 +04:00
parent 39bf35abe9
commit cff23c80cb
6 changed files with 361 additions and 140 deletions

View File

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

View File

@@ -1,7 +1,7 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common"
import { DatabasePromise } from "@tursodatabase/database-common" import { DatabasePromise } from "@tursodatabase/database-common"
import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-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 = { let BrowserIO: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> { async read(path: string): Promise<Buffer | Uint8Array | null> {
@@ -45,6 +45,12 @@ class Database extends DatabasePromise {
#guards: SyncEngineGuards; #guards: SyncEngineGuards;
#worker: Worker | null; #worker: Worker | null;
constructor(opts: DatabaseOpts) { 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({ const engine = new SyncEngine({
path: opts.path, path: opts.path,
clientName: opts.clientName, clientName: opts.clientName,
@@ -52,23 +58,31 @@ class Database extends DatabasePromise {
protocolVersion: SyncEngineProtocolVersion.V1, protocolVersion: SyncEngineProtocolVersion.V1,
longPollTimeoutMs: opts.longPollTimeoutMs, longPollTimeoutMs: opts.longPollTimeoutMs,
tracing: opts.tracing, tracing: opts.tracing,
bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null,
remoteEncryption: opts.remoteEncryption?.cipher,
}); });
super(engine.db() as unknown as any); super(engine.db() as unknown as any);
let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>);
let headers = typeof opts.authToken === "function" ? () => ({ if (typeof opts.authToken == "function") {
...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), const authToken = opts.authToken;
...(opts.encryption != null && { headers = async () => ({
"x-turso-encryption-key": opts.encryption.key, ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }),
"x-turso-encryption-cipher": opts.encryption.cipher, ...(opts.remoteEncryption != null && {
"x-turso-encryption-key": opts.remoteEncryption.key,
"x-turso-encryption-cipher": opts.remoteEncryption.cipher,
}) })
}) : { });
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), } else {
...(opts.encryption != null && { const authToken = opts.authToken;
"x-turso-encryption-key": opts.encryption.key, headers = {
"x-turso-encryption-cipher": opts.encryption.cipher, ...(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 = { this.#runOpts = {
url: opts.url, url: opts.url,
headers: headers, headers: headers,
@@ -83,7 +97,11 @@ class Database extends DatabasePromise {
* connect database and initialize it in case of clean start * connect database and initialize it in case of clean start
*/ */
override async connect() { override async connect() {
if (this.connected) { return; } if (this.connected) {
return;
} else if (this.#engine == null) {
await super.connect();
} else {
if (!this.memory) { if (!this.memory) {
this.#worker = await init(); this.#worker = await init();
await Promise.all([ await Promise.all([
@@ -95,6 +113,7 @@ class Database extends DatabasePromise {
]); ]);
} }
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; this.connected = true;
} }
/** /**
@@ -103,6 +122,9 @@ class Database extends DatabasePromise {
* @returns true if new changes were pulled from the remote * @returns true if new changes were pulled from the remote
*/ */
async pull() { 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())); const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait()));
if (changes.empty()) { if (changes.empty()) {
return false; return false;
@@ -115,24 +137,34 @@ 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 * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote
*/ */
async push() { 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())); await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push()));
} }
/** /**
* checkpoint WAL for local database * checkpoint WAL for local database
*/ */
async checkpoint() { 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())); await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint()));
} }
/** /**
* @returns statistic of current local database * @returns statistic of current local database
*/ */
async stats(): Promise<DatabaseStats> { async stats(): Promise<DatabaseStats> {
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())); return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats()));
} }
/** /**
* close the database and relevant files * close the database and relevant files
*/ */
async close() { async close() {
if (this.#engine != null) {
if (this.name != null && this.#worker != null) { if (this.name != null && this.#worker != null) {
await Promise.all([ await Promise.all([
unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, this.name),
@@ -142,9 +174,10 @@ class Database extends DatabasePromise {
unregisterFileAtWorker(this.#worker, `${this.name}-changes`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`),
]); ]);
} }
await super.close();
this.#engine.close(); this.#engine.close();
} }
await super.close();
}
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common"
import { DatabasePromise } from "@tursodatabase/database-common" import { DatabasePromise } from "@tursodatabase/database-common"
import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-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 = { let BrowserIO: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> { async read(path: string): Promise<Buffer | Uint8Array | null> {
@@ -45,6 +45,12 @@ class Database extends DatabasePromise {
#guards: SyncEngineGuards; #guards: SyncEngineGuards;
#worker: Worker | null; #worker: Worker | null;
constructor(opts: DatabaseOpts) { 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({ const engine = new SyncEngine({
path: opts.path, path: opts.path,
clientName: opts.clientName, clientName: opts.clientName,
@@ -52,23 +58,31 @@ class Database extends DatabasePromise {
protocolVersion: SyncEngineProtocolVersion.V1, protocolVersion: SyncEngineProtocolVersion.V1,
longPollTimeoutMs: opts.longPollTimeoutMs, longPollTimeoutMs: opts.longPollTimeoutMs,
tracing: opts.tracing, tracing: opts.tracing,
bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null,
remoteEncryption: opts.remoteEncryption?.cipher,
}); });
super(engine.db() as unknown as any); super(engine.db() as unknown as any);
let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>);
let headers = typeof opts.authToken === "function" ? () => ({ if (typeof opts.authToken == "function") {
...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), const authToken = opts.authToken;
...(opts.encryption != null && { headers = async () => ({
"x-turso-encryption-key": opts.encryption.key, ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }),
"x-turso-encryption-cipher": opts.encryption.cipher, ...(opts.remoteEncryption != null && {
"x-turso-encryption-key": opts.remoteEncryption.key,
"x-turso-encryption-cipher": opts.remoteEncryption.cipher,
}) })
}) : { });
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), } else {
...(opts.encryption != null && { const authToken = opts.authToken;
"x-turso-encryption-key": opts.encryption.key, headers = {
"x-turso-encryption-cipher": opts.encryption.cipher, ...(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 = { this.#runOpts = {
url: opts.url, url: opts.url,
headers: headers, headers: headers,
@@ -83,7 +97,11 @@ class Database extends DatabasePromise {
* connect database and initialize it in case of clean start * connect database and initialize it in case of clean start
*/ */
override async connect() { override async connect() {
if (this.connected) { return; } if (this.connected) {
return;
} else if (this.#engine == null) {
await super.connect();
} else {
if (!this.memory) { if (!this.memory) {
this.#worker = await init(); this.#worker = await init();
await Promise.all([ await Promise.all([
@@ -95,6 +113,7 @@ class Database extends DatabasePromise {
]); ]);
} }
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; this.connected = true;
} }
/** /**
@@ -103,6 +122,9 @@ class Database extends DatabasePromise {
* @returns true if new changes were pulled from the remote * @returns true if new changes were pulled from the remote
*/ */
async pull() { 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())); const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait()));
if (changes.empty()) { if (changes.empty()) {
return false; return false;
@@ -115,24 +137,34 @@ 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 * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote
*/ */
async push() { 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())); await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push()));
} }
/** /**
* checkpoint WAL for local database * checkpoint WAL for local database
*/ */
async checkpoint() { 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())); await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint()));
} }
/** /**
* @returns statistic of current local database * @returns statistic of current local database
*/ */
async stats(): Promise<DatabaseStats> { async stats(): Promise<DatabaseStats> {
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())); return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats()));
} }
/** /**
* close the database and relevant files * close the database and relevant files
*/ */
async close() { async close() {
if (this.#engine != null) {
if (this.name != null && this.#worker != null) { if (this.name != null && this.#worker != null) {
await Promise.all([ await Promise.all([
unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, this.name),
@@ -142,9 +174,10 @@ class Database extends DatabasePromise {
unregisterFileAtWorker(this.#worker, `${this.name}-changes`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`),
]); ]);
} }
await super.close();
this.#engine.close(); this.#engine.close();
} }
await super.close();
}
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common"
import { DatabasePromise } from "@tursodatabase/database-common" import { DatabasePromise } from "@tursodatabase/database-common"
import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-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 = { let BrowserIO: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> { async read(path: string): Promise<Buffer | Uint8Array | null> {
@@ -45,6 +45,12 @@ class Database extends DatabasePromise {
#guards: SyncEngineGuards; #guards: SyncEngineGuards;
#worker: Worker | null; #worker: Worker | null;
constructor(opts: DatabaseOpts) { 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({ const engine = new SyncEngine({
path: opts.path, path: opts.path,
clientName: opts.clientName, clientName: opts.clientName,
@@ -52,23 +58,31 @@ class Database extends DatabasePromise {
protocolVersion: SyncEngineProtocolVersion.V1, protocolVersion: SyncEngineProtocolVersion.V1,
longPollTimeoutMs: opts.longPollTimeoutMs, longPollTimeoutMs: opts.longPollTimeoutMs,
tracing: opts.tracing, tracing: opts.tracing,
bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null,
remoteEncryption: opts.remoteEncryption?.cipher,
}); });
super(engine.db() as unknown as any); super(engine.db() as unknown as any);
let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>);
let headers = typeof opts.authToken === "function" ? () => ({ if (typeof opts.authToken == "function") {
...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), const authToken = opts.authToken;
...(opts.encryption != null && { headers = async () => ({
"x-turso-encryption-key": opts.encryption.key, ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }),
"x-turso-encryption-cipher": opts.encryption.cipher, ...(opts.remoteEncryption != null && {
"x-turso-encryption-key": opts.remoteEncryption.key,
"x-turso-encryption-cipher": opts.remoteEncryption.cipher,
}) })
}) : { });
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), } else {
...(opts.encryption != null && { const authToken = opts.authToken;
"x-turso-encryption-key": opts.encryption.key, headers = {
"x-turso-encryption-cipher": opts.encryption.cipher, ...(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 = { this.#runOpts = {
url: opts.url, url: opts.url,
headers: headers, headers: headers,
@@ -83,7 +97,11 @@ class Database extends DatabasePromise {
* connect database and initialize it in case of clean start * connect database and initialize it in case of clean start
*/ */
override async connect() { override async connect() {
if (this.connected) { return; } if (this.connected) {
return;
} else if (this.#engine == null) {
await super.connect();
} else {
if (!this.memory) { if (!this.memory) {
this.#worker = await init(); this.#worker = await init();
await Promise.all([ await Promise.all([
@@ -95,6 +113,7 @@ class Database extends DatabasePromise {
]); ]);
} }
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; this.connected = true;
} }
/** /**
@@ -103,6 +122,9 @@ class Database extends DatabasePromise {
* @returns true if new changes were pulled from the remote * @returns true if new changes were pulled from the remote
*/ */
async pull() { 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())); const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait()));
if (changes.empty()) { if (changes.empty()) {
return false; return false;
@@ -115,24 +137,34 @@ 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 * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote
*/ */
async push() { 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())); await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push()));
} }
/** /**
* checkpoint WAL for local database * checkpoint WAL for local database
*/ */
async checkpoint() { 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())); await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint()));
} }
/** /**
* @returns statistic of current local database * @returns statistic of current local database
*/ */
async stats(): Promise<DatabaseStats> { async stats(): Promise<DatabaseStats> {
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())); return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats()));
} }
/** /**
* close the database and relevant files * close the database and relevant files
*/ */
async close() { async close() {
if (this.#engine != null) {
if (this.name != null && this.#worker != null) { if (this.name != null && this.#worker != null) {
await Promise.all([ await Promise.all([
unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, this.name),
@@ -142,9 +174,10 @@ class Database extends DatabasePromise {
unregisterFileAtWorker(this.#worker, `${this.name}-changes`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`),
]); ]);
} }
await super.close();
this.#engine.close(); this.#engine.close();
} }
await super.close();
}
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common" import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-wasm-common"
import { DatabasePromise } from "@tursodatabase/database-common" import { DatabasePromise } from "@tursodatabase/database-common"
import { ProtocolIo, run, DatabaseOpts, EncryptionOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, DatabaseStats, SyncEngineGuards } from "@tursodatabase/sync-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 = { let BrowserIO: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> { async read(path: string): Promise<Buffer | Uint8Array | null> {
@@ -45,6 +45,12 @@ class Database extends DatabasePromise {
#guards: SyncEngineGuards; #guards: SyncEngineGuards;
#worker: Worker | null; #worker: Worker | null;
constructor(opts: DatabaseOpts) { 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({ const engine = new SyncEngine({
path: opts.path, path: opts.path,
clientName: opts.clientName, clientName: opts.clientName,
@@ -52,23 +58,31 @@ class Database extends DatabasePromise {
protocolVersion: SyncEngineProtocolVersion.V1, protocolVersion: SyncEngineProtocolVersion.V1,
longPollTimeoutMs: opts.longPollTimeoutMs, longPollTimeoutMs: opts.longPollTimeoutMs,
tracing: opts.tracing, tracing: opts.tracing,
bootstrapIfEmpty: typeof opts.url != "function" || opts.url() != null,
remoteEncryption: opts.remoteEncryption?.cipher,
}); });
super(engine.db() as unknown as any); super(engine.db() as unknown as any);
let headers: { [K: string]: string } | (() => Promise<{ [K: string]: string }>);
let headers = typeof opts.authToken === "function" ? () => ({ if (typeof opts.authToken == "function") {
...(opts.authToken != null && { "Authorization": `Bearer ${(opts.authToken as any)()}` }), const authToken = opts.authToken;
...(opts.encryption != null && { headers = async () => ({
"x-turso-encryption-key": opts.encryption.key, ...(opts.authToken != null && { "Authorization": `Bearer ${await authToken()}` }),
"x-turso-encryption-cipher": opts.encryption.cipher, ...(opts.remoteEncryption != null && {
"x-turso-encryption-key": opts.remoteEncryption.key,
"x-turso-encryption-cipher": opts.remoteEncryption.cipher,
}) })
}) : { });
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }), } else {
...(opts.encryption != null && { const authToken = opts.authToken;
"x-turso-encryption-key": opts.encryption.key, headers = {
"x-turso-encryption-cipher": opts.encryption.cipher, ...(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 = { this.#runOpts = {
url: opts.url, url: opts.url,
headers: headers, headers: headers,
@@ -83,7 +97,11 @@ class Database extends DatabasePromise {
* connect database and initialize it in case of clean start * connect database and initialize it in case of clean start
*/ */
override async connect() { override async connect() {
if (this.connected) { return; } if (this.connected) {
return;
} else if (this.#engine == null) {
await super.connect();
} else {
if (!this.memory) { if (!this.memory) {
this.#worker = await init(); this.#worker = await init();
await Promise.all([ await Promise.all([
@@ -95,6 +113,7 @@ class Database extends DatabasePromise {
]); ]);
} }
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; this.connected = true;
} }
/** /**
@@ -103,6 +122,9 @@ class Database extends DatabasePromise {
* @returns true if new changes were pulled from the remote * @returns true if new changes were pulled from the remote
*/ */
async pull() { 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())); const changes = await this.#guards.wait(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.wait()));
if (changes.empty()) { if (changes.empty()) {
return false; return false;
@@ -115,24 +137,34 @@ 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 * if {@link DatabaseOpts.transform} is set - then provided callback will be called for every mutation before sending it to the remote
*/ */
async push() { 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())); await this.#guards.push(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.push()));
} }
/** /**
* checkpoint WAL for local database * checkpoint WAL for local database
*/ */
async checkpoint() { 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())); await this.#guards.checkpoint(async () => await run(this.#runOpts, this.#io, this.#engine, this.#engine.checkpoint()));
} }
/** /**
* @returns statistic of current local database * @returns statistic of current local database
*/ */
async stats(): Promise<DatabaseStats> { async stats(): Promise<DatabaseStats> {
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())); return (await run(this.#runOpts, this.#io, this.#engine, this.#engine.stats()));
} }
/** /**
* close the database and relevant files * close the database and relevant files
*/ */
async close() { async close() {
if (this.#engine != null) {
if (this.name != null && this.#worker != null) { if (this.name != null && this.#worker != null) {
await Promise.all([ await Promise.all([
unregisterFileAtWorker(this.#worker, this.name), unregisterFileAtWorker(this.#worker, this.name),
@@ -142,9 +174,10 @@ class Database extends DatabasePromise {
unregisterFileAtWorker(this.#worker, `${this.name}-changes`), unregisterFileAtWorker(this.#worker, `${this.name}-changes`),
]); ]);
} }
await super.close();
this.#engine.close(); this.#engine.close();
} }
await super.close();
}
} }
/** /**

View File

@@ -2,6 +2,7 @@ import { expect, test } from 'vitest'
import { Database, connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise-default.js' import { Database, connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise-default.js'
const localeCompare = (a, b) => a.x.localeCompare(b.x); const localeCompare = (a, b) => a.x.localeCompare(b.x);
const intCompare = (a, b) => a.x - b.x;
test('implicit connect', async () => { test('implicit connect', async () => {
const db = new Database({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); 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 }]); 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 () => { test('select-after-push', async () => {
{ {
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL }); const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });