opfs for sync in one commit!

This commit is contained in:
Nikita Sivukhin
2025-09-10 03:01:37 +04:00
parent 8ab8b31cb1
commit d55026f84f
75 changed files with 3553 additions and 5535 deletions

View File

@@ -0,0 +1,124 @@
<p align="center">
<h1 align="center">Turso Database for JavaScript in Browser</h1>
</p>
<p align="center">
<a title="JavaScript" target="_blank" href="https://www.npmjs.com/package/@tursodatabase/database"><img alt="npm" src="https://img.shields.io/npm/v/@tursodatabase/database"></a>
<a title="MIT" target="_blank" href="https://github.com/tursodatabase/turso/blob/main/LICENSE.md"><img src="http://img.shields.io/badge/license-MIT-orange.svg?style=flat-square"></a>
</p>
<p align="center">
<a title="Users Discord" target="_blank" href="https://tur.so/discord"><img alt="Chat with other users of Turso on Discord" src="https://img.shields.io/discord/933071162680958986?label=Discord&logo=Discord&style=social"></a>
</p>
---
## About
This package is the Turso embedded database library for JavaScript in Browser.
> **⚠️ Warning:** This software is ALPHA, only use for development, testing, and experimentation. We are working to make it production ready, but do not use it for critical data right now.
## Features
- **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)).
- **In-process**: No network overhead, runs directly in your Node.js process
- **TypeScript support**: Full TypeScript definitions included
## Installation
```bash
npm install @tursodatabase/database-browser
```
## Getting Started
### In-Memory Database
```javascript
import { connect } from '@tursodatabase/database-browser';
// Create an in-memory database
const db = await connect(':memory:');
// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', 'alice@example.com');
await insert.run('Bob', 'bob@example.com');
// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
// { id: 1, name: 'Alice', email: 'alice@example.com' },
// { id: 2, name: 'Bob', email: 'bob@example.com' }
// ]
```
### File-Based Database
```javascript
import { connect } from '@tursodatabase/database-browser';
// Create or open a database file
const db = await connect('my-database.db');
// Create a table
await db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');
console.log(`Inserted post with ID: ${result.lastInsertRowid}`);
```
### Transactions
```javascript
import { connect } from '@tursodatabase/database-browser';
const db = await connect('transactions.db');
// Using transactions for atomic operations
const transaction = db.transaction(async (users) => {
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
for (const user of users) {
await insert.run(user.name, user.email);
}
});
// Execute transaction
await transaction([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]);
```
## API Reference
For complete API documentation, see [JavaScript API Reference](../../../../docs/javascript-api-reference.md).
## Related Packages
* The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API.
* The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud.
## License
This project is licensed under the [MIT license](../../LICENSE.md).
## Support
- [GitHub Issues](https://github.com/tursodatabase/turso/issues)
- [Documentation](https://docs.turso.tech)
- [Discord Community](https://tur.so/discord)

View File

@@ -0,0 +1,76 @@
import {
createOnMessage as __wasmCreateOnMessageForFsProxy,
getDefaultContext as __emnapiGetDefaultContext,
instantiateNapiModule as __emnapiInstantiateNapiModule,
WASI as __WASI,
} from '@napi-rs/wasm-runtime'
import { MainDummyImports } from "@tursodatabase/database-browser-common";
const __wasi = new __WASI({
version: 'preview1',
})
const __wasmUrl = new URL('./sync.wasm32-wasi.wasm', import.meta.url).href
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 4000,
maximum: 65536,
shared: true,
})
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
export let MainWorker = null;
const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = await __emnapiInstantiateNapiModule(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 1,
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
type: 'module',
})
MainWorker = worker;
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
...MainDummyImports,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
for (const name of Object.keys(instance.exports)) {
if (name.startsWith('__napi_register__')) {
instance.exports[name]()
}
}
},
})
export default __napiModule.exports
export const Database = __napiModule.exports.Database
export const Statement = __napiModule.exports.Statement
export const Opfs = __napiModule.exports.Opfs
export const OpfsFile = __napiModule.exports.OpfsFile
export const connect = __napiModule.exports.connect
export const initThreadPool = __napiModule.exports.initThreadPool
export const GeneratorHolder = __napiModule.exports.GeneratorHolder
export const JsDataCompletion = __napiModule.exports.JsDataCompletion
export const JsProtocolIo = __napiModule.exports.JsProtocolIo
export const JsProtocolRequestBytes = __napiModule.exports.JsProtocolRequestBytes
export const SyncEngine = __napiModule.exports.SyncEngine
export const DatabaseChangeTypeJs = __napiModule.exports.DatabaseChangeTypeJs
export const SyncEngineProtocolVersion = __napiModule.exports.SyncEngineProtocolVersion

View File

@@ -0,0 +1,46 @@
{
"name": "@tursodatabase/sync-browser",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
},
"type": "module",
"license": "MIT",
"main": "dist/promise.js",
"packageManager": "yarn@4.9.2",
"files": [
"index.js",
"worker.mjs",
"sync.wasm32-wasi.wasm",
"dist/**",
"README.md"
],
"devDependencies": {
"@napi-rs/cli": "^3.1.5",
"@vitest/browser": "^3.2.4",
"playwright": "^1.55.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"scripts": {
"napi-build": "napi build --features browser --release --platform --target wasm32-wasip1-threads --no-js --manifest-path ../../Cargo.toml --output-dir . && rm index.d.ts sync.wasi* wasi* browser.js",
"tsc-build": "npm exec tsc",
"build": "npm run napi-build && npm run tsc-build",
"test": "VITE_TURSO_DB_URL=http://b--a--a.localhost:10000 CI=1 vitest --browser=chromium --run && VITE_TURSO_DB_URL=http://b--a--a.localhost:10000 CI=1 vitest --browser=firefox --run"
},
"napi": {
"binaryName": "sync",
"targets": [
"wasm32-wasip1-threads"
]
},
"imports": {
"#index": "./index.js"
},
"dependencies": {
"@napi-rs/wasm-runtime": "^1.0.3",
"@tursodatabase/sync-common": "^0.1.5",
"@tursodatabase/database-common": "^0.1.5"
}
}

View File

