mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-07 02:04:21 +01:00
Merge 'Sync engine defered sync' from Nikita Sivukhin
This PR makes sync client completely autonomous as now it can defer initial sync. This can open possibility to asynchronously create DB in the Turso Cloud while giving user ability to interact with local DB straight away. Closes #3531
This commit is contained in:
3
bindings/javascript/sync/packages/wasm/cp-entrypoint.sh
Normal file
3
bindings/javascript/sync/packages/wasm/cp-entrypoint.sh
Normal 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
|
||||
@@ -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<Buffer | Uint8Array | null> {
|
||||
@@ -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<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()));
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Buffer | Uint8Array | null> {
|
||||
@@ -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<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()));
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Buffer | Uint8Array | null> {
|
||||
@@ -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<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()));
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Buffer | Uint8Array | null> {
|
||||
@@ -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<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()));
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user