diff --git a/.github/workflows/napi-sync.yml b/.github/workflows/napi-sync.yml
deleted file mode 100644
index 7f854cb75..000000000
--- a/.github/workflows/napi-sync.yml
+++ /dev/null
@@ -1,190 +0,0 @@
-name: Build & publish @tursodatabase/sync
-
-on:
- push:
- branches:
- - main
- tags:
- - v*
- pull_request:
- branches:
- - main
-
-env:
- DEBUG: napi:*
- APP_NAME: turso-sync-js
- MACOSX_DEPLOYMENT_TARGET: "10.13"
-
-defaults:
- run:
- working-directory: sync/javascript
-
-jobs:
- build:
- timeout-minutes: 20
- strategy:
- fail-fast: false
- matrix:
- settings:
- - host: windows-latest
- build: |
- yarn build --target x86_64-pc-windows-msvc
- yarn test
- target: x86_64-pc-windows-msvc
- - host: ubuntu-latest
- target: x86_64-unknown-linux-gnu
- docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
- build: yarn build --target x86_64-unknown-linux-gnu
- - host: macos-latest
- target: aarch64-apple-darwin
- build: yarn build --target aarch64-apple-darwin
- - host: blacksmith-2vcpu-ubuntu-2404-arm
- target: aarch64-unknown-linux-gnu
- build: yarn build --target aarch64-unknown-linux-gnu
- - host: ubuntu-latest
- target: wasm32-wasip1-threads
- setup: |
- rustup target add wasm32-wasip1-threads
- wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz
- tar -xvf wasi-sdk-25.0-x86_64-linux.tar.gz
- build: |
- export WASI_SDK_PATH="$(pwd)/wasi-sdk-25.0-x86_64-linux"
- export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
- export TARGET_CXXFLAGS="--target=wasm32-wasi-threads --sysroot=$(pwd)/wasi-sdk-25.0-x86_64-linux/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj -lsetjmp"
- export TARGET_CFLAGS="$TARGET_CXXFLAGS"
- yarn build --target wasm32-wasip1-threads
- name: stable - ${{ matrix.settings.target }} - node@20
- runs-on: ${{ matrix.settings.host }}
- steps:
- - uses: actions/checkout@v4
- - name: Setup node
- uses: actions/setup-node@v4
- if: ${{ !matrix.settings.docker }}
- with:
- node-version: 20
- - name: Install
- uses: dtolnay/rust-toolchain@stable
- if: ${{ !matrix.settings.docker }}
- with:
- toolchain: stable
- targets: ${{ matrix.settings.target }}
- - name: Cache cargo
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- .cargo-cache
- target/
- key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
- - uses: mlugg/setup-zig@v2
- if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }}
- with:
- version: 0.13.0
- - name: Setup toolchain
- run: ${{ matrix.settings.setup }}
- if: ${{ matrix.settings.setup }}
- shell: bash
- - name: Install dependencies
- run: yarn install
- - name: Setup node x86
- uses: actions/setup-node@v4
- if: matrix.settings.target == 'x86_64-pc-windows-msvc'
- with:
- node-version: 20
- architecture: x64
- - name: Build in docker
- uses: addnab/docker-run-action@v3
- if: ${{ matrix.settings.docker }}
- with:
- image: ${{ matrix.settings.docker }}
- options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/sync/javascript"
- run: ${{ matrix.settings.build }}
- - name: Build
- run: ${{ matrix.settings.build }}
- if: ${{ !matrix.settings.docker }}
- shell: bash
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: turso-sync-js-${{ matrix.settings.target }}
- path: |
- sync/javascript/${{ env.APP_NAME }}.*.node
- sync/javascript/${{ env.APP_NAME }}.*.wasm
- if-no-files-found: error
- test-linux-x64-gnu-binding:
- name: Test turso-sync-js on Linux-x64-gnu - node@${{ matrix.node }}
- needs:
- - build
- strategy:
- fail-fast: false
- matrix:
- node:
- - "20"
- runs-on: blacksmith-4vcpu-ubuntu-2404
- steps:
- - uses: actions/checkout@v4
- - name: Setup node
- uses: useblacksmith/setup-node@v5
- with:
- node-version: ${{ matrix.node }}
- - name: Install dependencies
- run: yarn install
- - name: Download artifacts
- uses: actions/download-artifact@v4
- with:
- name: turso-sync-js-x86_64-unknown-linux-gnu
- path: sync/javascript
- - name: List packages
- run: ls -R .
- shell: bash
- - name: Test turso-sync-js
- run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn test
- publish:
- name: Publish
- runs-on: ubuntu-latest
- if: startsWith(github.ref, 'refs/tags/v')
- permissions:
- contents: read
- id-token: write
- needs:
- - test-linux-x64-gnu-binding
- steps:
- - uses: actions/checkout@v4
- - name: Setup node
- uses: useblacksmith/setup-node@v5
- with:
- node-version: 20
- - name: Install dependencies
- run: yarn install
- - name: create npm dirs
- run: yarn napi create-npm-dirs
- - name: Download all artifacts
- uses: actions/download-artifact@v4
- with:
- path: sync/javascript/artifacts
- - name: Move artifacts
- run: yarn artifacts
- - name: List packages
- run: ls -R ./npm
- shell: bash
- - name: Publish
- run: |
- npm config set provenance true
- if git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+$";
- then
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- make publish-native
- make publish-browser
- elif git log -1 --pretty=%B | grep "^Turso [0-9]\+\.[0-9]\+\.[0-9]\+";
- then
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- make publish-native-next
- make publish-browser-next
- else
- echo "Not a release, skipping publish"
- fi
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/napi.yml b/.github/workflows/napi.yml
index c53237552..bc187c3d9 100644
--- a/.github/workflows/napi.yml
+++ b/.github/workflows/napi.yml
@@ -32,19 +32,41 @@ jobs:
settings:
- host: windows-latest
target: x86_64-pc-windows-msvc
+ artifact: db-bindings-x86_64-pc-windows-msvc
build: yarn workspace @tursodatabase/database napi-build --target x86_64-pc-windows-msvc
+ - host: windows-latest
+ target: x86_64-pc-windows-msvc
+ artifact: sync-bindings-x86_64-pc-windows-msvc
+ build: yarn workspace @tursodatabase/sync napi-build --target x86_64-pc-windows-msvc
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
+ artifact: db-bindings-x86_64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: yarn workspace @tursodatabase/database napi-build --target x86_64-unknown-linux-gnu
+ - host: ubuntu-latest
+ target: x86_64-unknown-linux-gnu
+ artifact: sync-bindings-x86_64-unknown-linux-gnu
+ docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
+ build: yarn workspace @tursodatabase/sync napi-build --target x86_64-unknown-linux-gnu
- host: macos-latest
target: aarch64-apple-darwin
+ artifact: db-bindings-aarch64-apple-darwin
build: yarn workspace @tursodatabase/database napi-build --target aarch64-apple-darwin
+ - host: macos-latest
+ target: aarch64-apple-darwin
+ artifact: sync-bindings-aarch64-apple-darwin
+ build: yarn workspace @tursodatabase/sync napi-build --target aarch64-apple-darwin
- host: blacksmith-2vcpu-ubuntu-2404-arm
target: aarch64-unknown-linux-gnu
+ artifact: db-bindings-aarch64-unknown-linux-gnu
build: yarn workspace @tursodatabase/database napi-build --target aarch64-unknown-linux-gnu
+ - host: blacksmith-2vcpu-ubuntu-2404-arm
+ target: aarch64-unknown-linux-gnu
+ artifact: sync-bindings-aarch64-unknown-linux-gnu
+ build: yarn workspace @tursodatabase/sync napi-build --target aarch64-unknown-linux-gnu
- host: ubuntu-latest
target: wasm32-wasip1-threads
+ artifact: db-bindings-wasm32-wasip1-threads
setup: |
rustup target add wasm32-wasip1-threads
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz
@@ -55,6 +77,19 @@ jobs:
export TARGET_CXXFLAGS="--target=wasm32-wasi-threads --sysroot=$(pwd)/wasi-sdk-25.0-x86_64-linux/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj -lsetjmp"
export TARGET_CFLAGS="$TARGET_CXXFLAGS"
yarn workspace @tursodatabase/database-browser build
+ - host: ubuntu-latest
+ target: wasm32-wasip1-threads
+ artifact: sync-bindings-wasm32-wasip1-threads
+ setup: |
+ rustup target add wasm32-wasip1-threads
+ wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz
+ tar -xvf wasi-sdk-25.0-x86_64-linux.tar.gz
+ build: |
+ export WASI_SDK_PATH="$(pwd)/wasi-sdk-25.0-x86_64-linux"
+ export CMAKE_BUILD_PARALLEL_LEVEL=$(nproc)
+ export TARGET_CXXFLAGS="--target=wasm32-wasi-threads --sysroot=$(pwd)/wasi-sdk-25.0-x86_64-linux/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj -lsetjmp"
+ export TARGET_CFLAGS="$TARGET_CXXFLAGS"
+ yarn workspace @tursodatabase/sync-browser build
name: stable - ${{ matrix.settings.target }} - node@20
runs-on: ${{ matrix.settings.host }}
steps:
@@ -112,13 +147,15 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
- name: bindings-${{ matrix.settings.target }}
+ name: ${{ matrix.settings.artifact }}
path: |
- bindings/javascript/packages/native/${{ env.APP_NAME }}.*.node
- bindings/javascript/packages/browser/${{ env.APP_NAME }}.*.wasm
+ bindings/javascript/packages/native/turso.*.node
+ bindings/javascript/packages/browser/turso.*.wasm
+ bindings/javascript/sync/packages/native/sync.*.node
+ bindings/javascript/sync/packages/browser/sync.*.wasm
if-no-files-found: error
- test-linux-x64-gnu-binding:
- name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }}
+ test-db-linux-x64-gnu-binding:
+ name: Test DB bindings on Linux-x64-gnu - node@${{ matrix.node }}
needs:
- build
strategy:
@@ -137,11 +174,12 @@ jobs:
run: yarn install
- name: Build common
run: yarn workspace @tursodatabase/database-common build
- - name: Download all artifacts
+ - name: Download all DB artifacts
uses: actions/download-artifact@v4
with:
- path: bindings/javascript/packages
+ path: bindings/javascript
merge-multiple: true
+ pattern: 'db*'
- name: List packages
run: ls -R .
shell: bash
@@ -154,18 +192,28 @@ jobs:
contents: read
id-token: write
needs:
- - test-linux-x64-gnu-binding
+ - test-db-linux-x64-gnu-binding
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: useblacksmith/setup-node@v5
with:
node-version: 20
- - name: Download all artifacts
+ - name: Download all DB artifacts
uses: actions/download-artifact@v4
with:
- path: bindings/javascript/packages
+ path: bindings/javascript
merge-multiple: true
+ pattern: 'db*'
+ - name: Download all sync artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: bindings/javascript
+ merge-multiple: true
+ pattern: 'sync*'
+ - name: List packages
+ run: ls -R .
+ shell: bash
- name: Install dependencies
run: yarn install
- name: Install dependencies
diff --git a/.gitignore b/.gitignore
index b851e8025..294d5a6dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,4 @@ profile.json.gz
simulator-output/
&1
+bisected.sql
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bd406681..5135aef17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,185 @@
# Changelog
+## 0.1.5 -- 2025-09-10
+
+### Added
+
+* add missing module type for browser package (Nikita Sivukhin)
+* Implement 2-args json_each (Mikaël Francoeur)
+* Add OPFS support to JavaScript bindings (Nikita Sivukhin)
+* test/fuzz: add UPDATE/DELETE fuzz test (Jussi Saurio)
+* add gen-bigass-database.py (Jussi Saurio)
+* Add assertion: we read a page with the correct id (Jussi Saurio)
+* support float without fractional part (Lâm Hoàng Phúc)
+* expr: use more efficient implementation for binary condition exprs (Jussi Saurio)
+* Add json_each table-valued function (1-arg only) (Mikaël Francoeur)
+* Add io_uring support to stress (Pekka Enberg)
+* Refactor LIMIT/OFFSET handling to support expressions (bit-aloo)
+* Encryption: add support for other AEGIS and AES-GCM cipher variants (Frank Denis)
+* introduce package.json for separate *-browser package (both database and sync) (Nikita Sivukhin)
+* introduce `eq/contains/starts_with/ends_with_ignore_ascii_case` macros (Lâm Hoàng Phúc)
+* introduce `match_ignore_ascii_case` macro (Lâm Hoàng Phúc)
+* core: Make strict schema support experimental (Pekka Enberg)
+* core/printf: support for more basic substitution types (Luiz Gustavo)
+* Return sqlite_version() without being initialized (Preston Thorpe)
+* Support encryption for raw WAL frames (Gaurav Sarma)
+* bindings/java: Implement date, time related methods under JDBC4PreparedStatement (Kim Seon Woo)
+* Support cipher and encryption key URI options (William Souza)
+* Implement UPSERT (Preston Thorpe)
+* CLI: implement `Line` output .mode (Andrey Oskin)
+* add sqlite integrity check back (Pedro Muniz)
+* core: Initial pass on synchronous pragma (Pekka Enberg)
+* Introduce and propagate `IOContext` as required (Avinash Sajjanshetty)
+* Add some docs on encryption (Avinash Sajjanshetty)
+* sqlite3: Implement sqlite3_malloc() and sqlite3_free() (Pekka Enberg)
+* sqlite3: Implement sqlite3_next_stmt() (Pekka Enberg)
+* core/translate: Add support (Pekka Enberg)
+* sqlite3: Implement sqlite3_db_filename() (Pekka Enberg)
+* flake.nix: add uv dependency to nativeBuildInputs (Ceferino Patino)
+* sqlite3: Implement sqlite3_bind_parameter_index() (Pekka Enberg)
+* sqlite3: Implement sqlite3_clear_bindings() (Pekka Enberg)
+* sqlite3: Implement sqlite3_get_autocommit() (Pekka Enberg)
+* Add support for AEGIS encryption algorithm (Avinash Sajjanshetty)
+* bindings/java: Implement batch operations for JDBC4Statement (Kim Seon Woo)
+* Add syntax highlighting for EXPLAIN and ANALYZE (Alex Miller)
+* Add basic support for ANALYZE statement (Alex Miller)
+* correctly implement offset() in parser (Lâm Hoàng Phúc)
+* Switch to new parser in core (Levy A.)
+* github: Remove Intel Mac support (Pekka Enberg)
+* add remove_file method to the IO (Nikita Sivukhin)
+* Add libc fault injection to Antithesis (Pekka Enberg)
+* core/mvcc: support for MVCC (Pere Diaz Bou)
+* SQLite C API improvements: add column type and column decltype (Danawan Bimantoro)
+* Initial pass to support per page encryption (Avinash Sajjanshetty)
+
+### Updated
+* clean `print_query_result` (Lâm Hoàng Phúc)
+* update update-script to properly handle JS workspace (Nikita Sivukhin)
+* no need `QueryStatistics` if `self.opts.timer` is not set (Lâm Hoàng Phúc)
+* optimizer: convert outer join to inner join if possible (Jussi Saurio)
+* Handle case where null flag is set in op_column (Jussi Saurio)
+* remove &1 (Lâm Hoàng Phúc)
+* reduce cloning `Arc` (Lâm Hoàng Phúc)
+* Evaluate left join seek key condition again after null row (Jussi Saurio)
+* use mlugg/setup-zig instead of unmaintained action (Kingsword)
+* Prevent setting of encryption keys if already set (Gaurav Sarma)
+* Remove RefCell from Cursor (Pedro Muniz)
+* Page Cache: optimize and use sieve/Gclock hybird algorithm in place of LRU (Preston Thorpe)
+* core: handle edge cases for read_varint (Sonny)
+* Persistence for DBSP-based materialized views (Glauber Costa)
+* io_uring: prevent out of order operations that could interfere with durability (Preston Thorpe)
+* core: Simplify WalFileShared life cycle (Pekka Enberg)
+* prevent modification to system tables. (Glauber Costa)
+* mark completion as done only after callback will be executed (Nikita Sivukhin)
+* core/mvcc: make commit_txn return on I/O (Pere Diaz Bou)
+* windows iterator returns no values for shorter slice (Lâm Hoàng Phúc)
+* Unify resolution of aggregate functions (Piotr Rżysko)
+* replace some matches with `match_ignore_ascii_case` macro (Lâm Hoàng Phúc)
+* Make io_uring sound for connections on multiple threads (Preston Thorpe)
+* build native package for ARM64 (Nikita Sivukhin)
+* refactor parser fmt (Lâm Hoàng Phúc)
+* string sometimes used as identifier quoting (Lâm Hoàng Phúc)
+* CURRENT_TIMESTAMP can fallback TK_ID (Lâm Hoàng Phúc)
+* remove `turso_sqlite3_parser` from `turso_parser` (Lâm Hoàng Phúc)
+* Simulate I/O in memory (Pedro)
+* Simulate I/O in memory (Pedro Muniz)
+* Refactor encryption to manage authentication tag internally (bit-aloo)
+* Unify handling of grouped and ungrouped aggregations (Piotr Rżysko)
+* Evict page from cache if page is unlocked and unloaded (Pedro Muniz)
+* Use u64 for file offsets in I/O and calculate such offsets in u64 (Preston Thorpe)
+* Document how to use CDC (Pavan Nambi)
+* Upgrade Rust version in simulator build Dockerfile (Preston Thorpe)
+* Parse booleans to integer literals in expressions (Preston Thorpe)
+* Simulator Profiles (Pedro Muniz)
+* Change views to use DBSP circuits (Glauber Costa)
+* core/wal: cache file size (Pere Diaz Bou)
+* Remove some code duplication in the CLI (Preston Thorpe)
+* core/translate: parse_table remove unnecessary clone of table name (Pere Diaz Bou)
+* Update COMPAT.md to remove CREATE INDEX default disabled (Preston Thorpe)
+* core/translate: remove unneessary agg clones (Pere Diaz Bou)
+* core/vdbe: Micro-optimize "zero_or_null" opcode (Pekka Enberg)
+* translate: with_capacity insns (Pere Diaz Bou)
+* perf: avoid constructing PageType in helper methods (Jussi Saurio)
+* refactor/perf: remove BTreePageInner (Jussi Saurio)
+* Improve integrity check (Nikita Sivukhin)
+* translate/insert: Improve string format performance (Pere Diaz Bou)
+* core/schema: get_dependent_materialized_views_unnormalized (Pere Diaz Bou)
+* core/util: emit literal, cow instead of replace (Pere Diaz Bou)
+* core/translate: sanize_string fast path improvement (Pere Diaz Bou)
+* core/io: Switch Unix I/O to use libc::pwrite() (Pekka Enberg)
+* Update README.md for Go documentation (Preston Thorpe)
+* improve sync engine (Nikita Sivukhin)
+* Remove Go bindings (Preston Thorpe)
+* core/storage: Micro-optimize Pager::commit_dirty_pages() (Pekka Enberg)
+* Rename Go driver to `turso` to not conflict with sqlite3 (Preston Thorpe)
+* Refactor: `Cell` instead of `RefCell` to store `CipherMode` in connection (Avinash Sajjanshetty)
+* Improve documentation of page pinning (Jussi Saurio)
+* Remove double indirection in the Parser (Pedro Muniz")
+* Fail CI run if Turso output differs from SQLite in TPC-H queries (Jussi Saurio)
+* Decouple SQL generation from Simulator crate (Pedro Muniz)
+* Make fill_cell_payload() safe for async IO and cache spilling (Jussi Saurio)
+* Remove Windows IO in place of Generic IO (Preston Thorpe)
+* Improve encryption API (Avinash Sajjanshetty)
+* Remove double indirection in the Parser (Pedro Muniz)
+* Update TPC-H running instructions in PERF.md (Alex Miller)
+* Truncate the WAL on last connection close (Preston Thorpe)
+* DBSP projection (Pekka Enberg)
+* Use vectored I/O for appending WAL frames (Preston Thorpe)
+* Remove unnecessary argument from Pager::end_tx() (Nikita Sivukhin)
+* refactor/btree: rewrite the find_free_cell() function (Jussi Saurio)
+* refactor/btree: rewrite the free_cell_range() function (Jussi Saurio)
+* Remove Result from signature (Mikaël Francoeur)
+* Remove duplicated attribute in (bit-aloo)
+* reduce cloning Token in parser (Lâm Hoàng Phúc)
+* refactor encryption module and make it configurable (Avinash Sajjanshetty)
+* Replace a couple refcells for types that trivially impl Copy (Preston Thorpe)
+* wal-api: allow to mix frames insert with SQL execution (Nikita Sivukhin)
+* move check code into parser (Lâm Hoàng Phúc)
+* Serialize compat tests and use Mutex::lock() instead of Mutex::try_lock() in UnixIO (Jussi Saurio)
+* sim: remove "run_once faults" (Jussi Saurio)
+* should not return a Completion when there is a page cache hit (Pedro Muniz)
+* github: Reduce Python build matrix (Pekka Enberg)
+* Page cache truncate (Nikita Sivukhin)
+* Wal api checkpoint seq (Nikita Sivukhin)
+* Use more structured approach in translate_insert (Jussi Saurio)
+* Remove hardcoded flag usage in DBHeader for encryption (Avinash Sajjanshetty)
+* properly execute pragmas - they may require some IO (Nikita Sivukhin)
+* Wal checkpoint upper bound (Nikita Sivukhin)
+* Improve WAL checkpointing performance (Preston Thorpe)
+* core/mvcc: store txid in conn and reset transaction state on commit (Pere Diaz Bou)
+* core/mvcc: start first rowid at 1 (Pere Diaz Bou)
+* refactor/vdbe: move insert-related seeking to VDBE from BTreeCursor (Jussi Saurio)
+
+### Fixed
+* Fix clear_page_cache method and rollback (Preston Thorpe)
+* Fix read_entire_wal_dumb: incrementally build the frame cache (Preston Thorpe)
+* Fix merge script to prompt if tests are still in progress (Preston Thorpe)
+* SQL generation fixes (Pekka Enberg)
+* Fix affinity handling in MakeRecord (Pekka Enberg)
+* Fix infinite loop when IO failure happens on allocating first page (Preston Thorpe)
+* Fix crash in Next opcode if cursor stack has no pages (Jussi Saurio)
+* cli: Fix dump compatibility in "PRAGMA foreign_keys" (Pekka Enberg)
+* Small fixes (Nikita Sivukhin)
+* Avoid allocating and then immediately fallbacking errors in affinity (Jussi Saurio)
+* Fix float formatting and comparison + Blob concat (Levy A.)
+* Fix infinite loop when query starts comment token ("--") (Lâm Hoàng Phúc)
+* Fix sqlite3 test cases (Pekka Enberg)
+* Fix non-determinism in simulator (Pedro Muniz)
+* Fix column count in ImmutableRow (Glauber Costa)
+* Fix memory leak in page cache during balancing (Preston Thorpe)
+* Fix `sim-schema` command (Pedro Muniz)
+* Propagate decryption error from the callback (Avinash Sajjanshetty)
+* Fix sorter column deduplication (Piotr Rżysko)
+* Fix missing functions after revert (Pedro Muniz)
+* ci: fix merge-pr issue to escape command-line backticks (Ceferino Patino)
+* Fix several issues with integrity_check (Jussi Saurio)
+* core/io: Fix build on Android and iOS (Pekka Enberg)
+* WAL txn: fix reads from DB file (Nikita Sivukhin)
+* Fix blob type handling in JavaScript (Pekka Enberg)
+* Fix: all indexes need to be updated if the rowid changes (Jussi Saurio)
+* Fix: in UPDATE, insert rowid into index instead of NULL (Jussi Saurio)
+* Fix: normalize table name in DELETE (Jussi Saurio)
+
## 0.1.4 -- 2025-08-20
### Added
diff --git a/COMPAT.md b/COMPAT.md
index ebbb3f53b..2019f51c3 100644
--- a/COMPAT.md
+++ b/COMPAT.md
@@ -343,7 +343,7 @@ Modifiers:
| TimeOffset | Yes | |
| DateOffset | Yes | |
| DateTimeOffset | Yes | |
-| Ceiling | No | |
+| Ceiling | Yes | |
| Floor | No | |
| StartOfMonth | Yes | |
| StartOfYear | Yes | |
diff --git a/Cargo.lock b/Cargo.lock
index 3078152e6..30e8a1fb1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -104,12 +104,6 @@ dependencies = [
"backtrace",
]
-[[package]]
-name = "allocator-api2"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
-
[[package]]
name = "anarchist-readable-name-generator-lib"
version = "0.1.2"
@@ -667,7 +661,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_tester"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1554,8 +1548,6 @@ version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
- "allocator-api2",
- "equivalent",
"foldhash",
]
@@ -2126,7 +2118,7 @@ dependencies = [
[[package]]
name = "limbo_completion"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"mimalloc",
"turso_ext",
@@ -2134,7 +2126,7 @@ dependencies = [
[[package]]
name = "limbo_crypto"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"blake3",
"data-encoding",
@@ -2147,7 +2139,7 @@ dependencies = [
[[package]]
name = "limbo_csv"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"csv",
"mimalloc",
@@ -2157,7 +2149,7 @@ dependencies = [
[[package]]
name = "limbo_ipaddr"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"ipnetwork",
"mimalloc",
@@ -2166,7 +2158,7 @@ dependencies = [
[[package]]
name = "limbo_percentile"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"mimalloc",
"turso_ext",
@@ -2174,7 +2166,7 @@ dependencies = [
[[package]]
name = "limbo_regexp"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"mimalloc",
"regex",
@@ -2183,7 +2175,7 @@ dependencies = [
[[package]]
name = "limbo_sim"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anyhow",
"chrono",
@@ -2216,7 +2208,7 @@ dependencies = [
[[package]]
name = "limbo_sqlite_test_ext"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"cc",
]
@@ -2281,15 +2273,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
-[[package]]
-name = "lru"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198"
-dependencies = [
- "hashbrown 0.15.2",
-]
-
[[package]]
name = "matchers"
version = "0.1.0"
@@ -2971,7 +2954,7 @@ dependencies = [
[[package]]
name = "py-turso"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anyhow",
"pyo3",
@@ -3666,7 +3649,7 @@ checksum = "d372029cb5195f9ab4e4b9aef550787dce78b124fcaee8d82519925defcd6f0d"
[[package]]
name = "sql_generation"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anarchist-readable-name-generator-lib 0.2.0",
"anyhow",
@@ -4176,7 +4159,7 @@ dependencies = [
[[package]]
name = "turso"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
@@ -4188,7 +4171,7 @@ dependencies = [
[[package]]
name = "turso-java"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"jni",
"thiserror 2.0.12",
@@ -4197,7 +4180,7 @@ dependencies = [
[[package]]
name = "turso_cli"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anyhow",
"cfg-if",
@@ -4230,7 +4213,7 @@ dependencies = [
[[package]]
name = "turso_core"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"aegis",
"aes",
@@ -4252,7 +4235,6 @@ dependencies = [
"libc",
"libloading",
"libm",
- "lru",
"memory-stats",
"miette",
"mimalloc",
@@ -4289,7 +4271,7 @@ dependencies = [
[[package]]
name = "turso_dart"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"flutter_rust_bridge",
"turso_core",
@@ -4297,7 +4279,7 @@ dependencies = [
[[package]]
name = "turso_ext"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"chrono",
"getrandom 0.3.2",
@@ -4306,7 +4288,7 @@ dependencies = [
[[package]]
name = "turso_ext_tests"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"env_logger 0.11.7",
"lazy_static",
@@ -4317,7 +4299,7 @@ dependencies = [
[[package]]
name = "turso_macros"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"proc-macro2",
"quote",
@@ -4326,7 +4308,7 @@ dependencies = [
[[package]]
name = "turso_node"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"napi",
"napi-build",
@@ -4338,7 +4320,7 @@ dependencies = [
[[package]]
name = "turso_parser"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"bitflags 2.9.0",
"criterion",
@@ -4354,7 +4336,7 @@ dependencies = [
[[package]]
name = "turso_sqlite3"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"env_logger 0.11.7",
"libc",
@@ -4367,7 +4349,7 @@ dependencies = [
[[package]]
name = "turso_sqlite3_parser"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"bitflags 2.9.0",
"cc",
@@ -4385,7 +4367,7 @@ dependencies = [
[[package]]
name = "turso_stress"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"anarchist-readable-name-generator-lib 0.1.2",
"antithesis_sdk",
@@ -4401,7 +4383,7 @@ dependencies = [
[[package]]
name = "turso_sync_engine"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"base64",
"bytes",
@@ -4427,7 +4409,7 @@ dependencies = [
[[package]]
name = "turso_sync_js"
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
dependencies = [
"genawaiter",
"http",
diff --git a/Cargo.toml b/Cargo.toml
index e393d48e3..037a49bac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"bindings/dart/rust",
"bindings/java",
"bindings/javascript",
+ "bindings/javascript/sync",
"bindings/python",
"bindings/rust",
"cli",
@@ -27,35 +28,34 @@ members = [
"vendored/sqlite3-parser/sqlparser_bench",
"parser",
"sync/engine",
- "sync/javascript",
"sql_generation",
]
exclude = ["perf/latency/limbo"]
[workspace.package]
-version = "0.1.5-pre.5"
+version = "0.2.0-pre.1"
authors = ["the Limbo authors"]
edition = "2021"
license = "MIT"
repository = "https://github.com/tursodatabase/turso"
[workspace.dependencies]
-turso = { path = "bindings/rust", version = "0.1.5-pre.5" }
-turso_node = { path = "bindings/javascript", version = "0.1.5-pre.5" }
-limbo_completion = { path = "extensions/completion", version = "0.1.5-pre.5" }
-turso_core = { path = "core", version = "0.1.5-pre.5" }
-turso_sync_engine = { path = "sync/engine", version = "0.1.5-pre.5" }
-limbo_crypto = { path = "extensions/crypto", version = "0.1.5-pre.5" }
-limbo_csv = { path = "extensions/csv", version = "0.1.5-pre.5" }
-turso_ext = { path = "extensions/core", version = "0.1.5-pre.5" }
-turso_ext_tests = { path = "extensions/tests", version = "0.1.5-pre.5" }
-limbo_ipaddr = { path = "extensions/ipaddr", version = "0.1.5-pre.5" }
-turso_macros = { path = "macros", version = "0.1.5-pre.5" }
-limbo_percentile = { path = "extensions/percentile", version = "0.1.5-pre.5" }
-limbo_regexp = { path = "extensions/regexp", version = "0.1.5-pre.5" }
-turso_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.1.5-pre.5" }
-limbo_uuid = { path = "extensions/uuid", version = "0.1.5-pre.5" }
-turso_parser = { path = "parser", version = "0.1.5-pre.5" }
+turso = { path = "bindings/rust", version = "0.2.0-pre.1" }
+turso_node = { path = "bindings/javascript", version = "0.2.0-pre.1" }
+limbo_completion = { path = "extensions/completion", version = "0.2.0-pre.1" }
+turso_core = { path = "core", version = "0.2.0-pre.1" }
+turso_sync_engine = { path = "sync/engine", version = "0.2.0-pre.1" }
+limbo_crypto = { path = "extensions/crypto", version = "0.2.0-pre.1" }
+limbo_csv = { path = "extensions/csv", version = "0.2.0-pre.1" }
+turso_ext = { path = "extensions/core", version = "0.2.0-pre.1" }
+turso_ext_tests = { path = "extensions/tests", version = "0.2.0-pre.1" }
+limbo_ipaddr = { path = "extensions/ipaddr", version = "0.2.0-pre.1" }
+turso_macros = { path = "macros", version = "0.2.0-pre.1" }
+limbo_percentile = { path = "extensions/percentile", version = "0.2.0-pre.1" }
+limbo_regexp = { path = "extensions/regexp", version = "0.2.0-pre.1" }
+turso_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.2.0-pre.1" }
+limbo_uuid = { path = "extensions/uuid", version = "0.2.0-pre.1" }
+turso_parser = { path = "parser", version = "0.2.0-pre.1" }
sql_generation = { path = "sql_generation" }
strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26"
diff --git a/Makefile b/Makefile
index 30185f9e8..5275d201f 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ MINIMUM_TCL_VERSION := 8.6
SQLITE_EXEC ?= scripts/limbo-sqlite3
RUST_LOG := off
-all: check-rust-version limbo
+all: check-rust-version build
.PHONY: all
check-rust-version:
@@ -39,13 +39,13 @@ check-tcl-version:
| tclsh
.PHONY: check-tcl-version
-limbo:
+build: check-rust-version
cargo build
-.PHONY: limbo
+.PHONY: build
-limbo-c:
+turso-c:
cargo cbuild
-.PHONY: limbo-c
+.PHONY: turso-c
uv-sync:
uv sync --all-packages
@@ -55,14 +55,14 @@ uv-sync-test:
uv sync --all-extras --dev --package turso_test
.PHONE: uv-sync
-test: limbo uv-sync-test test-compat test-alter-column test-vector test-sqlite3 test-shell test-memory test-write test-update test-constraint test-collate test-extensions test-mvcc test-matviews
+test: build uv-sync-test test-compat test-alter-column test-vector test-sqlite3 test-shell test-memory test-write test-update test-constraint test-collate test-extensions test-mvcc test-matviews
.PHONY: test
-test-extensions: limbo uv-sync-test
+test-extensions: build uv-sync-test
RUST_LOG=$(RUST_LOG) uv run --project limbo_test test-extensions
.PHONY: test-extensions
-test-shell: limbo uv-sync-test
+test-shell: build uv-sync-test
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-shell
.PHONY: test-shell
@@ -100,11 +100,11 @@ test-json:
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) ./testing/json.test
.PHONY: test-json
-test-memory: limbo uv-sync-test
+test-memory: build uv-sync-test
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-memory
.PHONY: test-memory
-test-write: limbo uv-sync-test
+test-write: build uv-sync-test
@if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-write; \
else \
@@ -112,7 +112,7 @@ test-write: limbo uv-sync-test
fi
.PHONY: test-write
-test-update: limbo uv-sync-test
+test-update: build uv-sync-test
@if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-update; \
else \
@@ -120,7 +120,7 @@ test-update: limbo uv-sync-test
fi
.PHONY: test-update
-test-collate: limbo uv-sync-test
+test-collate: build uv-sync-test
@if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-collate; \
else \
@@ -128,7 +128,7 @@ test-collate: limbo uv-sync-test
fi
.PHONY: test-collate
-test-constraint: limbo uv-sync-test
+test-constraint: build uv-sync-test
@if [ "$(SQLITE_EXEC)" != "scripts/limbo-sqlite3" ]; then \
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-constraint; \
else \
@@ -136,22 +136,22 @@ test-constraint: limbo uv-sync-test
fi
.PHONY: test-constraint
-test-mvcc: limbo uv-sync-test
+test-mvcc: build uv-sync-test
RUST_LOG=$(RUST_LOG) SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-mvcc;
.PHONY: test-mvcc
-bench-vfs: uv-sync-test
- cargo build --release
+bench-vfs: uv-sync-test build-release
RUST_LOG=$(RUST_LOG) uv run --project limbo_test bench-vfs "$(SQL)" "$(N)"
-bench-sqlite: uv-sync-test
- cargo build --release
+bench-sqlite: uv-sync-test build-release
RUST_LOG=$(RUST_LOG) uv run --project limbo_test bench-sqlite "$(VFS)" "$(SQL)" "$(N)"
clickbench:
./perf/clickbench/benchmark.sh
.PHONY: clickbench
+build-release: check-rust-version
+ cargo build --bin tursodb --release --features=tracing_release
bench-exclude-tpc-h:
@benchmarks=$$(cargo bench --bench 2>&1 | grep -A 1000 '^Available bench targets:' | grep -v '^Available bench targets:' | grep -v '^ *$$' | grep -v 'tpc_h_benchmark' | xargs -I {} printf -- "--bench %s " {}); \
diff --git a/bindings/dart/rust/Cargo.toml b/bindings/dart/rust/Cargo.toml
index ace4c80a7..cbc0c6fbb 100644
--- a/bindings/dart/rust/Cargo.toml
+++ b/bindings/dart/rust/Cargo.toml
@@ -7,6 +7,9 @@ license.workspace = true
repository.workspace = true
publish = false
+[features]
+release = ["turso_core/tracing_release"]
+
[lib]
crate-type = ["cdylib", "staticlib"]
diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml
index b0f69db94..93858e0a1 100644
--- a/bindings/java/Cargo.toml
+++ b/bindings/java/Cargo.toml
@@ -6,7 +6,8 @@ edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
-
+[features]
+tracing_release = ["turso_core/tracing_release"]
[lib]
name = "_turso_java"
crate-type = ["cdylib"]
diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml
index a3b2384fe..836780122 100644
--- a/bindings/javascript/Cargo.toml
+++ b/bindings/javascript/Cargo.toml
@@ -20,6 +20,6 @@ tracing.workspace = true
[features]
encryption = ["turso_core/encryption"]
browser = []
-
+tracing_release = ["turso_core/tracing_release"]
[build-dependencies]
napi-build = "2.2.3"
diff --git a/bindings/javascript/examples/browser/index.html b/bindings/javascript/examples/browser/index.html
new file mode 100644
index 000000000..540e86750
--- /dev/null
+++ b/bindings/javascript/examples/browser/index.html
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+ Brutal DB Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bindings/javascript/examples/browser/package.json b/bindings/javascript/examples/browser/package.json
new file mode 100644
index 000000000..f77eb2b32
--- /dev/null
+++ b/bindings/javascript/examples/browser/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "wasm",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "devDependencies": {
+ "vite": "^7.1.4"
+ },
+ "dependencies": {
+ "@tursodatabase/database-browser": "../../browser"
+ }
+}
diff --git a/bindings/javascript/examples/browser/vite.config.js b/bindings/javascript/examples/browser/vite.config.js
new file mode 100644
index 000000000..3d37c5172
--- /dev/null
+++ b/bindings/javascript/examples/browser/vite.config.js
@@ -0,0 +1,22 @@
+import { defineConfig, searchForWorkspaceRoot } from 'vite'
+
+export default defineConfig({
+ server: {
+ fs: {
+ allow: ['.', '../../']
+ },
+ define:
+ {
+ 'process.env.NODE_DEBUG_NATIVE': 'false', // string replace at build-time
+ },
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ }
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ define: { 'process.env.NODE_DEBUG_NATIVE': 'false' },
+ },
+ },
+})
diff --git a/bindings/javascript/examples/wasm/index.html b/bindings/javascript/examples/wasm/index.html
new file mode 100644
index 000000000..efd7b0b7b
--- /dev/null
+++ b/bindings/javascript/examples/wasm/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/bindings/javascript/examples/wasm/package.json b/bindings/javascript/examples/wasm/package.json
new file mode 100644
index 000000000..3cd63f705
--- /dev/null
+++ b/bindings/javascript/examples/wasm/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "wasm",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "devDependencies": {
+ "vite": "^7.1.4"
+ },
+ "dependencies": {
+ "@tursodatabase/database": "../.."
+ }
+}
diff --git a/bindings/javascript/examples/wasm/vite.config.js b/bindings/javascript/examples/wasm/vite.config.js
new file mode 100644
index 000000000..299f21d60
--- /dev/null
+++ b/bindings/javascript/examples/wasm/vite.config.js
@@ -0,0 +1,26 @@
+import { defineConfig, searchForWorkspaceRoot } from 'vite'
+
+export default defineConfig({
+ build: {
+ minify: false, // Set this to false to disable minification
+ },
+ resolve: {
+ alias: {
+ '@tursodatabase/database-wasm32-wasi': '../../turso.wasi-browser.js'
+ },
+ },
+ server: {
+ fs: {
+ allow: ['.']
+ },
+ headers: {
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ }
+ },
+ optimizeDeps: {
+ exclude: [
+ "@tursodatabase/database-wasm32-wasi",
+ ]
+ },
+})
diff --git a/bindings/javascript/package-lock.json b/bindings/javascript/package-lock.json
index 551080310..685d79b56 100644
--- a/bindings/javascript/package-lock.json
+++ b/bindings/javascript/package-lock.json
@@ -1,15 +1,19 @@
{
"name": "javascript",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"workspaces": [
"packages/common",
"packages/native",
- "packages/browser"
+ "packages/browser",
+ "packages/browser-common",
+ "sync/packages/common",
+ "sync/packages/native",
+ "sync/packages/browser"
]
},
"node_modules/@babel/code-frame": {
@@ -1103,10 +1107,26 @@
"resolved": "packages/browser",
"link": true
},
+ "node_modules/@tursodatabase/database-browser-common": {
+ "resolved": "packages/browser-common",
+ "link": true
+ },
"node_modules/@tursodatabase/database-common": {
"resolved": "packages/common",
"link": true
},
+ "node_modules/@tursodatabase/sync": {
+ "resolved": "sync/packages/native",
+ "link": true
+ },
+ "node_modules/@tursodatabase/sync-browser": {
+ "resolved": "sync/packages/browser",
+ "link": true
+ },
+ "node_modules/@tursodatabase/sync-common": {
+ "resolved": "sync/packages/common",
+ "link": true
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
@@ -2485,11 +2505,12 @@
},
"packages/browser": {
"name": "@tursodatabase/database-browser",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"license": "MIT",
"dependencies": {
"@napi-rs/wasm-runtime": "^1.0.3",
- "@tursodatabase/database-common": "^0.1.5-pre.5"
+ "@tursodatabase/database-browser-common": "^0.2.0-pre.1",
+ "@tursodatabase/database-common": "^0.2.0-pre.1"
},
"devDependencies": {
"@napi-rs/cli": "^3.1.5",
@@ -2499,9 +2520,17 @@
"vitest": "^3.2.4"
}
},
+ "packages/browser-common": {
+ "name": "@tursodatabase/database-browser-common",
+ "version": "0.2.0-pre.1",
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.9.2"
+ }
+ },
"packages/common": {
"name": "@tursodatabase/database-common",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"license": "MIT",
"devDependencies": {
"typescript": "^5.9.2"
@@ -2518,10 +2547,51 @@
},
"packages/native": {
"name": "@tursodatabase/database",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"license": "MIT",
"dependencies": {
- "@tursodatabase/database-common": "^0.1.5-pre.5"
+ "@tursodatabase/database-common": "^0.2.0-pre.1"
+ },
+ "devDependencies": {
+ "@napi-rs/cli": "^3.1.5",
+ "@types/node": "^24.3.1",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
+ }
+ },
+ "sync/packages/browser": {
+ "name": "@tursodatabase/sync-browser",
+ "version": "0.2.0-pre.1",
+ "license": "MIT",
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.3",
+ "@tursodatabase/database-browser-common": "^0.2.0-pre.1",
+ "@tursodatabase/database-common": "^0.2.0-pre.1",
+ "@tursodatabase/sync-common": "^0.2.0-pre.1"
+ },
+ "devDependencies": {
+ "@napi-rs/cli": "^3.1.5",
+ "@vitest/browser": "^3.2.4",
+ "playwright": "^1.55.0",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
+ }
+ },
+ "sync/packages/common": {
+ "name": "@tursodatabase/sync-common",
+ "version": "0.2.0-pre.1",
+ "license": "MIT",
+ "devDependencies": {
+ "typescript": "^5.9.2"
+ }
+ },
+ "sync/packages/native": {
+ "name": "@tursodatabase/sync",
+ "version": "0.2.0-pre.1",
+ "license": "MIT",
+ "dependencies": {
+ "@tursodatabase/database-common": "^0.2.0-pre.1",
+ "@tursodatabase/sync-common": "^0.2.0-pre.1"
},
"devDependencies": {
"@napi-rs/cli": "^3.1.5",
diff --git a/bindings/javascript/package.json b/bindings/javascript/package.json
index 70213de55..1e3b9bd88 100644
--- a/bindings/javascript/package.json
+++ b/bindings/javascript/package.json
@@ -7,7 +7,11 @@
"workspaces": [
"packages/common",
"packages/native",
- "packages/browser"
+ "packages/browser",
+ "packages/browser-common",
+ "sync/packages/common",
+ "sync/packages/native",
+ "sync/packages/browser"
],
- "version": "0.1.5-pre.5"
+ "version": "0.2.0-pre.1"
}
diff --git a/bindings/javascript/packages/browser-common/README.md b/bindings/javascript/packages/browser-common/README.md
new file mode 100644
index 000000000..179123f7f
--- /dev/null
+++ b/bindings/javascript/packages/browser-common/README.md
@@ -0,0 +1,8 @@
+## About
+
+This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser.
+
+Do not use this package directly - instead you must use `@tursodatabase/database` or `@tursodatabase/database-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.
+
diff --git a/bindings/javascript/packages/browser-common/index.ts b/bindings/javascript/packages/browser-common/index.ts
new file mode 100644
index 000000000..9fab8790f
--- /dev/null
+++ b/bindings/javascript/packages/browser-common/index.ts
@@ -0,0 +1,239 @@
+function getUint8ArrayFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): Uint8Array {
+ ptr = ptr >>> 0;
+ return new Uint8Array(memory.buffer).subarray(ptr, ptr + len);
+}
+
+function getStringFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): string {
+ const shared = getUint8ArrayFromMemory(memory, ptr, len);
+ const copy = new Uint8Array(shared.length);
+ copy.set(shared);
+ const decoder = new TextDecoder('utf-8');
+ return decoder.decode(copy);
+}
+
+interface BrowserImports {
+ is_web_worker(): boolean;
+ lookup_file(ptr: number, len: number): number;
+ read(handle: number, ptr: number, len: number, offset: number): number;
+ write(handle: number, ptr: number, len: number, offset: number): number;
+ sync(handle: number): number;
+ truncate(handle: number, len: number): number;
+ size(handle: number): number;
+}
+
+function panic(name): never {
+ throw new Error(`method ${name} must be invoked only from the main thread`);
+}
+
+const MainDummyImports: BrowserImports = {
+ is_web_worker: function (): boolean {
+ return false;
+ },
+ lookup_file: function (ptr: number, len: number): number {
+ panic("lookup_file")
+ },
+ read: function (handle: number, ptr: number, len: number, offset: number): number {
+ panic("read")
+ },
+ write: function (handle: number, ptr: number, len: number, offset: number): number {
+ panic("write")
+ },
+ sync: function (handle: number): number {
+ panic("sync")
+ },
+ truncate: function (handle: number, len: number): number {
+ panic("truncate")
+ },
+ size: function (handle: number): number {
+ panic("size")
+ }
+};
+
+function workerImports(opfs: OpfsDirectory, memory: WebAssembly.Memory): BrowserImports {
+ return {
+ is_web_worker: function (): boolean {
+ return true;
+ },
+ lookup_file: function (ptr: number, len: number): number {
+ try {
+ const handle = opfs.lookupFileHandle(getStringFromMemory(memory, ptr, len));
+ return handle == null ? -404 : handle;
+ } catch (e) {
+ return -1;
+ }
+ },
+ read: function (handle: number, ptr: number, len: number, offset: number): number {
+ try {
+ return opfs.read(handle, getUint8ArrayFromMemory(memory, ptr, len), offset);
+ } catch (e) {
+ return -1;
+ }
+ },
+ write: function (handle: number, ptr: number, len: number, offset: number): number {
+ try {
+ return opfs.write(handle, getUint8ArrayFromMemory(memory, ptr, len), offset)
+ } catch (e) {
+ return -1;
+ }
+ },
+ sync: function (handle: number): number {
+ try {
+ opfs.sync(handle);
+ return 0;
+ } catch (e) {
+ return -1;
+ }
+ },
+ truncate: function (handle: number, len: number): number {
+ try {
+ opfs.truncate(handle, len);
+ return 0;
+ } catch (e) {
+ return -1;
+ }
+ },
+ size: function (handle: number): number {
+ try {
+ return opfs.size(handle);
+ } catch (e) {
+ return -1;
+ }
+ }
+ }
+}
+
+class OpfsDirectory {
+ fileByPath: Map;
+ fileByHandle: Map;
+ fileHandleNo: number;
+
+ constructor() {
+ this.fileByPath = new Map();
+ this.fileByHandle = new Map();
+ this.fileHandleNo = 0;
+ }
+
+ async registerFile(path: string) {
+ if (this.fileByPath.has(path)) {
+ return;
+ }
+ const opfsRoot = await navigator.storage.getDirectory();
+ const opfsHandle = await opfsRoot.getFileHandle(path, { create: true });
+ const opfsSync = await opfsHandle.createSyncAccessHandle();
+ this.fileHandleNo += 1;
+ this.fileByPath.set(path, { handle: this.fileHandleNo, sync: opfsSync });
+ this.fileByHandle.set(this.fileHandleNo, opfsSync);
+ }
+
+ async unregisterFile(path: string) {
+ const file = this.fileByPath.get(path);
+ if (file == null) {
+ return;
+ }
+ this.fileByPath.delete(path);
+ this.fileByHandle.delete(file.handle);
+ file.sync.close();
+ }
+ lookupFileHandle(path: string): number | null {
+ try {
+ const file = this.fileByPath.get(path);
+ if (file == null) {
+ return null;
+ }
+ return file.handle;
+ } catch (e) {
+ console.error('lookupFile', path, e);
+ throw e;
+ }
+ }
+ read(handle: number, buffer: Uint8Array, offset: number): number {
+ try {
+ const file = this.fileByHandle.get(handle);
+ const result = file.read(buffer, { at: Number(offset) });
+ return result;
+ } catch (e) {
+ console.error('read', handle, buffer.length, offset, e);
+ throw e;
+ }
+ }
+ write(handle: number, buffer: Uint8Array, offset: number): number {
+ try {
+ const file = this.fileByHandle.get(handle);
+ const result = file.write(buffer, { at: Number(offset) });
+ return result;
+ } catch (e) {
+ console.error('write', handle, buffer.length, offset, e);
+ throw e;
+ }
+ }
+ sync(handle: number) {
+ try {
+ const file = this.fileByHandle.get(handle);
+ file.flush();
+ } catch (e) {
+ console.error('sync', handle, e);
+ throw e;
+ }
+ }
+ truncate(handle: number, size: number) {
+ try {
+ const file = this.fileByHandle.get(handle);
+ const result = file.truncate(size);
+ return result;
+ } catch (e) {
+ console.error('truncate', handle, size, e);
+ throw e;
+ }
+ }
+ size(handle: number): number {
+ try {
+ const file = this.fileByHandle.get(handle);
+ const size = file.getSize()
+ return size;
+ } catch (e) {
+ console.error('size', handle, e);
+ throw e;
+ }
+ }
+}
+
+var workerRequestId = 0;
+function waitForWorkerResponse(worker: Worker, id: number): Promise {
+ let waitResolve, waitReject;
+ const callback = msg => {
+ if (msg.data.id == id) {
+ if (msg.data.error != null) {
+ waitReject(msg.data.error)
+ } else {
+ waitResolve()
+ }
+ cleanup();
+ }
+ };
+ const cleanup = () => worker.removeEventListener("message", callback);
+
+ worker.addEventListener("message", callback);
+ const result = new Promise((resolve, reject) => {
+ waitResolve = resolve;
+ waitReject = reject;
+ });
+ return result;
+}
+
+function registerFileAtWorker(worker: Worker, path: string): Promise {
+ workerRequestId += 1;
+ const currentId = workerRequestId;
+ const promise = waitForWorkerResponse(worker, currentId);
+ worker.postMessage({ __turso__: "register", path: path, id: currentId });
+ return promise;
+}
+
+function unregisterFileAtWorker(worker: Worker, path: string): Promise {
+ workerRequestId += 1;
+ const currentId = workerRequestId;
+ const promise = waitForWorkerResponse(worker, currentId);
+ worker.postMessage({ __turso__: "unregister", path: path, id: currentId });
+ return promise;
+}
+
+export { OpfsDirectory, workerImports, MainDummyImports, waitForWorkerResponse, registerFileAtWorker, unregisterFileAtWorker }
\ No newline at end of file
diff --git a/bindings/javascript/packages/browser-common/package.json b/bindings/javascript/packages/browser-common/package.json
new file mode 100644
index 000000000..62600252f
--- /dev/null
+++ b/bindings/javascript/packages/browser-common/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@tursodatabase/database-browser-common",
+ "version": "0.2.0-pre.1",
+ "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'"
+ }
+}
diff --git a/bindings/javascript/packages/browser-common/tsconfig.json b/bindings/javascript/packages/browser-common/tsconfig.json
new file mode 100644
index 000000000..410aeee0e
--- /dev/null
+++ b/bindings/javascript/packages/browser-common/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "module": "esnext",
+ "target": "esnext",
+ "outDir": "dist/",
+ "lib": [
+ "es2020",
+ "DOM",
+ "WebWorker"
+ ],
+ },
+ "include": [
+ "*"
+ ]
+}
\ No newline at end of file
diff --git a/bindings/javascript/packages/browser/index.js b/bindings/javascript/packages/browser/index.js
index be8564969..84eb146aa 100644
--- a/bindings/javascript/packages/browser/index.js
+++ b/bindings/javascript/packages/browser/index.js
@@ -5,6 +5,7 @@ import {
WASI as __WASI,
} from '@napi-rs/wasm-runtime'
+import { MainDummyImports } from "@tursodatabase/database-browser-common";
const __wasi = new __WASI({
@@ -25,10 +26,6 @@ const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
export let MainWorker = null;
-function panic(name) {
- throw new Error(`method ${name} must be invoked only from the main thread`);
-}
-
const {
instance: __napiInstance,
module: __wasiModule,
@@ -49,14 +46,8 @@ const {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
+ ...MainDummyImports,
memory: __sharedMemory,
- is_web_worker: () => false,
- lookup_file: () => panic("lookup_file"),
- read: () => panic("read"),
- write: () => panic("write"),
- sync: () => panic("sync"),
- truncate: () => panic("truncate"),
- size: () => panic("size"),
}
return importObject
},
diff --git a/bindings/javascript/packages/browser/package.json b/bindings/javascript/packages/browser/package.json
index 5475fd25b..9c754f391 100644
--- a/bindings/javascript/packages/browser/package.json
+++ b/bindings/javascript/packages/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@tursodatabase/database-browser",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
@@ -40,6 +40,7 @@
},
"dependencies": {
"@napi-rs/wasm-runtime": "^1.0.3",
- "@tursodatabase/database-common": "^0.1.5-pre.5"
+ "@tursodatabase/database-browser-common": "^0.2.0-pre.1",
+ "@tursodatabase/database-common": "^0.2.0-pre.1"
}
}
diff --git a/bindings/javascript/packages/browser/promise.ts b/bindings/javascript/packages/browser/promise.ts
index 8f713f958..e5d0d3c9e 100644
--- a/bindings/javascript/packages/browser/promise.ts
+++ b/bindings/javascript/packages/browser/promise.ts
@@ -1,50 +1,24 @@
-import { DatabasePromise, NativeDatabase, DatabaseOpts, SqliteError } from "@tursodatabase/database-common"
+import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-browser-common"
+import { DatabasePromise, NativeDatabase, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common"
import { connect as nativeConnect, initThreadPool, MainWorker } from "#index";
-let workerRequestId = 0;
class Database extends DatabasePromise {
- files: string[];
- constructor(db: NativeDatabase, files: string[], opts: DatabaseOpts = {}) {
+ path: string | null;
+ constructor(db: NativeDatabase, fsPath: string | null, opts: DatabaseOpts = {}) {
super(db, opts)
- this.files = files;
+ this.path = fsPath;
}
async close() {
- let currentId = workerRequestId;
- workerRequestId += this.files.length;
-
- let tasks = [];
- for (const file of this.files) {
- (MainWorker as any).postMessage({ __turso__: "unregister", path: file, id: currentId });
- tasks.push(waitFor(currentId));
- currentId += 1;
+ if (this.path != null) {
+ await Promise.all([
+ unregisterFileAtWorker(MainWorker, this.path),
+ unregisterFileAtWorker(MainWorker, `${this.path}-wal`)
+ ]);
}
- await Promise.all(tasks);
this.db.close();
}
}
-function waitFor(id: number): Promise {
- let waitResolve, waitReject;
- const callback = msg => {
- if (msg.data.id == id) {
- if (msg.data.error != null) {
- waitReject(msg.data.error)
- } else {
- waitResolve()
- }
- cleanup();
- }
- };
- const cleanup = () => (MainWorker as any).removeEventListener("message", callback);
-
- (MainWorker as any).addEventListener("message", callback);
- const result = new Promise((resolve, reject) => {
- waitResolve = resolve;
- waitReject = reject;
- });
- return result;
-}
-
/**
* Creates a new database connection asynchronously.
*
@@ -55,24 +29,18 @@ function waitFor(id: number): Promise {
async function connect(path: string, opts: DatabaseOpts = {}): Promise {
if (path == ":memory:") {
const db = await nativeConnect(path, { tracing: opts.tracing });
- return new Database(db, [], opts);
+ return new Database(db, null, opts);
}
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not set");
}
-
- let currentId = workerRequestId;
- workerRequestId += 2;
-
- let dbHandlePromise = waitFor(currentId);
- let walHandlePromise = waitFor(currentId + 1);
- (MainWorker as any).postMessage({ __turso__: "register", path: `${path}`, id: currentId });
- (MainWorker as any).postMessage({ __turso__: "register", path: `${path}-wal`, id: currentId + 1 });
- await Promise.all([dbHandlePromise, walHandlePromise]);
+ await Promise.all([
+ registerFileAtWorker(MainWorker, path),
+ registerFileAtWorker(MainWorker, `${path}-wal`)
+ ]);
const db = await nativeConnect(path, { tracing: opts.tracing });
- const files = [path, `${path}-wal`];
- return new Database(db, files, opts);
+ return new Database(db, path, opts);
}
export { connect, Database, SqliteError }
diff --git a/bindings/javascript/packages/browser/tsconfig.json b/bindings/javascript/packages/browser/tsconfig.json
index b46abc167..b380f4fa5 100644
--- a/bindings/javascript/packages/browser/tsconfig.json
+++ b/bindings/javascript/packages/browser/tsconfig.json
@@ -5,9 +5,12 @@
"declarationMap": true,
"module": "nodenext",
"target": "esnext",
+ "moduleResolution": "nodenext",
"outDir": "dist/",
"lib": [
- "es2020"
+ "es2020",
+ "DOM",
+ "WebWorker"
],
"paths": {
"#index": [
diff --git a/bindings/javascript/packages/browser/worker.mjs b/bindings/javascript/packages/browser/worker.mjs
index 9c29d4390..104fd4c19 100644
--- a/bindings/javascript/packages/browser/worker.mjs
+++ b/bindings/javascript/packages/browser/worker.mjs
@@ -1,108 +1,9 @@
import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime'
+import { OpfsDirectory, workerImports } from '@tursodatabase/database-browser-common';
-var fileByPath = new Map();
-var fileByHandle = new Map();
-let fileHandles = 0;
+var opfs = new OpfsDirectory();
var memory = null;
-function getUint8ArrayFromWasm(ptr, len) {
- ptr = ptr >>> 0;
- return new Uint8Array(memory.buffer).subarray(ptr, ptr + len);
-}
-
-
-async function registerFile(path) {
- if (fileByPath.has(path)) {
- return;
- }
- const opfsRoot = await navigator.storage.getDirectory();
- const opfsHandle = await opfsRoot.getFileHandle(path, { create: true });
- const opfsSync = await opfsHandle.createSyncAccessHandle();
- fileHandles += 1;
- fileByPath.set(path, { handle: fileHandles, sync: opfsSync });
- fileByHandle.set(fileHandles, opfsSync);
-}
-
-async function unregisterFile(path) {
- const file = fileByPath.get(path);
- if (file == null) {
- return;
- }
- fileByPath.delete(path);
- fileByHandle.delete(file.handle);
- file.sync.close();
-}
-
-function lookup_file(pathPtr, pathLen) {
- try {
- const buffer = getUint8ArrayFromWasm(pathPtr, pathLen);
- const notShared = new Uint8Array(buffer.length);
- notShared.set(buffer);
- const decoder = new TextDecoder('utf-8');
- const path = decoder.decode(notShared);
- const file = fileByPath.get(path);
- if (file == null) {
- return -404;
- }
- return file.handle;
- } catch (e) {
- console.error('lookupFile', pathPtr, pathLen, e);
- return -1;
- }
-}
-function read(handle, bufferPtr, bufferLen, offset) {
- try {
- const buffer = getUint8ArrayFromWasm(bufferPtr, bufferLen);
- const file = fileByHandle.get(Number(handle));
- const result = file.read(buffer, { at: Number(offset) });
- return result;
- } catch (e) {
- console.error('read', handle, bufferPtr, bufferLen, offset, e);
- return -1;
- }
-}
-function write(handle, bufferPtr, bufferLen, offset) {
- try {
- const buffer = getUint8ArrayFromWasm(bufferPtr, bufferLen);
- const file = fileByHandle.get(Number(handle));
- const result = file.write(buffer, { at: Number(offset) });
- return result;
- } catch (e) {
- console.error('write', handle, bufferPtr, bufferLen, offset, e);
- return -1;
- }
-}
-function sync(handle) {
- try {
- const file = fileByHandle.get(Number(handle));
- file.flush();
- return 0;
- } catch (e) {
- console.error('sync', handle, e);
- return -1;
- }
-}
-function truncate(handle, size) {
- try {
- const file = fileByHandle.get(Number(handle));
- const result = file.truncate(size);
- return result;
- } catch (e) {
- console.error('truncate', handle, size, e);
- return -1;
- }
-}
-function size(handle) {
- try {
- const file = fileByHandle.get(Number(handle));
- const size = file.getSize()
- return size;
- } catch (e) {
- console.error('size', handle, e);
- return -1;
- }
-}
-
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
memory = wasmMemory;
@@ -124,14 +25,8 @@ const handler = new MessageHandler({
...importObject.env,
...importObject.napi,
...importObject.emnapi,
+ ...workerImports(opfs, memory),
memory: wasmMemory,
- is_web_worker: () => true,
- lookup_file: lookup_file,
- read: read,
- write: write,
- sync: sync,
- truncate: truncate,
- size: size,
}
},
})
@@ -141,16 +36,16 @@ const handler = new MessageHandler({
globalThis.onmessage = async function (e) {
if (e.data.__turso__ == 'register') {
try {
- await registerFile(e.data.path)
- self.postMessage({ id: e.data.id })
+ 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 unregisterFile(e.data.path)
- self.postMessage({ id: e.data.id })
+ await opfs.unregisterFile(e.data.path);
+ self.postMessage({ id: e.data.id });
} catch (error) {
self.postMessage({ id: e.data.id, error: error });
}
diff --git a/bindings/javascript/packages/common/package.json b/bindings/javascript/packages/common/package.json
index 4a4af4d3c..3a46a23e9 100644
--- a/bindings/javascript/packages/common/package.json
+++ b/bindings/javascript/packages/common/package.json
@@ -1,6 +1,6 @@
{
"name": "@tursodatabase/database-common",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
diff --git a/bindings/javascript/packages/common/types.ts b/bindings/javascript/packages/common/types.ts
index 2b843bb9f..3a2b075da 100644
--- a/bindings/javascript/packages/common/types.ts
+++ b/bindings/javascript/packages/common/types.ts
@@ -18,7 +18,6 @@ export interface NativeDatabase {
prepare(sql: string): NativeStatement;
- pluck(pluckMode: boolean);
defaultSafeIntegers(toggle: boolean);
totalChanges(): number;
changes(): number;
@@ -32,6 +31,11 @@ export const STEP_ROW = 1;
export const STEP_DONE = 2;
export const STEP_IO = 3;
+export interface TableColumn {
+ name: string,
+ type: string
+}
+
export interface NativeStatement {
stepAsync(): Promise;
stepSync(): number;
@@ -39,7 +43,7 @@ export interface NativeStatement {
pluck(pluckMode: boolean);
safeIntegers(toggle: boolean);
raw(toggle: boolean);
- columns(): string[];
+ columns(): TableColumn[];
row(): any;
reset();
finalize();
diff --git a/bindings/javascript/packages/native/index.d.ts b/bindings/javascript/packages/native/index.d.ts
index 1c510cfdc..8654b88d2 100644
--- a/bindings/javascript/packages/native/index.d.ts
+++ b/bindings/javascript/packages/native/index.d.ts
@@ -11,6 +11,8 @@ export declare class Database {
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
/**
@@ -32,7 +34,7 @@ export declare class Database {
*
* # Returns
*/
- batchAsync(sql: string): Promise
+ batchAsync(sql: string): Promise
/**
* Prepares a statement for execution.
*
@@ -123,7 +125,7 @@ export declare class Statement {
* Step the statement and return result code (executed on the background thread):
* 1 = Row available, 2 = Done, 3 = I/O needed
*/
- stepAsync(): Promise
+ stepAsync(): Promise
/** Get the current row data according to the presentation mode */
row(): unknown
/** Sets the presentation mode to raw. */
@@ -139,7 +141,7 @@ export declare class Statement {
*/
safeIntegers(toggle?: boolean | undefined | null): void
/** Get column information for the statement */
- columns(): unknown[]
+ columns(): Promise
/** Finalizes the statement. */
finalize(): void
}
diff --git a/bindings/javascript/packages/native/index.js b/bindings/javascript/packages/native/index.js
index d69167a1a..2e4dcac5d 100644
--- a/bindings/javascript/packages/native/index.js
+++ b/bindings/javascript/packages/native/index.js
@@ -81,8 +81,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-android-arm64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -97,8 +97,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-android-arm-eabi')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -117,8 +117,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-win32-x64-msvc')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -133,8 +133,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-win32-ia32-msvc')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -149,8 +149,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-win32-arm64-msvc')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -168,8 +168,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-darwin-universal')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -184,8 +184,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-darwin-x64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -200,8 +200,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-darwin-arm64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -220,8 +220,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-freebsd-x64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -236,8 +236,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-freebsd-arm64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -257,8 +257,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-x64-musl')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -273,8 +273,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-x64-gnu')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -291,8 +291,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-arm64-musl')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -307,8 +307,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-arm64-gnu')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -325,8 +325,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-arm-musleabihf')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -341,8 +341,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-arm-gnueabihf')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -359,8 +359,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-riscv64-musl')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -375,8 +375,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-riscv64-gnu')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -392,8 +392,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-ppc64-gnu')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -408,8 +408,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-linux-s390x-gnu')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -428,8 +428,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-openharmony-arm64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -444,8 +444,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-openharmony-x64')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
@@ -460,8 +460,8 @@ function requireNative() {
try {
const binding = require('@tursodatabase/database-openharmony-arm')
const bindingPackageVersion = require('@tursodatabase/database-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.`)
+ if (bindingPackageVersion !== '0.1.5' && 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 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
diff --git a/bindings/javascript/packages/native/package.json b/bindings/javascript/packages/native/package.json
index abd6cfe97..93bc4731e 100644
--- a/bindings/javascript/packages/native/package.json
+++ b/bindings/javascript/packages/native/package.json
@@ -1,6 +1,6 @@
{
"name": "@tursodatabase/database",
- "version": "0.1.5-pre.5",
+ "version": "0.2.0-pre.1",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
@@ -44,7 +44,7 @@
]
},
"dependencies": {
- "@tursodatabase/database-common": "^0.1.5-pre.5"
+ "@tursodatabase/database-common": "^0.2.0-pre.1"
},
"imports": {
"#index": "./index.js"
diff --git a/bindings/javascript/replace.sh b/bindings/javascript/replace.sh
new file mode 100644
index 000000000..323bed2f0
--- /dev/null
+++ b/bindings/javascript/replace.sh
@@ -0,0 +1,11 @@
+sed -i "s/$NAME_FROM/$NAME_TO/g" packages/common/package.json
+sed -i "s/$NAME_FROM/$NAME_TO/g" packages/native/package.json
+sed -i "s/$NAME_FROM/$NAME_TO/g" packages/browser/package.json
+
+sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/common/package.json
+sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/native/package.json
+sed -i "s/$VERSION_FROM/$VERSION_TO/g" packages/browser/package.json
+
+sed -i "s/$NAME_FROM\/database-common/$NAME_TO\/database-common/g" packages/native/promise.ts
+sed -i "s/$NAME_FROM\/database-common/$NAME_TO\/database-common/g" packages/native/compat.ts
+sed -i "s/$NAME_FROM\/database-common/$NAME_TO\/database-common/g" packages/browser/promise.ts
diff --git a/bindings/javascript/src/browser.rs b/bindings/javascript/src/browser.rs
index f9c6bffa9..b2c2047d2 100644
--- a/bindings/javascript/src/browser.rs
+++ b/bindings/javascript/src/browser.rs
@@ -28,7 +28,6 @@ pub fn init_thread_pool() -> napi::Result> {
pub struct ConnectTask {
path: String,
- is_memory: bool,
io: Arc,
}
@@ -70,7 +69,7 @@ impl Task for ConnectTask {
Some(result.db),
self.io.clone(),
result.conn,
- self.is_memory,
+ self.path.clone(),
))
}
}
@@ -88,16 +87,11 @@ pub fn connect(path: String, opts: Option) -> Result>,
io: Arc,
conn: Option>,
- is_memory: bool,
+ path: String,
is_open: Cell,
default_safe_integers: Cell,
}
@@ -186,20 +186,20 @@ impl Database {
.connect()
.map_err(|e| Error::new(Status::GenericFailure, format!("Failed to connect: {e}")))?;
- Ok(Self::create(Some(db), io, conn, is_memory(&path)))
+ Ok(Self::create(Some(db), io, conn, path))
}
pub fn create(
db: Option>,
io: Arc,
conn: Arc,
- is_memory: bool,
+ path: String,
) -> Self {
Database {
_db: db,
io,
conn: Some(conn),
- is_memory,
+ path,
is_open: Cell::new(true),
default_safe_integers: Cell::new(false),
}
@@ -218,7 +218,13 @@ impl Database {
/// Returns whether the database is in memory-only mode.
#[napi(getter)]
pub fn memory(&self) -> bool {
- self.is_memory
+ is_memory(&self.path)
+ }
+
+ /// Returns whether the database is in memory-only mode.
+ #[napi(getter)]
+ pub fn path(&self) -> String {
+ self.path.clone()
}
/// Returns whether the database connection is open.
@@ -246,7 +252,7 @@ impl Database {
/// * `sql` - The SQL statements to execute.
///
/// # Returns
- #[napi]
+ #[napi(ts_return_type = "Promise")]
pub fn batch_async(&self, sql: String) -> Result> {
Ok(AsyncTask::new(DbTask::Batch {
conn: self.conn()?.clone(),
@@ -319,7 +325,7 @@ impl Database {
#[napi]
pub fn close(&mut self) -> Result<()> {
self.is_open.set(false);
- let _ = self._db.take().unwrap();
+ let _ = self._db.take();
let _ = self.conn.take().unwrap();
Ok(())
}
@@ -338,7 +344,7 @@ impl Database {
#[napi]
pub fn io_loop_sync(&self) -> Result<()> {
self.io
- .run_once()
+ .step()
.map_err(|e| Error::new(Status::GenericFailure, format!("IO error: {e}")))?;
Ok(())
}
@@ -482,7 +488,7 @@ impl Statement {
/// Step the statement and return result code (executed on the background thread):
/// 1 = Row available, 2 = Done, 3 = I/O needed
- #[napi]
+ #[napi(ts_return_type = "Promise")]
pub fn step_async(&self) -> Result> {
Ok(AsyncTask::new(DbTask::Step {
stmt: self.stmt.clone(),
@@ -577,7 +583,7 @@ impl Statement {
}
/// Get column information for the statement
- #[napi]
+ #[napi(ts_return_type = "Promise")]
pub fn columns<'env>(&self, env: &'env Env) -> Result> {
let stmt_ref = self.stmt.borrow();
let stmt = stmt_ref
@@ -631,7 +637,7 @@ impl Task for IoLoopTask {
type JsValue = ();
fn compute(&mut self) -> napi::Result {
- self.io.run_once().map_err(|e| {
+ self.io.step().map_err(|e| {
napi::Error::new(napi::Status::GenericFailure, format!("IO error: {e}"))
})?;
Ok(())
diff --git a/sync/javascript/Cargo.toml b/bindings/javascript/sync/Cargo.toml
similarity index 93%
rename from sync/javascript/Cargo.toml
rename to bindings/javascript/sync/Cargo.toml
index e16f64891..029a04fb1 100644
--- a/sync/javascript/Cargo.toml
+++ b/bindings/javascript/sync/Cargo.toml
@@ -21,3 +21,6 @@ tracing-subscriber = "0.3.19"
[build-dependencies]
napi-build = "2.2.3"
+
+[features]
+browser = ["turso_node/browser"]
\ No newline at end of file
diff --git a/sync/javascript/README.md b/bindings/javascript/sync/README.md
similarity index 100%
rename from sync/javascript/README.md
rename to bindings/javascript/sync/README.md
diff --git a/sync/javascript/build.rs b/bindings/javascript/sync/build.rs
similarity index 100%
rename from sync/javascript/build.rs
rename to bindings/javascript/sync/build.rs
diff --git a/bindings/javascript/sync/packages/browser/README.md b/bindings/javascript/sync/packages/browser/README.md
new file mode 100644
index 000000000..e443f495e
--- /dev/null
+++ b/bindings/javascript/sync/packages/browser/README.md
@@ -0,0 +1,124 @@
+
+
Turso Database for JavaScript in Browser
+
+
+
+
+
+
+
+
+
+
+---
+
+## 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)
diff --git a/sync/javascript/turso-sync-js.wasi-browser.js b/bindings/javascript/sync/packages/browser/index.js
similarity index 68%
rename from sync/javascript/turso-sync-js.wasi-browser.js
rename to bindings/javascript/sync/packages/browser/index.js
index 55e6a698d..77e4d6567 100644
--- a/sync/javascript/turso-sync-js.wasi-browser.js
+++ b/bindings/javascript/sync/packages/browser/index.js
@@ -1,17 +1,18 @@
import {
createOnMessage as __wasmCreateOnMessageForFsProxy,
getDefaultContext as __emnapiGetDefaultContext,
- instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync,
+ 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('./turso-sync-js.wasm32-wasi.wasm', import.meta.url).href
+const __wasmUrl = new URL('./sync.wasm32-wasi.wasm', import.meta.url).href
const __emnapiContext = __emnapiGetDefaultContext()
@@ -23,19 +24,21 @@ const __sharedMemory = new WebAssembly.Memory({
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
+export let MainWorker = null;
+
const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
-} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
+} = await __emnapiInstantiateNapiModule(__wasmFile, {
context: __emnapiContext,
- asyncWorkPoolSize: 4,
+ asyncWorkPoolSize: 1,
wasi: __wasi,
onCreateWorker() {
- const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
+ const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
type: 'module',
})
-
+ MainWorker = worker;
return worker
},
overwriteImports(importObject) {
@@ -43,6 +46,7 @@ const {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
+ ...MainDummyImports,
memory: __sharedMemory,
}
return importObject
@@ -58,11 +62,15 @@ const {
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 JsDataPollResult = __napiModule.exports.JsDataPollResult
export const JsProtocolIo = __napiModule.exports.JsProtocolIo
-export const JsProtocolRequestData = __napiModule.exports.JsProtocolRequestData
+export const JsProtocolRequestBytes = __napiModule.exports.JsProtocolRequestBytes
export const SyncEngine = __napiModule.exports.SyncEngine
export const DatabaseChangeTypeJs = __napiModule.exports.DatabaseChangeTypeJs
export const SyncEngineProtocolVersion = __napiModule.exports.SyncEngineProtocolVersion
+
diff --git a/bindings/javascript/sync/packages/browser/package.json b/bindings/javascript/sync/packages/browser/package.json
new file mode 100644
index 000000000..655b73666
--- /dev/null
+++ b/bindings/javascript/sync/packages/browser/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "@tursodatabase/sync-browser",
+ "version": "0.2.0-pre.1",
+ "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/database-browser-common": "^0.2.0-pre.1",
+ "@tursodatabase/database-common": "^0.2.0-pre.1",
+ "@tursodatabase/sync-common": "^0.2.0-pre.1"
+ }
+}
diff --git a/bindings/javascript/sync/packages/browser/promise.test.ts b/bindings/javascript/sync/packages/browser/promise.test.ts
new file mode 100644
index 000000000..b602ce698
--- /dev/null
+++ b/bindings/javascript/sync/packages/browser/promise.test.ts
@@ -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 }]);
+})
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/browser/promise.ts b/bindings/javascript/sync/packages/browser/promise.ts
new file mode 100644
index 000000000..8c28ad057
--- /dev/null
+++ b/bindings/javascript/sync/packages/browser/promise.ts
@@ -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 {
+ const result = localStorage.getItem(path);
+ if (result == null) {
+ return null;
+ }
+ return new TextEncoder().encode(result);
+ },
+ async write(path: string, data: Buffer | Uint8Array): Promise {
+ 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 {
+ 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} - A promise that resolves to a Database instance.
+ */
+async function connect(opts: SyncOpts): Promise {
+ 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 }
diff --git a/sync/javascript/tsconfig.json b/bindings/javascript/sync/packages/browser/tsconfig.json
similarity index 79%
rename from sync/javascript/tsconfig.json
rename to bindings/javascript/sync/packages/browser/tsconfig.json
index ad8b905ec..b380f4fa5 100644
--- a/sync/javascript/tsconfig.json
+++ b/bindings/javascript/sync/packages/browser/tsconfig.json
@@ -1,17 +1,19 @@
{
"compilerOptions": {
"skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
"module": "nodenext",
"target": "esnext",
"moduleResolution": "nodenext",
- "declaration": true,
"outDir": "dist/",
"lib": [
"es2020",
- "dom",
+ "DOM",
+ "WebWorker"
],
"paths": {
- "#entry-point": [
+ "#index": [
"./index.js"
]
}
diff --git a/bindings/javascript/sync/packages/browser/vitest.config.ts b/bindings/javascript/sync/packages/browser/vitest.config.ts
new file mode 100644
index 000000000..deeaec485
--- /dev/null
+++ b/bindings/javascript/sync/packages/browser/vitest.config.ts
@@ -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' }
+ ],
+ },
+ },
+})
diff --git a/sync/javascript/wasi-worker-browser.mjs b/bindings/javascript/sync/packages/browser/worker.mjs
similarity index 50%
rename from sync/javascript/wasi-worker-browser.mjs
rename to bindings/javascript/sync/packages/browser/worker.mjs
index 8b1b17221..38c377f83 100644
--- a/sync/javascript/wasi-worker-browser.mjs
+++ b/bindings/javascript/sync/packages/browser/worker.mjs
@@ -1,13 +1,18 @@
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() {
+ printErr: function () {
// eslint-disable-next-line no-console
console.error.apply(console, arguments)
},
@@ -20,6 +25,7 @@ const handler = new MessageHandler({
...importObject.env,
...importObject.napi,
...importObject.emnapi,
+ ...workerImports(opfs, memory),
memory: wasmMemory,
}
},
@@ -27,6 +33,23 @@ const handler = new MessageHandler({
},
})
-globalThis.onmessage = function (e) {
+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)
}
diff --git a/bindings/javascript/sync/packages/common/README.md b/bindings/javascript/sync/packages/common/README.md
new file mode 100644
index 000000000..f9327f368
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/README.md
@@ -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.
+
diff --git a/bindings/javascript/sync/packages/common/index.ts b/bindings/javascript/sync/packages/common/index.ts
new file mode 100644
index 000000000..1b264c80b
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/index.ts
@@ -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 }
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/common/package.json b/bindings/javascript/sync/packages/common/package.json
new file mode 100644
index 000000000..39a998f78
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@tursodatabase/sync-common",
+ "version": "0.2.0-pre.1",
+ "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'"
+ }
+}
diff --git a/bindings/javascript/sync/packages/common/run.ts b/bindings/javascript/sync/packages/common/run.ts
new file mode 100644
index 000000000..f26333d4b
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/run.ts
@@ -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 {
+ promise: Promise,
+ finished: boolean
+}
+
+function trackPromise(p: Promise): TrackPromise {
+ let status = { promise: null, finished: false };
+ status.promise = p.finally(() => status.finished = true);
+ return status;
+}
+
+function timeoutMs(ms: number): Promise {
+ 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 {
+ return values.get(path);
+ },
+ async write(path: string, data: Buffer | Uint8Array): Promise {
+ values.set(path, data);
+ }
+ }
+};
+
+
+export async function run(opts: RunOpts, io: ProtocolIo, engine: any, generator: any): Promise {
+ 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();
+}
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/common/tsconfig.json b/bindings/javascript/sync/packages/common/tsconfig.json
new file mode 100644
index 000000000..9bc14edd3
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "module": "esnext",
+ "target": "esnext",
+ "outDir": "dist/",
+ "lib": [
+ "es2020",
+ "dom"
+ ],
+ },
+ "include": [
+ "*"
+ ]
+}
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/common/types.ts b/bindings/javascript/sync/packages/common/types.ts
new file mode 100644
index 000000000..25fa1e47e
--- /dev/null
+++ b/bindings/javascript/sync/packages/common/types.ts
@@ -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
+ after?: Record
+ updates?: Record
+}
+
+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;
+ write(path: string, content: Buffer | Uint8Array): Promise;
+}
+
+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
+}
+
+export type GeneratorResponse =
+ | { type: 'IO' }
+ | { type: 'Done' }
+ | { type: 'SyncEngineStats', operations: number, mainWal: number, revertWal: number, lastPullUnixTime: number, lastPushUnixTime: number | null }
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/native/README.md b/bindings/javascript/sync/packages/native/README.md
new file mode 100644
index 000000000..d5444435c
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/README.md
@@ -0,0 +1,125 @@
+
+
Turso Database for JavaScript in Node
+
+
+
+
+
+
+
+
+
+
+---
+
+## 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)
diff --git a/sync/javascript/index.d.ts b/bindings/javascript/sync/packages/native/index.d.ts
similarity index 74%
rename from sync/javascript/index.d.ts
rename to bindings/javascript/sync/packages/native/index.d.ts
index 62d3b18ae..3ff5f0390 100644
--- a/sync/javascript/index.d.ts
+++ b/bindings/javascript/sync/packages/native/index.d.ts
@@ -8,13 +8,15 @@ export declare class Database {
* # Arguments
* * `path` - The path to the database file.
*/
- constructor(path: string)
+ 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.
+ * Executes a batch of SQL statements on main thread
*
* # Arguments
*
@@ -22,7 +24,17 @@ export declare class Database {
*
* # Returns
*/
- batch(sql: string): void
+ 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
/**
* Prepares a statement for execution.
*
@@ -105,10 +117,15 @@ export declare class Statement {
*/
bindAt(index: number, value: unknown): void
/**
- * Step the statement and return result code:
+ * Step the statement and return result code (executed on the main thread):
* 1 = Row available, 2 = Done, 3 = I/O needed
*/
- step(): number
+ 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
/** Get the current row data according to the presentation mode */
row(): unknown
/** Sets the presentation mode to raw. */
@@ -124,31 +141,32 @@ export declare class Statement {
*/
safeIntegers(toggle?: boolean | undefined | null): void
/** Get column information for the statement */
- columns(): unknown[]
+ columns(): Promise
/** Finalizes the statement. */
finalize(): void
}
+
+export interface DatabaseOpts {
+ tracing?: string
+}
export declare class GeneratorHolder {
- resume(error?: string | undefined | null): number
- take(): GeneratorResponse | null
+ resumeSync(error?: string | undefined | null): GeneratorResponse
+ resumeAsync(error?: string | undefined | null): Promise
}
export declare class JsDataCompletion {
poison(err: string): void
status(value: number): void
- push(value: Buffer): void
+ pushBuffer(value: Buffer): void
+ pushTransform(values: Array): void
done(): void
}
-export declare class JsDataPollResult {
-
-}
-
export declare class JsProtocolIo {
- takeRequest(): JsProtocolRequestData | null
+ takeRequest(): JsProtocolRequestBytes | null
}
-export declare class JsProtocolRequestData {
+export declare class JsProtocolRequestBytes {
request(): JsProtocolRequest
completion(): JsDataCompletion
}
@@ -159,13 +177,14 @@ export declare class SyncEngine {
ioLoopSync(): void
/** Runs the I/O loop asynchronously, returning a Promise. */
ioLoopAsync(): Promise
- protocolIo(): JsProtocolRequestData | null
+ protocolIo(): JsProtocolRequestBytes | null
sync(): GeneratorHolder
push(): GeneratorHolder
stats(): GeneratorHolder
pull(): GeneratorHolder
checkpoint(): GeneratorHolder
open(): Database
+ close(): void
}
export declare const enum DatabaseChangeTypeJs {
@@ -193,21 +212,29 @@ export interface DatabaseRowStatementJs {
values: Array
}
+export type DatabaseRowTransformResultJs =
+ | { type: 'Keep' }
+ | { type: 'Skip' }
+ | { type: 'Rewrite', stmt: DatabaseRowStatementJs }
+
export type GeneratorResponse =
- | { type: 'SyncEngineStats', operations: number, wal: number }
+ | { 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, headers: Array<[string, string]> }
| { type: 'FullRead', path: string }
| { type: 'FullWrite', path: string, content: Array }
+ | { type: 'Transform', mutations: Array }
export interface SyncEngineOpts {
path: string
clientName?: string
walPullBatchSize?: number
- enableTracing?: string
+ tracing?: string
tablesIgnore?: Array
- transform?: (arg: DatabaseRowMutationJs) => DatabaseRowStatementJs | null
+ useTransform: boolean
protocolVersion?: SyncEngineProtocolVersion
}
diff --git a/bindings/javascript/sync/packages/native/index.js b/bindings/javascript/sync/packages/native/index.js
new file mode 100644
index 000000000..ea974fe7e
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/index.js
@@ -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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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' && 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 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 }
diff --git a/bindings/javascript/sync/packages/native/package.json b/bindings/javascript/sync/packages/native/package.json
new file mode 100644
index 000000000..007f9ccfb
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@tursodatabase/sync",
+ "version": "0.2.0-pre.1",
+ "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": "VITE_TURSO_DB_URL=http://b--a--a.localhost:10000 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.2.0-pre.1",
+ "@tursodatabase/sync-common": "^0.2.0-pre.1"
+ },
+ "imports": {
+ "#index": "./index.js"
+ }
+}
diff --git a/bindings/javascript/sync/packages/native/promise.test.ts b/bindings/javascript/sync/packages/native/promise.test.ts
new file mode 100644
index 000000000..ec8381190
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/promise.test.ts
@@ -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 }]);
+})
\ No newline at end of file
diff --git a/bindings/javascript/sync/packages/native/promise.ts b/bindings/javascript/sync/packages/native/promise.ts
new file mode 100644
index 000000000..86f020109
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/promise.ts
@@ -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 {
+ try {
+ return await promises.readFile(path);
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return null;
+ }
+ throw error;
+ }
+ },
+ async write(path: string, data: Buffer | Uint8Array): Promise {
+ 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 {
+ return values.get(path);
+ },
+ async write(path: string, data: Buffer | Uint8Array): Promise {
+ 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 {
+ 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} - A promise that resolves to a Database instance.
+ */
+async function connect(opts: SyncOpts): Promise {
+ 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 }
diff --git a/bindings/javascript/sync/packages/native/tsconfig.json b/bindings/javascript/sync/packages/native/tsconfig.json
new file mode 100644
index 000000000..e40dd870e
--- /dev/null
+++ b/bindings/javascript/sync/packages/native/tsconfig.json
@@ -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": [
+ "*"
+ ]
+}
\ No newline at end of file
diff --git a/bindings/javascript/sync/src/generator.rs b/bindings/javascript/sync/src/generator.rs
new file mode 100644
index 000000000..141dec016
--- /dev/null
+++ b/bindings/javascript/sync/src/generator.rs
@@ -0,0 +1,102 @@
+use napi::{bindgen_prelude::AsyncTask, Env, Task};
+use napi_derive::napi;
+use std::{
+ future::Future,
+ sync::{Arc, Mutex},
+};
+
+use turso_sync_engine::types::ProtocolCommand;
+
+pub const GENERATOR_RESUME_IO: u32 = 0;
+pub const GENERATOR_RESUME_DONE: u32 = 1;
+
+pub trait Generator {
+ fn resume(&mut self, result: Option) -> napi::Result;
+}
+
+impl>> Generator
+ for genawaiter::sync::Gen, F>
+{
+ fn resume(&mut self, error: Option) -> napi::Result {
+ let result = match error {
+ Some(err) => Err(turso_sync_engine::errors::Error::DatabaseSyncEngineError(
+ format!("JsProtocolIo error: {err}"),
+ )),
+ None => Ok(()),
+ };
+ match self.resume_with(result) {
+ genawaiter::GeneratorState::Yielded(ProtocolCommand::IO) => Ok(GeneratorResponse::IO),
+ genawaiter::GeneratorState::Complete(Ok(())) => Ok(GeneratorResponse::Done),
+ genawaiter::GeneratorState::Complete(Err(err)) => Err(napi::Error::new(
+ napi::Status::GenericFailure,
+ format!("sync engine operation failed: {err}"),
+ )),
+ }
+ }
+}
+
+#[napi(discriminant = "type")]
+pub enum GeneratorResponse {
+ IO,
+ Done,
+ SyncEngineStats {
+ operations: i64,
+ main_wal: i64,
+ revert_wal: i64,
+ last_pull_unix_time: i64,
+ last_push_unix_time: Option,
+ },
+}
+
+#[napi]
+#[derive(Clone)]
+pub struct GeneratorHolder {
+ pub(crate) generator: Arc>,
+ pub(crate) response: Arc>>,
+}
+
+pub struct ResumeTask {
+ holder: GeneratorHolder,
+ error: Option,
+}
+
+unsafe impl Send for ResumeTask {}
+
+impl Task for ResumeTask {
+ type Output = GeneratorResponse;
+ type JsValue = GeneratorResponse;
+
+ fn compute(&mut self) -> napi::Result {
+ resume_sync(&self.holder, self.error.take())
+ }
+
+ fn resolve(&mut self, _: Env, output: Self::Output) -> napi::Result {
+ Ok(output)
+ }
+}
+
+fn resume_sync(holder: &GeneratorHolder, error: Option) -> napi::Result {
+ let result = holder.generator.lock().unwrap().resume(error)?;
+ if let GeneratorResponse::Done = result {
+ let response = holder.response.lock().unwrap().take();
+ Ok(response.unwrap_or(GeneratorResponse::Done))
+ } else {
+ Ok(result)
+ }
+}
+
+#[napi]
+impl GeneratorHolder {
+ #[napi]
+ pub fn resume_sync(&self, error: Option) -> napi::Result {
+ resume_sync(self, error)
+ }
+
+ #[napi]
+ pub fn resume_async(&self, error: Option) -> napi::Result> {
+ Ok(AsyncTask::new(ResumeTask {
+ holder: self.clone(),
+ error,
+ }))
+ }
+}
diff --git a/sync/javascript/src/js_protocol_io.rs b/bindings/javascript/sync/src/js_protocol_io.rs
similarity index 53%
rename from sync/javascript/src/js_protocol_io.rs
rename to bindings/javascript/sync/src/js_protocol_io.rs
index 429c85f7f..9208e4636 100644
--- a/sync/javascript/src/js_protocol_io.rs
+++ b/bindings/javascript/sync/src/js_protocol_io.rs
@@ -7,7 +7,15 @@ use std::{
use napi::bindgen_prelude::*;
use napi_derive::napi;
-use turso_sync_engine::protocol_io::{DataCompletion, DataPollResult, ProtocolIO};
+use turso_sync_engine::{
+ protocol_io::{DataCompletion, DataPollResult, ProtocolIO},
+ types::{DatabaseRowTransformResult, DatabaseStatementReplay},
+};
+
+use crate::{
+ core_change_type_to_js, core_values_map_to_js, js_value_to_core, DatabaseRowMutationJs,
+ DatabaseRowTransformResultJs,
+};
#[napi]
pub enum JsProtocolRequest {
@@ -24,15 +32,34 @@ pub enum JsProtocolRequest {
path: String,
content: Vec,
},
+ Transform {
+ mutations: Vec,
+ },
}
#[derive(Clone)]
#[napi]
pub struct JsDataCompletion(Arc>);
+pub struct JsBytesPollResult(Buffer);
+
+impl DataPollResult for JsBytesPollResult {
+ fn data(&self) -> &[u8] {
+ &self.0
+ }
+}
+pub struct JsTransformPollResult(Vec);
+
+impl DataPollResult for JsTransformPollResult {
+ fn data(&self) -> &[DatabaseRowTransformResult] {
+ &self.0
+ }
+}
+
struct JsDataCompletionInner {
status: Option,
chunks: VecDeque,
+ transformed: VecDeque,
finished: bool,
err: Option,
}
@@ -49,8 +76,8 @@ impl JsDataCompletion {
}
}
-impl DataCompletion for JsDataCompletion {
- type DataPollResult = JsDataPollResult;
+impl DataCompletion for JsDataCompletion {
+ type DataPollResult = JsBytesPollResult;
fn status(&self) -> turso_sync_engine::Result