@@ -0,0 +1,281 @@
import { expect, test } from 'vitest'
import { connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise.js'
const localeCompare = (a, b) => a.x.localeCompare(b.x);
test('select-after-push', 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.push();
await db.close();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("INSERT INTO t VALUES (1), (2), (3)");
await db.push();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
const rows = await db.prepare('SELECT * FROM t').all();
expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }])
}
})
test('select-without-push', 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.push();
await db.close();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("INSERT INTO t VALUES (1), (2), (3)");
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
const rows = await db.prepare('SELECT * FROM t').all();
expect(rows).toEqual([])
}
})
test('merge-non-overlapping-keys', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k3', 'value3'), ('k4', 'value4'), ('k5', 'value5')");
await Promise.all([db1.push(), db2.push()]);
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value3' }, { x: 'k4', y: 'value4' }, { x: 'k5', y: 'value5' }];
expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare))
expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare))
})
test('last-push-wins', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')");
await db2.push();
await db1.push();
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value5' }, { x: 'k4', y: 'value4' }];
expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare))
expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare))
})
test('last-push-wins-with-delete', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')");
await db1.exec("DELETE FROM q")
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')");
await db2.push();
await db1.push();
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k3', y: 'value5' }];
expect(rows1).toEqual(expected)
expect(rows2).toEqual(expected)
})
test('constraint-conflict', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS u(x TEXT PRIMARY KEY, y UNIQUE)");
await db.exec("DELETE FROM u");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO u VALUES ('k1', 'value1')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO u VALUES ('k2', 'value1')");
await db1.push();
await expect(async () => await db2.push()).rejects.toThrow('SQLite error: UNIQUE constraint failed: u.y');
})
test('checkpoint', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
for (let i = 0; i < 1000; i++) {
await db1.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`);
}
expect((await db1.stats()).mainWal).toBeGreaterThan(4096 * 1000);
await db1.checkpoint();
expect((await db1.stats()).mainWal).toBe(0);
let revertWal = (await db1.stats()).revertWal;
expect(revertWal).toBeLessThan(4096 * 1000 / 100);
for (let i = 0; i < 1000; i++) {
await db1.exec(`UPDATE q SET y = 'u${i}' WHERE x = 'k${i}'`);
}
await db1.checkpoint();
expect((await db1.stats()).revertWal).toBe(revertWal);
})
test('persistence', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const path = `test-${(Math.random() * 10000) | 0}.db`;
{
const db1 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`);
await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`);
await db1.close();
}
{
const db2 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`);
await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`);
const stmt = db2.prepare('SELECT * FROM q');
const rows = await stmt.all();
const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }];
expect(rows).toEqual(expected)
stmt.close();
await db2.close();
}
{
const db3 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db3.push();
await db3.close();
}
{
const db4 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
const rows = await db4.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }];
expect(rows).toEqual(expected)
await db4.close();
}
})
test('transform', async () => {
{
const db = await connect({
path: ':memory:',
url: process.env.VITE_TURSO_DB_URL,
});
await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)");
await db.exec("DELETE FROM counter");
await db.exec("INSERT INTO counter VALUES ('1', 0)")
await db.push();
await db.close();
}
const transform = (m: DatabaseRowMutation) => ({
operation: 'rewrite',
stmt: {
sql: `UPDATE counter SET value = value + ? WHERE key = ?`,
values: [m.after.value - m.before.value, m.after.key]
}
} as DatabaseRowTransformResult);
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
await Promise.all([db1.push(), db2.push()]);
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM counter').all();
const rows2 = await db2.prepare('SELECT * FROM counter').all();
expect(rows1).toEqual([{ key: '1', value: 2 }]);
expect(rows2).toEqual([{ key: '1', value: 2 }]);
})
test('transform-many', async () => {
{
const db = await connect({
path: ':memory:',
url: process.env.VITE_TURSO_DB_URL,
});
await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)");
await db.exec("DELETE FROM counter");
await db.exec("INSERT INTO counter VALUES ('1', 0)")
await db.push();
await db.close();
}
const transform = (m: DatabaseRowMutation) => ({
operation: 'rewrite',
stmt: {
sql: `UPDATE counter SET value = value + ? WHERE key = ?`,
values: [m.after.value - m.before.value, m.after.key]
}
} as DatabaseRowTransformResult);
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
for (let i = 0; i < 1002; i++) {
await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
}
for (let i = 0; i < 1001; i++) {
await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
}
let start = performance.now();
await Promise.all([db1.push(), db2.push()]);
console.info('push', performance.now() - start);
start = performance.now();
await Promise.all([db1.pull(), db2.pull()]);
console.info('pull', performance.now() - start);
const rows1 = await db1.prepare('SELECT * FROM counter').all();
const rows2 = await db2.prepare('SELECT * FROM counter').all();
expect(rows1).toEqual([{ key: '1', value: 1001 + 1002 }]);
expect(rows2).toEqual([{ key: '1', value: 1001 + 1002 }]);
})

View File

@@ -0,0 +1,113 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-browser-common"
import { DatabasePromise, DatabaseOpts, NativeDatabase } from "@tursodatabase/database-common"
import { ProtocolIo, run, SyncOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult, memoryIO } from "@tursodatabase/sync-common";
import { connect as nativeConnect, initThreadPool, MainWorker } from "#index";
import { Database as NativeDB, SyncEngine } from "#index";
let BrowserIo: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> {
const result = localStorage.getItem(path);
if (result == null) {
return null;
}
return new TextEncoder().encode(result);
},
async write(path: string, data: Buffer | Uint8Array): Promise<void> {
const array = new Uint8Array(data);
const value = new TextDecoder('utf-8').decode(array);
localStorage.setItem(path, value);
}
};
class Database extends DatabasePromise {
runOpts: RunOpts;
engine: any;
io: ProtocolIo;
fsPath: string | null;
constructor(db: NativeDatabase, io: ProtocolIo, runOpts: RunOpts, engine: any, fsPath: string | null, opts: DatabaseOpts = {}) {
super(db, opts)
this.runOpts = runOpts;
this.engine = engine;
this.fsPath = fsPath;
this.io = io;
}
async sync() {
await run(this.runOpts, this.io, this.engine, this.engine.sync());
}
async pull() {
await run(this.runOpts, this.io, this.engine, this.engine.pull());
}
async push() {
await run(this.runOpts, this.io, this.engine, this.engine.push());
}
async checkpoint() {
await run(this.runOpts, this.io, this.engine, this.engine.checkpoint());
}
async stats(): Promise<{ operations: number, mainWal: number, revertWal: number, lastPullUnixTime: number, lastPushUnixTime: number | null }> {
return (await run(this.runOpts, this.io, this.engine, this.engine.stats()));
}
override async close(): Promise<void> {
this.db.close();
this.engine.close();
if (this.fsPath != null) {
await Promise.all([
unregisterFileAtWorker(MainWorker, this.fsPath),
unregisterFileAtWorker(MainWorker, `${this.fsPath}-wal`),
unregisterFileAtWorker(MainWorker, `${this.fsPath}-revert`),
unregisterFileAtWorker(MainWorker, `${this.fsPath}-info`),
unregisterFileAtWorker(MainWorker, `${this.fsPath}-changes`),
]);
}
}
}
/**
* Creates a new database connection asynchronously.
*
* @param {string} path - Path to the database file.
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(opts: SyncOpts): Promise<Database> {
const engine = new SyncEngine({
path: opts.path,
clientName: opts.clientName,
tablesIgnore: opts.tablesIgnore,
useTransform: opts.transform != null,
tracing: opts.tracing,
protocolVersion: 1
});
const runOpts: RunOpts = {
url: opts.url,
headers: {
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }),
...(opts.encryptionKey != null && { "x-turso-encryption-key": opts.encryptionKey })
},
preemptionMs: 1,
transform: opts.transform,
};
const isMemory = opts.path == ':memory:';
let io = isMemory ? memoryIO() : BrowserIo;
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not set");
}
if (!isMemory) {
await Promise.all([
registerFileAtWorker(MainWorker, opts.path),
registerFileAtWorker(MainWorker, `${opts.path}-wal`),
registerFileAtWorker(MainWorker, `${opts.path}-revert`),
registerFileAtWorker(MainWorker, `${opts.path}-info`),
registerFileAtWorker(MainWorker, `${opts.path}-changes`),
]);
}
await run(runOpts, io, engine, engine.init());
const nativeDb = engine.open();
return new Database(nativeDb as any, io, runOpts, engine, isMemory ? null : opts.path, {});
}
export { connect, Database, }
export type { DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult }

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"module": "nodenext",
"target": "esnext",
"moduleResolution": "nodenext",
"outDir": "dist/",
"lib": [
"es2020",
"DOM",
"WebWorker"
],
"paths": {
"#index": [
"./index.js"
]
}
},
"include": [
"*"
]
}

View File

@@ -0,0 +1,23 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
define: {
'process.env.NODE_DEBUG_NATIVE': 'false',
},
server: {
headers: {
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin"
},
},
test: {
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' }
],
},
},
})

View File

@@ -0,0 +1,55 @@
import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime'
import { OpfsDirectory, workerImports } from "@tursodatabase/database-browser-common";
var opfs = new OpfsDirectory();
var memory = null;
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
memory = wasmMemory;
const wasi = new WASI({
print: function () {
// eslint-disable-next-line no-console
console.log.apply(console, arguments)
},
printErr: function () {
// eslint-disable-next-line no-console
console.error.apply(console, arguments)
},
})
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
...workerImports(opfs, memory),
memory: wasmMemory,
}
},
})
},
})
globalThis.onmessage = async function (e) {
if (e.data.__turso__ == 'register') {
try {
await opfs.registerFile(e.data.path);
self.postMessage({ id: e.data.id });
} catch (error) {
self.postMessage({ id: e.data.id, error: error });
}
return;
} else if (e.data.__turso__ == 'unregister') {
try {
await opfs.unregisterFile(e.data.path);
self.postMessage({ id: e.data.id });
} catch (error) {
self.postMessage({ id: e.data.id, error: error });
}
return;
}
handler.handle(e)
}

View File

@@ -0,0 +1,8 @@
## About
This package is the Turso Sync common JS library which is shared between final builds for Node and Browser.
Do not use this package directly - instead you must use `@tursodatabase/sync` or `@tursodatabase/sync-browser`.
> **⚠️ Warning:** This software is ALPHA, only use for development, testing, and experimentation. We are working to make it production ready, but do not use it for critical data right now.

View File

@@ -0,0 +1,5 @@
import { run, memoryIO } from "./run.js"
import { SyncOpts, ProtocolIo, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } from "./types.js"
export { run, memoryIO, }
export type { SyncOpts, ProtocolIo, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult }

View File

@@ -0,0 +1,25 @@
{
"name": "@tursodatabase/sync-common",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
},
"type": "module",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"packageManager": "yarn@4.9.2",
"files": [
"dist/**",
"README.md"
],
"devDependencies": {
"typescript": "^5.9.2"
},
"scripts": {
"tsc-build": "npm exec tsc",
"build": "npm run tsc-build",
"test": "echo 'no tests'"
}
}

View File

@@ -0,0 +1,127 @@
"use strict";
import { GeneratorResponse, ProtocolIo, RunOpts } from "./types.js";
const GENERATOR_RESUME_IO = 0;
const GENERATOR_RESUME_DONE = 1;
interface TrackPromise<T> {
promise: Promise<T>,
finished: boolean
}
function trackPromise<T>(p: Promise<T>): TrackPromise<T> {
let status = { promise: null, finished: false };
status.promise = p.finally(() => status.finished = true);
return status;
}
function timeoutMs(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function process(opts: RunOpts, io: ProtocolIo, request: any) {
const requestType = request.request();
const completion = request.completion();
if (requestType.type == 'Http') {
try {
let headers = opts.headers;
if (requestType.headers != null && requestType.headers.length > 0) {
headers = { ...opts.headers };
for (let header of requestType.headers) {
headers[header[0]] = header[1];
}
}
const response = await fetch(`${opts.url}${requestType.path}`, {
method: requestType.method,
headers: headers,
body: requestType.body != null ? new Uint8Array(requestType.body) : null,
});
completion.status(response.status);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
completion.done();
break;
}
completion.pushBuffer(value);
}
} catch (error) {
completion.poison(`fetch error: ${error}`);
}
} else if (requestType.type == 'FullRead') {
try {
const metadata = await io.read(requestType.path);
if (metadata != null) {
completion.pushBuffer(metadata);
}
completion.done();
} catch (error) {
completion.poison(`metadata read error: ${error}`);
}
} else if (requestType.type == 'FullWrite') {
try {
await io.write(requestType.path, requestType.content);
completion.done();
} catch (error) {
completion.poison(`metadata write error: ${error}`);
}
} else if (requestType.type == 'Transform') {
if (opts.transform == null) {
completion.poison("transform is not set");
return;
}
const results = [];
for (const mutation of requestType.mutations) {
const result = opts.transform(mutation);
if (result == null) {
results.push({ type: 'Keep' });
} else if (result.operation == 'skip') {
results.push({ type: 'Skip' });
} else if (result.operation == 'rewrite') {
results.push({ type: 'Rewrite', stmt: result.stmt });
} else {
completion.poison("unexpected transform operation");
return;
}
}
completion.pushTransform(results);
completion.done();
}
}
export function memoryIO(): ProtocolIo {
let values = new Map();
return {
async read(path: string): Promise<Buffer | Uint8Array | null> {
return values.get(path);
},
async write(path: string, data: Buffer | Uint8Array): Promise<void> {
values.set(path, data);
}
}
};
export async function run(opts: RunOpts, io: ProtocolIo, engine: any, generator: any): Promise<any> {
let tasks = [];
while (true) {
const { type, ...rest }: GeneratorResponse = await generator.resumeAsync(null);
if (type == 'Done') {
return null;
}
if (type == 'SyncEngineStats') {
return rest;
}
for (let request = engine.protocolIo(); request != null; request = engine.protocolIo()) {
tasks.push(trackPromise(process(opts, io, request)));
}
const tasksRace = tasks.length == 0 ? Promise.resolve() : Promise.race([timeoutMs(opts.preemptionMs), ...tasks.map(t => t.promise)]);
await Promise.all([engine.ioLoopAsync(), tasksRace]);
tasks = tasks.filter(t => !t.finished);
}
return generator.take();
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"module": "esnext",
"target": "esnext",
"outDir": "dist/",
"lib": [
"es2020",
"dom"
],
},
"include": [
"*"
]
}

View File

@@ -0,0 +1,50 @@
export declare const enum DatabaseChangeType {
Insert = 0,
Update = 1,
Delete = 2
}
export interface DatabaseRowMutation {
changeTime: number
tableName: string
id: number
changeType: DatabaseChangeType
before?: Record<string, any>
after?: Record<string, any>
updates?: Record<string, any>
}
export type DatabaseRowTransformResult = { operation: 'skip' } | { operation: 'rewrite', stmt: DatabaseRowStatement } | null;
export type Transform = (arg: DatabaseRowMutation) => DatabaseRowTransformResult;
export interface RunOpts {
preemptionMs: number,
url: string,
headers: { [K: string]: string }
transform?: Transform,
}
export interface ProtocolIo {
read(path: string): Promise<Buffer | Uint8Array | null>;
write(path: string, content: Buffer | Uint8Array): Promise<void>;
}
export interface SyncOpts {
path: string;
clientName?: string;
url: string;
authToken?: string;
encryptionKey?: string;
tablesIgnore?: string[],
transform?: Transform,
tracing?: string,
}
export interface DatabaseRowStatement {
sql: string
values: Array<any>
}
export type GeneratorResponse =
| { type: 'IO' }
| { type: 'Done' }
| { type: 'SyncEngineStats', operations: number, mainWal: number, revertWal: number, lastPullUnixTime: number, lastPushUnixTime: number | null }

View File

@@ -0,0 +1,125 @@
<p align="center">
<h1 align="center">Turso Database for JavaScript in Node</h1>
</p>
<p align="center">
<a title="JavaScript" target="_blank" href="https://www.npmjs.com/package/@tursodatabase/database"><img alt="npm" src="https://img.shields.io/npm/v/@tursodatabase/database"></a>
<a title="MIT" target="_blank" href="https://github.com/tursodatabase/turso/blob/main/LICENSE.md"><img src="http://img.shields.io/badge/license-MIT-orange.svg?style=flat-square"></a>
</p>
<p align="center">
<a title="Users Discord" target="_blank" href="https://tur.so/discord"><img alt="Chat with other users of Turso on Discord" src="https://img.shields.io/discord/933071162680958986?label=Discord&logo=Discord&style=social"></a>
</p>
---
## About
This package is the Turso embedded database library for JavaScript in Node.
> **⚠️ Warning:** This software is ALPHA, only use for development, testing, and experimentation. We are working to make it production ready, but do not use it for critical data right now.
## Features
- **SQLite compatible:** SQLite query language and file format support ([status](https://github.com/tursodatabase/turso/blob/main/COMPAT.md)).
- **In-process**: No network overhead, runs directly in your Node.js process
- **TypeScript support**: Full TypeScript definitions included
- **Cross-platform**: Supports Linux (x86 and arm64), macOS, Windows (browser is supported in the separate package `@tursodatabase/database-browser` package)
## Installation
```bash
npm install @tursodatabase/database
```
## Getting Started
### In-Memory Database
```javascript
import { connect } from '@tursodatabase/database';
// Create an in-memory database
const db = await connect(':memory:');
// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', 'alice@example.com');
await insert.run('Bob', 'bob@example.com');
// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
// { id: 1, name: 'Alice', email: 'alice@example.com' },
// { id: 2, name: 'Bob', email: 'bob@example.com' }
// ]
```
### File-Based Database
```javascript
import { connect } from '@tursodatabase/database';
// Create or open a database file
const db = await connect('my-database.db');
// Create a table
await db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');
console.log(`Inserted post with ID: ${result.lastInsertRowid}`);
```
### Transactions
```javascript
import { connect } from '@tursodatabase/database';
const db = await connect('transactions.db');
// Using transactions for atomic operations
const transaction = db.transaction(async (users) => {
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
for (const user of users) {
await insert.run(user.name, user.email);
}
});
// Execute transaction
await transaction([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]);
```
## API Reference
For complete API documentation, see [JavaScript API Reference](../../../../docs/javascript-api-reference.md).
## Related Packages
* The [@tursodatabase/serverless](https://www.npmjs.com/package/@tursodatabase/serverless) package provides a serverless driver with the same API.
* The [@tursodatabase/sync](https://www.npmjs.com/package/@tursodatabase/sync) package provides bidirectional sync between a local Turso database and Turso Cloud.
## License
This project is licensed under the [MIT license](../../LICENSE.md).
## Support
- [GitHub Issues](https://github.com/tursodatabase/turso/issues)
- [Documentation](https://docs.turso.tech)
- [Discord Community](https://tur.so/discord)

View File

@@ -0,0 +1,244 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** A database connection. */
export declare class Database {
/**
* Creates a new database instance.
*
* # Arguments
* * `path` - The path to the database file.
*/
constructor(path: string, opts?: DatabaseOpts | undefined | null)
/** Returns whether the database is in memory-only mode. */
get memory(): boolean
/** Returns whether the database is in memory-only mode. */
get path(): string
/** Returns whether the database connection is open. */
get open(): boolean
/**
* Executes a batch of SQL statements on main thread
*
* # Arguments
*
* * `sql` - The SQL statements to execute.
*
* # Returns
*/
batchSync(sql: string): void
/**
* Executes a batch of SQL statements outside of main thread
*
* # Arguments
*
* * `sql` - The SQL statements to execute.
*
* # Returns
*/
batchAsync(sql: string): Promise<void>
/**
* Prepares a statement for execution.
*
* # Arguments
*
* * `sql` - The SQL statement to prepare.
*
* # Returns
*
* A `Statement` instance.
*/
prepare(sql: string): Statement
/**
* Returns the rowid of the last row inserted.
*
* # Returns
*
* The rowid of the last row inserted.
*/
lastInsertRowid(): number
/**
* Returns the number of changes made by the last statement.
*
* # Returns
*
* The number of changes made by the last statement.
*/
changes(): number
/**
* Returns the total number of changes made by all statements.
*
* # Returns
*
* The total number of changes made by all statements.
*/
totalChanges(): number
/**
* Closes the database connection.
*
* # Returns
*
* `Ok(())` if the database is closed successfully.
*/
close(): void
/**
* Sets the default safe integers mode for all statements from this database.
*
* # Arguments
*
* * `toggle` - Whether to use safe integers by default.
*/
defaultSafeIntegers(toggle?: boolean | undefined | null): void
/** Runs the I/O loop synchronously. */
ioLoopSync(): void
/** Runs the I/O loop asynchronously, returning a Promise. */
ioLoopAsync(): Promise<void>
}
/** A prepared statement. */
export declare class Statement {
reset(): void
/** Returns the number of parameters in the statement. */
parameterCount(): number
/**
* Returns the name of a parameter at a specific 1-based index.
*
* # Arguments
*
* * `index` - The 1-based parameter index.
*/
parameterName(index: number): string | null
/**
* Binds a parameter at a specific 1-based index with explicit type.
*
* # Arguments
*
* * `index` - The 1-based parameter index.
* * `value_type` - The type constant (0=null, 1=int, 2=float, 3=text, 4=blob).
* * `value` - The value to bind.
*/
bindAt(index: number, value: unknown): void
/**
* Step the statement and return result code (executed on the main thread):
* 1 = Row available, 2 = Done, 3 = I/O needed
*/
stepSync(): number
/**
* Step the statement and return result code (executed on the background thread):
* 1 = Row available, 2 = Done, 3 = I/O needed
*/
stepAsync(): Promise<number>
/** Get the current row data according to the presentation mode */
row(): unknown
/** Sets the presentation mode to raw. */
raw(raw?: boolean | undefined | null): void
/** Sets the presentation mode to pluck. */
pluck(pluck?: boolean | undefined | null): void
/**
* Sets safe integers mode for this statement.
*
* # Arguments
*
* * `toggle` - Whether to use safe integers.
*/
safeIntegers(toggle?: boolean | undefined | null): void
/** Get column information for the statement */
columns(): Promise<any>
/** Finalizes the statement. */
finalize(): void
}
export interface DatabaseOpts {
tracing?: string
}
export declare class GeneratorHolder {
resumeSync(error?: string | undefined | null): GeneratorResponse
resumeAsync(error?: string | undefined | null): Promise<unknown>
}
export declare class JsDataCompletion {
poison(err: string): void
status(value: number): void
pushBuffer(value: Buffer): void
pushTransform(values: Array<DatabaseRowTransformResultJs>): void
done(): void
}
export declare class JsProtocolIo {
takeRequest(): JsProtocolRequestBytes | null
}
export declare class JsProtocolRequestBytes {
request(): JsProtocolRequest
completion(): JsDataCompletion
}
export declare class SyncEngine {
constructor(opts: SyncEngineOpts)
init(): GeneratorHolder
ioLoopSync(): void
/** Runs the I/O loop asynchronously, returning a Promise. */
ioLoopAsync(): Promise<void>
protocolIo(): JsProtocolRequestBytes | null
sync(): GeneratorHolder
push(): GeneratorHolder
stats(): GeneratorHolder
pull(): GeneratorHolder
checkpoint(): GeneratorHolder
open(): Database
close(): void
}
export declare const enum DatabaseChangeTypeJs {
Insert = 0,
Update = 1,
Delete = 2
}
export interface DatabaseOpts {
path: string
}
export interface DatabaseRowMutationJs {
changeTime: number
tableName: string
id: number
changeType: DatabaseChangeTypeJs
before?: Record<string, any>
after?: Record<string, any>
updates?: Record<string, any>
}
export interface DatabaseRowStatementJs {
sql: string
values: Array<any>
}
export type DatabaseRowTransformResultJs =
| { type: 'Keep' }
| { type: 'Skip' }
| { type: 'Rewrite', stmt: DatabaseRowStatementJs }
export type GeneratorResponse =
| { type: 'IO' }
| { type: 'Done' }
| { type: 'SyncEngineStats', operations: number, mainWal: number, revertWal: number, lastPullUnixTime: number, lastPushUnixTime?: number }
export type JsProtocolRequest =
| { type: 'Http', method: string, path: string, body?: Array<number>, headers: Array<[string, string]> }
| { type: 'FullRead', path: string }
| { type: 'FullWrite', path: string, content: Array<number> }
| { type: 'Transform', mutations: Array<DatabaseRowMutationJs> }
export interface SyncEngineOpts {
path: string
clientName?: string
walPullBatchSize?: number
tracing?: string
tablesIgnore?: Array<string>
useTransform: boolean
protocolVersion?: SyncEngineProtocolVersion
}
export declare const enum SyncEngineProtocolVersion {
Legacy = 0,
V1 = 1
}

View File

@@ -0,0 +1,520 @@
// prettier-ignore
/* eslint-disable */
// @ts-nocheck
/* auto-generated by NAPI-RS */
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const __dirname = new URL('.', import.meta.url).pathname
const { readFileSync } = require('node:fs')
let nativeBinding = null
const loadErrors = []
const isMusl = () => {
let musl = false
if (process.platform === 'linux') {
musl = isMuslFromFilesystem()
if (musl === null) {
musl = isMuslFromReport()
}
if (musl === null) {
musl = isMuslFromChildProcess()
}
}
return musl
}
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
const isMuslFromFilesystem = () => {
try {
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
} catch {
return null
}
}
const isMuslFromReport = () => {
let report = null
if (typeof process.report?.getReport === 'function') {
process.report.excludeNetwork = true
report = process.report.getReport()
}
if (!report) {
return null
}
if (report.header && report.header.glibcVersionRuntime) {
return false
}
if (Array.isArray(report.sharedObjects)) {
if (report.sharedObjects.some(isFileMusl)) {
return true
}
}
return false
}
const isMuslFromChildProcess = () => {
try {
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
} catch (e) {
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
return false
}
}
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
} catch (err) {
loadErrors.push(err)
}
} else if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
return require('./sync.android-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-android-arm64')
const bindingPackageVersion = require('@tursodatabase/sync-android-arm64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./sync.android-arm-eabi.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-android-arm-eabi')
const bindingPackageVersion = require('@tursodatabase/sync-android-arm-eabi/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
try {
return require('./sync.win32-x64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-win32-x64-msvc')
const bindingPackageVersion = require('@tursodatabase/sync-win32-x64-msvc/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'ia32') {
try {
return require('./sync.win32-ia32-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-win32-ia32-msvc')
const bindingPackageVersion = require('@tursodatabase/sync-win32-ia32-msvc/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./sync.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-win32-arm64-msvc')
const bindingPackageVersion = require('@tursodatabase/sync-win32-arm64-msvc/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
} else if (process.platform === 'darwin') {
try {
return require('./sync.darwin-universal.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-darwin-universal')
const bindingPackageVersion = require('@tursodatabase/sync-darwin-universal/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
if (process.arch === 'x64') {
try {
return require('./sync.darwin-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-darwin-x64')
const bindingPackageVersion = require('@tursodatabase/sync-darwin-x64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./sync.darwin-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-darwin-arm64')
const bindingPackageVersion = require('@tursodatabase/sync-darwin-arm64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
}
} else if (process.platform === 'freebsd') {
if (process.arch === 'x64') {
try {
return require('./sync.freebsd-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-freebsd-x64')
const bindingPackageVersion = require('@tursodatabase/sync-freebsd-x64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./sync.freebsd-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-freebsd-arm64')
const bindingPackageVersion = require('@tursodatabase/sync-freebsd-arm64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
try {
return require('./sync.linux-x64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-x64-musl')
const bindingPackageVersion = require('@tursodatabase/sync-linux-x64-musl/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./sync.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-x64-gnu')
const bindingPackageVersion = require('@tursodatabase/sync-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
try {
return require('./sync.linux-arm64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-arm64-musl')
const bindingPackageVersion = require('@tursodatabase/sync-linux-arm64-musl/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./sync.linux-arm64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-arm64-gnu')
const bindingPackageVersion = require('@tursodatabase/sync-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./sync.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-arm-musleabihf')
const bindingPackageVersion = require('@tursodatabase/sync-linux-arm-musleabihf/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./sync.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-arm-gnueabihf')
const bindingPackageVersion = require('@tursodatabase/sync-linux-arm-gnueabihf/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./sync.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-riscv64-musl')
const bindingPackageVersion = require('@tursodatabase/sync-linux-riscv64-musl/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./sync.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-riscv64-gnu')
const bindingPackageVersion = require('@tursodatabase/sync-linux-riscv64-gnu/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./sync.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-ppc64-gnu')
const bindingPackageVersion = require('@tursodatabase/sync-linux-ppc64-gnu/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./sync.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-linux-s390x-gnu')
const bindingPackageVersion = require('@tursodatabase/sync-linux-s390x-gnu/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
} else if (process.platform === 'openharmony') {
if (process.arch === 'arm64') {
try {
return require('./sync.openharmony-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-openharmony-arm64')
const bindingPackageVersion = require('@tursodatabase/sync-openharmony-arm64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'x64') {
try {
return require('./sync.openharmony-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-openharmony-x64')
const bindingPackageVersion = require('@tursodatabase/sync-openharmony-x64/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./sync.openharmony-arm.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@tursodatabase/sync-openharmony-arm')
const bindingPackageVersion = require('@tursodatabase/sync-openharmony-arm/package.json').version
if (bindingPackageVersion !== '0.1.5-pre.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.1.5-pre.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
}
} else {
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
}
}
nativeBinding = requireNative()
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
try {
nativeBinding = require('./sync.wasi.cjs')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
loadErrors.push(err)
}
}
if (!nativeBinding) {
try {
nativeBinding = require('@tursodatabase/sync-wasm32-wasi')
} catch (err) {
if (process.env.NAPI_RS_FORCE_WASI) {
loadErrors.push(err)
}
}
}
}
if (!nativeBinding) {
if (loadErrors.length > 0) {
throw new Error(
`Cannot find native binding. ` +
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
{ cause: loadErrors }
)
}
throw new Error(`Failed to load native binding`)
}
const { Database, Statement, GeneratorHolder, JsDataCompletion, JsProtocolIo, JsProtocolRequestBytes, SyncEngine, DatabaseChangeTypeJs, SyncEngineProtocolVersion } = nativeBinding
export { Database }
export { Statement }
export { GeneratorHolder }
export { JsDataCompletion }
export { JsProtocolIo }
export { JsProtocolRequestBytes }
export { SyncEngine }
export { DatabaseChangeTypeJs }
export { SyncEngineProtocolVersion }

View File

@@ -0,0 +1,53 @@
{
"name": "@tursodatabase/sync",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
},
"license": "MIT",
"module": "./dist/promise.js",
"main": "./dist/promise.js",
"type": "module",
"exports": {
".": "./dist/promise.js",
"./compat": "./dist/compat.js"
},
"files": [
"index.js",
"dist/**",
"README.md"
],
"packageManager": "yarn@4.9.2",
"devDependencies": {
"@napi-rs/cli": "^3.1.5",
"@types/node": "^24.3.1",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"scripts": {
"napi-build": "napi build --platform --release --esm --manifest-path ../../Cargo.toml --output-dir .",
"napi-dirs": "napi create-npm-dirs",
"napi-artifacts": "napi artifacts --output-dir .",
"tsc-build": "npm exec tsc",
"build": "npm run napi-build && npm run tsc-build",
"test": "vitest --run",
"prepublishOnly": "npm run napi-dirs && npm run napi-artifacts && napi prepublish -t npm"
},
"napi": {
"binaryName": "sync",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"universal-apple-darwin",
"aarch64-unknown-linux-gnu"
]
},
"dependencies": {
"@tursodatabase/database-common": "^0.1.5",
"@tursodatabase/sync-common": "^0.1.5"
},
"imports": {
"#index": "./index.js"
}
}

View File

@@ -0,0 +1,288 @@
import { unlinkSync } from "node:fs";
import { expect, test } from 'vitest'
import { connect, DatabaseRowMutation, DatabaseRowTransformResult } from './promise.js'
const localeCompare = (a, b) => a.x.localeCompare(b.x);
test('select-after-push', 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.push();
await db.close();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("INSERT INTO t VALUES (1), (2), (3)");
await db.push();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
const rows = await db.prepare('SELECT * FROM t').all();
expect(rows).toEqual([{ x: 1 }, { x: 2 }, { x: 3 }])
}
})
test('select-without-push', 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.push();
await db.close();
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("INSERT INTO t VALUES (1), (2), (3)");
}
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
const rows = await db.prepare('SELECT * FROM t').all();
expect(rows).toEqual([])
}
})
test('merge-non-overlapping-keys', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k3', 'value3'), ('k4', 'value4'), ('k5', 'value5')");
await Promise.all([db1.push(), db2.push()]);
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value3' }, { x: 'k4', y: 'value4' }, { x: 'k5', y: 'value5' }];
expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare))
expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare))
})
test('last-push-wins', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')");
await db2.push();
await db1.push();
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'value1' }, { x: 'k2', y: 'value2' }, { x: 'k3', y: 'value5' }, { x: 'k4', y: 'value4' }];
expect(rows1.sort(localeCompare)).toEqual(expected.sort(localeCompare))
expect(rows2.sort(localeCompare)).toEqual(expected.sort(localeCompare))
})
test('last-push-wins-with-delete', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO q VALUES ('k1', 'value1'), ('k2', 'value2'), ('k4', 'value4')");
await db1.exec("DELETE FROM q")
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO q VALUES ('k1', 'value3'), ('k2', 'value4'), ('k3', 'value5')");
await db2.push();
await db1.push();
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM q').all();
const rows2 = await db1.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k3', y: 'value5' }];
expect(rows1).toEqual(expected)
expect(rows2).toEqual(expected)
})
test('constraint-conflict', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS u(x TEXT PRIMARY KEY, y UNIQUE)");
await db.exec("DELETE FROM u");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db1.exec("INSERT INTO u VALUES ('k1', 'value1')");
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db2.exec("INSERT INTO u VALUES ('k2', 'value1')");
await db1.push();
await expect(async () => await db2.push()).rejects.toThrow('SQLite error: UNIQUE constraint failed: u.y');
})
test('checkpoint', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
for (let i = 0; i < 1000; i++) {
await db1.exec(`INSERT INTO q VALUES ('k${i}', 'v${i}')`);
}
expect((await db1.stats()).mainWal).toBeGreaterThan(4096 * 1000);
await db1.checkpoint();
expect((await db1.stats()).mainWal).toBe(0);
let revertWal = (await db1.stats()).revertWal;
expect(revertWal).toBeLessThan(4096 * 1000 / 100);
for (let i = 0; i < 1000; i++) {
await db1.exec(`UPDATE q SET y = 'u${i}' WHERE x = 'k${i}'`);
}
await db1.checkpoint();
expect((await db1.stats()).revertWal).toBe(revertWal);
})
test('persistence', async () => {
{
const db = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL });
await db.exec("CREATE TABLE IF NOT EXISTS q(x TEXT PRIMARY KEY, y)");
await db.exec("DELETE FROM q");
await db.push();
await db.close();
}
const path = `test-${(Math.random() * 10000) | 0}.db`;
try {
{
const db1 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db1.exec(`INSERT INTO q VALUES ('k1', 'v1')`);
await db1.exec(`INSERT INTO q VALUES ('k2', 'v2')`);
await db1.close();
}
{
const db2 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db2.exec(`INSERT INTO q VALUES ('k3', 'v3')`);
await db2.exec(`INSERT INTO q VALUES ('k4', 'v4')`);
const rows = await db2.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }];
expect(rows).toEqual(expected)
await db2.close();
}
{
const db3 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
await db3.push();
await db3.close();
}
{
const db4 = await connect({ path: path, url: process.env.VITE_TURSO_DB_URL });
const rows = await db4.prepare('SELECT * FROM q').all();
const expected = [{ x: 'k1', y: 'v1' }, { x: 'k2', y: 'v2' }, { x: 'k3', y: 'v3' }, { x: 'k4', y: 'v4' }];
expect(rows).toEqual(expected)
await db4.close();
}
} finally {
unlinkSync(path);
unlinkSync(`${path}-wal`);
unlinkSync(`${path}-info`);
unlinkSync(`${path}-changes`);
try { unlinkSync(`${path}-revert`) } catch (e) { }
}
})
test('transform', async () => {
{
const db = await connect({
path: ':memory:',
url: process.env.VITE_TURSO_DB_URL,
});
await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)");
await db.exec("DELETE FROM counter");
await db.exec("INSERT INTO counter VALUES ('1', 0)")
await db.push();
await db.close();
}
const transform = (m: DatabaseRowMutation) => ({
operation: 'rewrite',
stmt: {
sql: `UPDATE counter SET value = value + ? WHERE key = ?`,
values: [m.after.value - m.before.value, m.after.key]
}
} as DatabaseRowTransformResult);
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
await Promise.all([db1.push(), db2.push()]);
await Promise.all([db1.pull(), db2.pull()]);
const rows1 = await db1.prepare('SELECT * FROM counter').all();
const rows2 = await db2.prepare('SELECT * FROM counter').all();
expect(rows1).toEqual([{ key: '1', value: 2 }]);
expect(rows2).toEqual([{ key: '1', value: 2 }]);
})
test('transform-many', async () => {
{
const db = await connect({
path: ':memory:',
url: process.env.VITE_TURSO_DB_URL,
});
await db.exec("CREATE TABLE IF NOT EXISTS counter(key TEXT PRIMARY KEY, value INTEGER)");
await db.exec("DELETE FROM counter");
await db.exec("INSERT INTO counter VALUES ('1', 0)")
await db.push();
await db.close();
}
const transform = (m: DatabaseRowMutation) => ({
operation: 'rewrite',
stmt: {
sql: `UPDATE counter SET value = value + ? WHERE key = ?`,
values: [m.after.value - m.before.value, m.after.key]
}
} as DatabaseRowTransformResult);
const db1 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
const db2 = await connect({ path: ':memory:', url: process.env.VITE_TURSO_DB_URL, transform: transform });
for (let i = 0; i < 1002; i++) {
await db1.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
}
for (let i = 0; i < 1001; i++) {
await db2.exec("UPDATE counter SET value = value + 1 WHERE key = '1'");
}
let start = performance.now();
await Promise.all([db1.push(), db2.push()]);
console.info('push', performance.now() - start);
start = performance.now();
await Promise.all([db1.pull(), db2.pull()]);
console.info('pull', performance.now() - start);
const rows1 = await db1.prepare('SELECT * FROM counter').all();
const rows2 = await db2.prepare('SELECT * FROM counter').all();
expect(rows1).toEqual([{ key: '1', value: 1001 + 1002 }]);
expect(rows2).toEqual([{ key: '1', value: 1001 + 1002 }]);
})

View File

@@ -0,0 +1,104 @@
import { DatabasePromise, DatabaseOpts, NativeDatabase } from "@tursodatabase/database-common"
import { ProtocolIo, run, SyncOpts, RunOpts, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult } from "@tursodatabase/sync-common";
import { Database as NativeDB, SyncEngine } from "#index";
import { promises } from "node:fs";
let NodeIO: ProtocolIo = {
async read(path: string): Promise<Buffer | Uint8Array | null> {
try {
return await promises.readFile(path);
} catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
},
async write(path: string, data: Buffer | Uint8Array): Promise<void> {
const unix = Math.floor(Date.now() / 1000);
const nonce = Math.floor(Math.random() * 1000000000);
const tmp = `${path}.tmp.${unix}.${nonce}`;
await promises.writeFile(tmp, new Uint8Array(data));
try {
await promises.rename(tmp, path);
} catch (err) {
await promises.unlink(tmp);
throw err;
}
}
};
function memoryIO(): ProtocolIo {
let values = new Map();
return {
async read(path: string): Promise<Buffer | Uint8Array | null> {
return values.get(path);
},
async write(path: string, data: Buffer | Uint8Array): Promise<void> {
values.set(path, data);
}
}
};
class Database extends DatabasePromise {
runOpts: RunOpts;
engine: any;
io: ProtocolIo;
constructor(db: NativeDatabase, io: ProtocolIo, runOpts: RunOpts, engine: any, opts: DatabaseOpts = {}) {
super(db, opts)
this.runOpts = runOpts;
this.engine = engine;
this.io = io;
}
async sync() {
await run(this.runOpts, this.io, this.engine, this.engine.sync());
}
async pull() {
await run(this.runOpts, this.io, this.engine, this.engine.pull());
}
async push() {
await run(this.runOpts, this.io, this.engine, this.engine.push());
}
async checkpoint() {
await run(this.runOpts, this.io, this.engine, this.engine.checkpoint());
}
async stats(): Promise<{ operations: number, mainWal: number, revertWal: number, lastPullUnixTime: number, lastPushUnixTime: number | null }> {
return (await run(this.runOpts, this.io, this.engine, this.engine.stats()));
}
override async close(): Promise<void> {
this.engine.close();
}
}
/**
* Creates a new database connection asynchronously.
*
* @param {string} path - Path to the database file.
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(opts: SyncOpts): Promise<Database> {
const engine = new SyncEngine({
path: opts.path,
clientName: opts.clientName,
tablesIgnore: opts.tablesIgnore,
useTransform: opts.transform != null,
tracing: opts.tracing,
protocolVersion: 1
});
const runOpts: RunOpts = {
url: opts.url,
headers: {
...(opts.authToken != null && { "Authorization": `Bearer ${opts.authToken}` }),
...(opts.encryptionKey != null && { "x-turso-encryption-key": opts.encryptionKey })
},
preemptionMs: 1,
transform: opts.transform,
};
let io = opts.path == ':memory:' ? memoryIO() : NodeIO;
await run(runOpts, io, engine, engine.init());
const nativeDb = engine.open();
return new Database(nativeDb as any, io, runOpts, engine, {});
}
export { connect, Database, DatabaseRowMutation, DatabaseRowStatement, DatabaseRowTransformResult }

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"module": "nodenext",
"target": "esnext",
"outDir": "dist/",
"lib": [
"es2020"
],
"paths": {
"#index": [
"./index.d.ts"
]
}
},
"include": [
"*"
]
}