diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 821858b52d..76894e0b7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2935,7 +2935,7 @@ importers: version: 0.31.5 drizzle-orm: specifier: ^0.44.2 - version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2) + version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(sql.js@1.13.0) devDependencies: '@types/node': specifier: ^22.13.9 @@ -4116,7 +4116,7 @@ importers: version: 24.7.1 drizzle-orm: specifier: ^0.44.2 - version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2) + version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(sql.js@1.13.0) tsup: specifier: ^8.3.6 version: 8.5.0(@microsoft/api-extractor@7.53.2(@types/node@24.7.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.2)(yaml@2.8.2) @@ -4316,7 +4316,7 @@ importers: version: 0.31.5 drizzle-orm: specifier: ^0.44.2 - version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2) + version: 0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(sql.js@1.13.0) get-port: specifier: ^7.1.0 version: 7.1.0 @@ -4420,12 +4420,12 @@ importers: '@rivetkit/bare-ts': specifier: ^0.6.2 version: 0.6.2 + sql.js: + specifier: ^1.10.2 + version: 1.13.0 vbare: specifier: ^0.0.4 version: 0.0.4 - wa-sqlite: - specifier: ^1.0.0 - version: 1.0.0 devDependencies: '@bare-ts/tools': specifier: ^0.13.0 @@ -4433,6 +4433,9 @@ importers: '@types/node': specifier: ^22.13.1 version: 22.19.5 + '@types/sql.js': + specifier: ^1.4.9 + version: 1.4.9 commander: specifier: ^12.0.0 version: 12.1.0 @@ -4448,6 +4451,51 @@ importers: vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.5)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + optionalDependencies: + '@rivetkit/sqlite-vfs-darwin-arm64': + specifier: workspace:* + version: link:../sqlite-vfs-darwin-arm64 + '@rivetkit/sqlite-vfs-darwin-x64': + specifier: workspace:* + version: link:../sqlite-vfs-darwin-x64 + '@rivetkit/sqlite-vfs-linux-arm64': + specifier: workspace:* + version: link:../sqlite-vfs-linux-arm64 + '@rivetkit/sqlite-vfs-linux-x64': + specifier: workspace:* + version: link:../sqlite-vfs-linux-x64 + '@rivetkit/sqlite-vfs-win32-x64': + specifier: workspace:* + version: link:../sqlite-vfs-win32-x64 + + rivetkit-typescript/packages/sqlite-vfs-darwin-arm64: {} + + rivetkit-typescript/packages/sqlite-vfs-darwin-x64: {} + + rivetkit-typescript/packages/sqlite-vfs-linux-arm64: {} + + rivetkit-typescript/packages/sqlite-vfs-linux-x64: {} + + rivetkit-typescript/packages/sqlite-vfs-test: + dependencies: + '@rivetkit/sqlite-vfs': + specifier: workspace:* + version: link:../sqlite-vfs + devDependencies: + '@types/node': + specifier: ^22.13.1 + version: 22.19.5 + tsx: + specifier: ^4.7.0 + version: 4.20.6 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.5)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + + rivetkit-typescript/packages/sqlite-vfs-win32-x64: {} rivetkit-typescript/packages/traces: dependencies: @@ -4866,7 +4914,7 @@ importers: version: 19.2.2 drizzle-orm: specifier: ^0.38.0 - version: 0.38.4(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/react@19.2.2)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(react@19.1.0) + version: 0.38.4(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/react@19.2.2)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(react@19.1.0)(sql.js@1.13.0) typescript: specifier: ^5.7.3 version: 5.9.3 @@ -9751,6 +9799,9 @@ packages: '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + '@types/escape-html@1.0.4': resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==} @@ -9892,6 +9943,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/sql.js@1.4.9': + resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -15793,6 +15847,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sql.js@1.13.0: + resolution: {integrity: sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==} + srvx@0.10.0: resolution: {integrity: sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA==} engines: {node: '>=20.16.0'} @@ -17032,9 +17089,6 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - wa-sqlite@1.0.0: - resolution: {integrity: sha512-Kyybo5/BaJp76z7gDWGk2J6Hthl4NIPsE+swgraEjy3IY6r5zIR02wAs1OJH4XtJp1y3puj3Onp5eMGS0z7nUA==} - walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -23021,6 +23075,8 @@ snapshots: '@types/diff-match-patch@1.0.36': {} + '@types/emscripten@1.41.5': {} + '@types/escape-html@1.0.4': {} '@types/eslint-scope@3.7.7': @@ -23178,6 +23234,11 @@ snapshots: '@types/semver@7.7.1': {} + '@types/sql.js@1.4.9': + dependencies: + '@types/emscripten': 1.41.5 + '@types/node': 22.19.5 + '@types/stack-utils@2.0.3': {} '@types/trusted-types@2.0.7': {} @@ -25295,29 +25356,33 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/react@19.2.2)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(react@19.1.0): + drizzle-orm@0.38.4(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/react@19.2.2)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(react@19.1.0)(sql.js@1.13.0): optionalDependencies: '@cloudflare/workers-types': 4.20251014.0 '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.16.0 '@types/react': 19.2.2 + '@types/sql.js': 1.4.9 better-sqlite3: 11.10.0 bun-types: 1.3.0(@types/react@19.2.2) kysely: 0.28.8 pg: 8.17.2 react: 19.1.0 + sql.js: 1.13.0 - drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2): + drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@11.10.0)(bun-types@1.3.0(@types/react@19.2.2))(kysely@0.28.8)(pg@8.17.2)(sql.js@1.13.0): optionalDependencies: '@cloudflare/workers-types': 4.20251014.0 '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.13 '@types/pg': 8.16.0 + '@types/sql.js': 1.4.9 better-sqlite3: 11.10.0 bun-types: 1.3.0(@types/react@19.2.2) kysely: 0.28.8 pg: 8.17.2 + sql.js: 1.13.0 dset@3.1.4: {} @@ -30536,6 +30601,8 @@ snapshots: sprintf-js@1.0.3: {} + sql.js@1.13.0: {} + srvx@0.10.0: {} stack-utils@2.0.6: @@ -32696,8 +32763,6 @@ snapshots: w3c-keyname@2.2.8: {} - wa-sqlite@1.0.0: {} - walker@1.0.8: dependencies: makeerror: 1.0.12 diff --git a/rivetkit-rust/Cargo.lock b/rivetkit-rust/Cargo.lock new file mode 100644 index 0000000000..2ffbb347dd --- /dev/null +++ b/rivetkit-rust/Cargo.lock @@ -0,0 +1,2450 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "eventsource-client" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee53bc484c1ff2390bbf8744b4f8c405fccadb38700de6ed43c8f22b8da9280c" +dependencies = [ + "base64 0.22.1", + "futures", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "hyper-timeout", + "log", + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.36", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.32", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rivetkit-client" +version = "0.9.0-rc.2" +dependencies = [ + "anyhow", + "base64 0.22.1", + "eventsource-client", + "fs_extra", + "futures-util", + "portpicker", + "reqwest", + "serde", + "serde_cbor", + "serde_json", + "tempfile", + "tokio", + "tokio-test", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "tungstenite", + "urlencoding", +] + +[[package]] +name = "rivetkit-sqlite-vfs-core" +version = "0.0.1" + +[[package]] +name = "rivetkit-sqlite-vfs-native" +version = "0.0.1" +dependencies = [ + "libsqlite3-sys", + "napi", + "napi-build", + "napi-derive", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "rivetkit-sqlite-vfs-wasm" +version = "0.0.1" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/rivetkit-rust/Cargo.toml b/rivetkit-rust/Cargo.toml new file mode 100644 index 0000000000..1460006afc --- /dev/null +++ b/rivetkit-rust/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +resolver = "2" +members = [ + "packages/client", + "packages/sqlite-vfs-core", + "packages/sqlite-vfs-native", + "packages/sqlite-vfs-wasm" +] + +[workspace.package] +edition = "2021" +license = "Apache-2.0" diff --git a/rivetkit-rust/packages/sqlite-vfs-core/Cargo.toml b/rivetkit-rust/packages/sqlite-vfs-core/Cargo.toml new file mode 100644 index 0000000000..0c6842e046 --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rivetkit-sqlite-vfs-core" +version = "0.0.1" +description = "Core types and key encoding for RivetKit SQLite VFS" +edition = "2021" +license = "Apache-2.0" + +[dependencies] diff --git a/rivetkit-rust/packages/sqlite-vfs-core/src/lib.rs b/rivetkit-rust/packages/sqlite-vfs-core/src/lib.rs new file mode 100644 index 0000000000..244f7a5b4f --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-core/src/lib.rs @@ -0,0 +1,24 @@ +pub const DEFAULT_CHUNK_SIZE: usize = 4096; +pub const SQLITE_PREFIX: u8 = 9; +pub const META_PREFIX: u8 = 0; +pub const CHUNK_PREFIX: u8 = 1; + +pub fn meta_key(file_name: &str) -> Vec { + let file_name_bytes = file_name.as_bytes(); + let mut key = Vec::with_capacity(2 + file_name_bytes.len()); + key.push(SQLITE_PREFIX); + key.push(META_PREFIX); + key.extend_from_slice(file_name_bytes); + key +} + +pub fn chunk_key(file_name: &str, chunk_index: u32) -> Vec { + let file_name_bytes = file_name.as_bytes(); + let mut key = Vec::with_capacity(2 + file_name_bytes.len() + 1 + 4); + key.push(SQLITE_PREFIX); + key.push(CHUNK_PREFIX); + key.extend_from_slice(file_name_bytes); + key.push(0); + key.extend_from_slice(&chunk_index.to_be_bytes()); + key +} diff --git a/rivetkit-rust/packages/sqlite-vfs-native/Cargo.toml b/rivetkit-rust/packages/sqlite-vfs-native/Cargo.toml new file mode 100644 index 0000000000..ff0c8611df --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-native/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rivetkit-sqlite-vfs-native" +version = "0.0.1" +description = "Native sqlite-vfs addon for RivetKit" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +libsqlite3-sys = { version = "0.28.0", features = ["bundled"] } +napi = { version = "2.16.12", features = ["napi6"] } +napi-derive = "2.16.12" +tempfile = "3.13.0" +thiserror = "1.0.64" + +[build-dependencies] +napi-build = "2.1.4" diff --git a/rivetkit-rust/packages/sqlite-vfs-native/build.rs b/rivetkit-rust/packages/sqlite-vfs-native/build.rs new file mode 100644 index 0000000000..bed4fb7f63 --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-native/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/rivetkit-rust/packages/sqlite-vfs-native/src/lib.rs b/rivetkit-rust/packages/sqlite-vfs-native/src/lib.rs new file mode 100644 index 0000000000..b07c50a856 --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-native/src/lib.rs @@ -0,0 +1,269 @@ +use std::ffi::{CStr, CString}; +use std::path::PathBuf; +use std::ptr; + +use libsqlite3_sys as sqlite; +use napi::bindgen_prelude::Buffer; +use napi::{Env, Error, JsFunction, JsObject, Result, Status}; +use napi_derive::napi; +use tempfile::TempDir; + +const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; +const MIN_SAFE_INTEGER: i64 = -9_007_199_254_740_991; + +#[napi] +pub struct NativeDatabase { + db: *mut sqlite::sqlite3, + _temp_dir: TempDir, + path: PathBuf, + closed: bool, +} + +unsafe impl Send for NativeDatabase {} + +impl Drop for NativeDatabase { + fn drop(&mut self) { + let _ = self.close_internal(); + } +} + +#[napi] +impl NativeDatabase { + #[napi(constructor)] + pub fn new(bytes: Option) -> Result { + let temp_dir = TempDir::new().map_err(|err| { + Error::new(Status::GenericFailure, format!("temp dir error: {err}")) + })?; + let path = temp_dir.path().join("db.sqlite"); + if let Some(data) = bytes { + std::fs::write(&path, &data).map_err(|err| { + Error::new(Status::GenericFailure, format!("write db error: {err}")) + })?; + } + + let mut db = ptr::null_mut(); + let c_path = CString::new(path.to_string_lossy().as_bytes()).map_err(|_| { + Error::new(Status::InvalidArg, "invalid database path") + })?; + + let flags = sqlite::SQLITE_OPEN_READWRITE | sqlite::SQLITE_OPEN_CREATE; + let rc = unsafe { sqlite::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags, ptr::null()) }; + if rc != sqlite::SQLITE_OK { + return Err(sqlite_error(db, "sqlite3_open_v2")); + } + + exec_pragma(db, "PRAGMA journal_mode=DELETE;")?; + exec_pragma(db, "PRAGMA synchronous=FULL;")?; + + Ok(Self { + db, + _temp_dir: temp_dir, + path, + closed: false, + }) + } + + #[napi] + pub fn exec(&mut self, env: Env, sql: String, callback: Option) -> Result<()> { + if self.db.is_null() || self.closed { + return Err(Error::new(Status::GenericFailure, "database is closed")); + } + + let c_sql = CString::new(sql).map_err(|_| { + Error::new(Status::InvalidArg, "sql contains null byte") + })?; + let mut tail = c_sql.as_ptr(); + + loop { + let mut stmt: *mut sqlite::sqlite3_stmt = ptr::null_mut(); + let rc = unsafe { sqlite::sqlite3_prepare_v2(self.db, tail, -1, &mut stmt, &mut tail) }; + if rc != sqlite::SQLITE_OK { + if !stmt.is_null() { + unsafe { + sqlite::sqlite3_finalize(stmt); + } + } + return Err(sqlite_error(self.db, "sqlite3_prepare_v2")); + } + + if stmt.is_null() { + if unsafe { *tail } == 0 { + break; + } + continue; + } + + let column_count = unsafe { sqlite::sqlite3_column_count(stmt) }; + let column_names = collect_column_names(stmt, column_count); + + loop { + let step_rc = unsafe { sqlite::sqlite3_step(stmt) }; + if step_rc == sqlite::SQLITE_ROW { + if let Some(cb) = &callback { + let row = build_row(&env, stmt, column_count)?; + let columns = build_columns(&env, &column_names)?; + cb.call(None, &[row.into_unknown(), columns.into_unknown()])?; + } + } else if step_rc == sqlite::SQLITE_DONE { + break; + } else { + unsafe { + sqlite::sqlite3_finalize(stmt); + } + return Err(sqlite_error(self.db, "sqlite3_step")); + } + } + + unsafe { + sqlite::sqlite3_finalize(stmt); + } + + if unsafe { *tail } == 0 { + break; + } + } + + Ok(()) + } + + #[napi] + pub fn export(&self) -> Result { + if self.db.is_null() || self.closed { + return Err(Error::new(Status::GenericFailure, "database is closed")); + } + let bytes = std::fs::read(&self.path).map_err(|err| { + Error::new(Status::GenericFailure, format!("read db error: {err}")) + })?; + Ok(Buffer::from(bytes)) + } + + #[napi] + pub fn close(&mut self) -> Result<()> { + self.close_internal() + } +} + +impl NativeDatabase { + fn close_internal(&mut self) -> Result<()> { + if self.closed || self.db.is_null() { + return Ok(()); + } + + let rc = unsafe { sqlite::sqlite3_close(self.db) }; + if rc != sqlite::SQLITE_OK { + return Err(sqlite_error(self.db, "sqlite3_close")); + } + self.db = ptr::null_mut(); + self.closed = true; + Ok(()) + } +} + +fn sqlite_error(db: *mut sqlite::sqlite3, context: &str) -> Error { + if db.is_null() { + return Error::new(Status::GenericFailure, format!("{context}: sqlite error")); + } + unsafe { + let msg = sqlite::sqlite3_errmsg(db); + if msg.is_null() { + return Error::new(Status::GenericFailure, format!("{context}: sqlite error")); + } + let c_str = CStr::from_ptr(msg); + Error::new( + Status::GenericFailure, + format!("{context}: {}", c_str.to_string_lossy()), + ) + } +} + +fn exec_pragma(db: *mut sqlite::sqlite3, sql: &str) -> Result<()> { + let c_sql = CString::new(sql).map_err(|_| { + Error::new(Status::InvalidArg, "pragma contains null byte") + })?; + let mut err_msg: *mut i8 = ptr::null_mut(); + let rc = unsafe { sqlite::sqlite3_exec(db, c_sql.as_ptr(), None, ptr::null_mut(), &mut err_msg) }; + if rc != sqlite::SQLITE_OK { + if !err_msg.is_null() { + let msg = unsafe { CStr::from_ptr(err_msg) }.to_string_lossy().to_string(); + unsafe { + sqlite::sqlite3_free(err_msg as *mut _); + } + return Err(Error::new(Status::GenericFailure, msg)); + } + return Err(sqlite_error(db, "sqlite3_exec")); + } + Ok(()) +} + +fn collect_column_names(stmt: *mut sqlite::sqlite3_stmt, count: i32) -> Vec { + let mut names = Vec::with_capacity(count as usize); + for i in 0..count { + let name_ptr = unsafe { sqlite::sqlite3_column_name(stmt, i) }; + let name = if name_ptr.is_null() { + String::new() + } else { + unsafe { CStr::from_ptr(name_ptr) }.to_string_lossy().to_string() + }; + names.push(name); + } + names +} + +fn build_columns(env: &Env, names: &[String]) -> Result { + let mut columns = env.create_array_with_length(names.len())?; + for (index, name) in names.iter().enumerate() { + let js_name = env.create_string(name)?; + columns.set_element(index as u32, js_name)?; + } + Ok(columns) +} + +fn build_row(env: &Env, stmt: *mut sqlite::sqlite3_stmt, count: i32) -> Result { + let mut row = env.create_array_with_length(count as usize)?; + for i in 0..count { + let value = match unsafe { sqlite::sqlite3_column_type(stmt, i) } { + sqlite::SQLITE_INTEGER => { + let v = unsafe { sqlite::sqlite3_column_int64(stmt, i) }; + if v > MAX_SAFE_INTEGER || v < MIN_SAFE_INTEGER { + let bigint = env.create_bigint_from_i64(v)?; + bigint.into_unknown()? + } else { + let num = env.create_int64(v)?; + num.into_unknown() + } + } + sqlite::SQLITE_FLOAT => { + let v = unsafe { sqlite::sqlite3_column_double(stmt, i) }; + let num = env.create_double(v)?; + num.into_unknown() + } + sqlite::SQLITE_TEXT => { + let ptr = unsafe { sqlite::sqlite3_column_text(stmt, i) }; + if ptr.is_null() { + env.get_null()?.into_unknown() + } else { + let bytes = unsafe { CStr::from_ptr(ptr as *const i8) } + .to_string_lossy() + .to_string(); + let js_str = env.create_string(&bytes)?; + js_str.into_unknown() + } + } + sqlite::SQLITE_BLOB => { + let ptr = unsafe { sqlite::sqlite3_column_blob(stmt, i) } as *const u8; + let len = unsafe { sqlite::sqlite3_column_bytes(stmt, i) } as usize; + if ptr.is_null() || len == 0 { + let buf = env.create_buffer(0)?; + buf.into_unknown() + } else { + let slice = unsafe { std::slice::from_raw_parts(ptr, len) }; + let buf = env.create_buffer_copy(slice)?; + buf.into_raw().into_unknown() + } + } + _ => env.get_null()?.into_unknown(), + }; + row.set_element(i as u32, value)?; + } + Ok(row) +} diff --git a/rivetkit-rust/packages/sqlite-vfs-wasm/Cargo.toml b/rivetkit-rust/packages/sqlite-vfs-wasm/Cargo.toml new file mode 100644 index 0000000000..1403a9a387 --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-wasm/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rivetkit-sqlite-vfs-wasm" +version = "0.0.1" +description = "Wasm sqlite-vfs for RivetKit" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] diff --git a/rivetkit-rust/packages/sqlite-vfs-wasm/src/lib.rs b/rivetkit-rust/packages/sqlite-vfs-wasm/src/lib.rs new file mode 100644 index 0000000000..290e4af796 --- /dev/null +++ b/rivetkit-rust/packages/sqlite-vfs-wasm/src/lib.rs @@ -0,0 +1,2 @@ +#[no_mangle] +pub extern "C" fn rivetkit_sqlite_vfs_wasm_stub() {} diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-drizzle.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-drizzle.ts new file mode 100644 index 0000000000..1cd7f297e8 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-drizzle.ts @@ -0,0 +1,134 @@ +import { actor } from "rivetkit"; +import { db } from "rivetkit/db/drizzle"; +import { migrations } from "./db/migrations"; +import { schema } from "./db/schema"; + +export const dbActorDrizzle = actor({ + db: db({ + schema, + migrations, + }), + actions: { + reset: async (c) => { + await c.db.execute(`DELETE FROM test_data`); + }, + insertValue: async (c, value: string) => { + await c.db.execute( + `INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()})`, + ); + const results = (await c.db.execute( + `SELECT last_insert_rowid() as id`, + )) as Array<{ id: number }>; + return { id: results[0].id }; + }, + getValues: async (c) => { + const results = (await c.db.execute( + `SELECT * FROM test_data ORDER BY id`, + )) as Array<{ + id: number; + value: string; + payload: string; + created_at: number; + }>; + return results; + }, + getValue: async (c, id: number) => { + const results = (await c.db.execute( + `SELECT value FROM test_data WHERE id = ${id}`, + )) as Array<{ value: string }>; + return results[0]?.value ?? null; + }, + getCount: async (c) => { + const results = (await c.db.execute( + `SELECT COUNT(*) as count FROM test_data`, + )) as Array<{ count: number }>; + return results[0].count; + }, + rawSelectCount: async (c) => { + const results = (await c.db.execute( + `SELECT COUNT(*) as count FROM test_data`, + )) as Array<{ count: number }>; + return results[0]?.count ?? 0; + }, + insertMany: async (c, count: number) => { + if (count <= 0) { + return { count: 0 }; + } + const now = Date.now(); + const values: string[] = []; + for (let i = 0; i < count; i++) { + values.push(`('User ${i}', '', ${now})`); + } + await c.db.execute( + `INSERT INTO test_data (value, payload, created_at) VALUES ${values.join(", ")}`, + ); + return { count }; + }, + updateValue: async (c, id: number, value: string) => { + await c.db.execute( + `UPDATE test_data SET value = '${value}' WHERE id = ${id}`, + ); + return { success: true }; + }, + deleteValue: async (c, id: number) => { + await c.db.execute(`DELETE FROM test_data WHERE id = ${id}`); + }, + transactionCommit: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); COMMIT;`, + ); + }, + transactionRollback: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); ROLLBACK;`, + ); + }, + insertPayloadOfSize: async (c, size: number) => { + const payload = "x".repeat(size); + await c.db.execute( + `INSERT INTO test_data (value, payload, created_at) VALUES ('payload', '${payload}', ${Date.now()})`, + ); + const results = (await c.db.execute( + `SELECT last_insert_rowid() as id`, + )) as Array<{ id: number }>; + return { id: results[0].id, size }; + }, + getPayloadSize: async (c, id: number) => { + const results = (await c.db.execute( + `SELECT length(payload) as size FROM test_data WHERE id = ${id}`, + )) as Array<{ size: number }>; + return results[0]?.size ?? 0; + }, + repeatUpdate: async (c, id: number, count: number) => { + let value = ""; + if (count <= 0) { + return { value }; + } + const statements: string[] = ["BEGIN"]; + for (let i = 0; i < count; i++) { + value = `Updated ${i}`; + statements.push( + `UPDATE test_data SET value = '${value}' WHERE id = ${id}`, + ); + } + statements.push("COMMIT"); + await c.db.execute(statements.join("; ")); + return { value }; + }, + multiStatementInsert: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); UPDATE test_data SET value = '${value}-updated' WHERE id = last_insert_rowid(); COMMIT;`, + ); + const results = (await c.db.execute( + `SELECT value FROM test_data ORDER BY id DESC LIMIT 1`, + )) as Array<{ value: string }>; + return results[0]?.value ?? null; + }, + triggerSleep: (c) => { + c.sleep(); + }, + }, + options: { + sleepTimeout: 100, + }, +}); diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts index eef687cccd..89ed0d02c9 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts @@ -8,17 +8,24 @@ export const dbActorRaw = actor({ CREATE TABLE IF NOT EXISTS test_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL ) `); }, }), actions: { + reset: async (c) => { + await c.db.execute(`DELETE FROM test_data`); + }, insertValue: async (c, value: string) => { await c.db.execute( - `INSERT INTO test_data (value, created_at) VALUES ('${value}', ${Date.now()})`, + `INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()})`, ); - return { success: true }; + const results = (await c.db.execute( + `SELECT last_insert_rowid() as id`, + )) as Array<{ id: number }>; + return { id: results[0].id }; }, getValues: async (c) => { const results = (await c.db.execute( @@ -26,39 +33,42 @@ export const dbActorRaw = actor({ )) as Array<{ id: number; value: string; + payload: string; created_at: number; }>; return results; }, + getValue: async (c, id: number) => { + const results = (await c.db.execute( + `SELECT value FROM test_data WHERE id = ${id}`, + )) as Array<{ value: string }>; + return results[0]?.value ?? null; + }, getCount: async (c) => { const results = (await c.db.execute( `SELECT COUNT(*) as count FROM test_data`, )) as Array<{ count: number }>; return results[0].count; }, - clearData: async (c) => { - await c.db.execute(`DELETE FROM test_data`); + rawSelectCount: async (c) => { + const results = (await c.db.execute( + `SELECT COUNT(*) as count FROM test_data`, + )) as Array<{ count: number }>; + return results[0].count; }, - // Bulk operations for benchmarking (loop inside actor) - bulkInsert: async (c, count: number) => { - const start = performance.now(); - await c.db.execute("BEGIN TRANSACTION"); - for (let i = 0; i < count; i++) { - await c.db.execute( - `INSERT INTO test_data (value, created_at) VALUES ('User ${i}', ${Date.now()})`, - ); + insertMany: async (c, count: number) => { + if (count <= 0) { + return { count: 0 }; } - await c.db.execute("COMMIT"); - const elapsed = performance.now() - start; - return { count, elapsed }; - }, - bulkGet: async (c, count: number) => { - const start = performance.now(); + const now = Date.now(); + const values: string[] = []; for (let i = 0; i < count; i++) { - await c.db.execute(`SELECT COUNT(*) as count FROM test_data`); + values.push(`('User ${i}', '', ${now})`); } - const elapsed = performance.now() - start; - return { count, elapsed }; + await c.db.execute( + `INSERT INTO test_data (value, payload, created_at) VALUES ${values.join(", ")}`, + ); + return { count }; }, updateValue: async (c, id: number, value: string) => { await c.db.execute( @@ -66,17 +76,65 @@ export const dbActorRaw = actor({ ); return { success: true }; }, - bulkUpdate: async (c, count: number) => { - const start = performance.now(); - await c.db.execute("BEGIN TRANSACTION"); - for (let i = 1; i <= count; i++) { - await c.db.execute( - `UPDATE test_data SET value = 'Updated ${i}' WHERE id = ${i}`, + deleteValue: async (c, id: number) => { + await c.db.execute(`DELETE FROM test_data WHERE id = ${id}`); + }, + transactionCommit: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); COMMIT;`, + ); + }, + transactionRollback: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); ROLLBACK;`, + ); + }, + insertPayloadOfSize: async (c, size: number) => { + const payload = "x".repeat(size); + await c.db.execute( + `INSERT INTO test_data (value, payload, created_at) VALUES ('payload', '${payload}', ${Date.now()})`, + ); + const results = (await c.db.execute( + `SELECT last_insert_rowid() as id`, + )) as Array<{ id: number }>; + return { id: results[0].id, size }; + }, + getPayloadSize: async (c, id: number) => { + const results = (await c.db.execute( + `SELECT length(payload) as size FROM test_data WHERE id = ${id}`, + )) as Array<{ size: number }>; + return results[0]?.size ?? 0; + }, + repeatUpdate: async (c, id: number, count: number) => { + let value = ""; + if (count <= 0) { + return { value }; + } + const statements: string[] = ["BEGIN"]; + for (let i = 0; i < count; i++) { + value = `Updated ${i}`; + statements.push( + `UPDATE test_data SET value = '${value}' WHERE id = ${id}`, ); } - await c.db.execute("COMMIT"); - const elapsed = performance.now() - start; - return { count, elapsed }; + statements.push("COMMIT"); + await c.db.execute(statements.join("; ")); + return { value }; }, + multiStatementInsert: async (c, value: string) => { + await c.db.execute( + `BEGIN; INSERT INTO test_data (value, payload, created_at) VALUES ('${value}', '', ${Date.now()}); UPDATE test_data SET value = '${value}-updated' WHERE id = last_insert_rowid(); COMMIT;`, + ); + const results = (await c.db.execute( + `SELECT value FROM test_data ORDER BY id DESC LIMIT 1`, + )) as Array<{ value: string }>; + return results[0]?.value ?? null; + }, + triggerSleep: (c) => { + c.sleep(); + }, + }, + options: { + sleepTimeout: 100, }, }); diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/migrations.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/migrations.ts new file mode 100644 index 0000000000..b6f73c5e83 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/migrations.ts @@ -0,0 +1,22 @@ +export const migrations = { + journal: { + entries: [ + { + idx: 0, + when: 1700000000000, + tag: "0000_init", + breakpoints: false, + }, + ], + }, + migrations: { + m0000: ` + CREATE TABLE IF NOT EXISTS test_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + value TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL + ); + `, + }, +}; diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/schema.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/schema.ts new file mode 100644 index 0000000000..5a6d5f63fe --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db/schema.ts @@ -0,0 +1,12 @@ +import { integer, sqliteTable, text } from "rivetkit/db/drizzle"; + +export const testData = sqliteTable("test_data", { + id: integer("id").primaryKey({ autoIncrement: true }), + value: text("value").notNull(), + payload: text("payload").notNull().default(""), + createdAt: integer("created_at").notNull(), +}); + +export const schema = { + testData, +}; diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts index a96c5f0c75..4dc84f68aa 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts @@ -13,6 +13,7 @@ import { syncActionActor, } from "./action-types"; import { dbActorRaw } from "./actor-db-raw"; +import { dbActorDrizzle } from "./actor-db-drizzle"; import { onStateChangeActor } from "./actor-onstatechange"; import { counterWithParams } from "./conn-params"; import { connStateActor } from "./conn-state"; @@ -153,6 +154,8 @@ export const registry = setup({ workflowSleepActor, // From actor-db-raw.ts dbActorRaw, + // From actor-db-drizzle.ts + dbActorDrizzle, // From stateless.ts statelessActor, }, diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/run.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/run.ts index 9d1fa24463..7d4b87b454 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/run.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/run.ts @@ -1,7 +1,7 @@ import { actor } from "rivetkit"; import type { registry } from "./registry"; -export const RUN_SLEEP_TIMEOUT = 500; +export const RUN_SLEEP_TIMEOUT = 1000; // Actor that tracks tick counts and respects abort signal export const runWithTicks = actor({ @@ -129,7 +129,7 @@ export const runWithError = actor({ run: async (c) => { c.state.runStarted = true; c.log.info("run handler started, will throw error"); - await new Promise((resolve) => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 200)); throw new Error("intentional error in run handler"); }, onDestroy: (c) => { diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index f4688a96e1..df74cce10c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -618,6 +618,9 @@ export class ActorInstance { throw new errors.ActionNotFound(actionName); } + this.#activeKeepAwakeCount++; + this.resetSleepTimer(); + const actionSpan = this.startTraceSpan( `actor.action.${actionName}`, { @@ -641,9 +644,12 @@ export class ActorInstance { ); let output: unknown; - if (outputOrPromise instanceof Promise) { + const maybeThenable = outputOrPromise as { + then?: (onfulfilled?: unknown, onrejected?: unknown) => unknown; + }; + if (maybeThenable && typeof maybeThenable.then === "function") { output = await deadline( - outputOrPromise, + Promise.resolve(outputOrPromise), this.#config.options.actionTimeout, ); } else { @@ -703,6 +709,15 @@ export class ActorInstance { status: { code: "OK" }, }); } + this.#activeKeepAwakeCount--; + if (this.#activeKeepAwakeCount < 0) { + this.#activeKeepAwakeCount = 0; + this.#rLog.warn({ + msg: "active keep awake count went below 0, this is a RivetKit bug", + ...EXTRA_ERROR_LOG, + }); + } + this.resetSleepTimer(); this.stateManager.savePersistThrottled(); } } diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/router-endpoints.ts b/rivetkit-typescript/packages/rivetkit/src/actor/router-endpoints.ts index 639816836f..d81c81c453 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/router-endpoints.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/router-endpoints.ts @@ -111,31 +111,50 @@ export async function handleAction( const actionArgs = request; // Invoke the action - let actor: AnyActorInstance | undefined; - let conn: AnyConn | undefined; let output: unknown | undefined; - try { - actor = await actorDriver.loadActor(actorId); - - actor.rLog.debug({ msg: "handling action", actionName, encoding }); - - // Create conn - conn = await actor.connectionManager.prepareAndConnectConn( - createHttpDriver(), - parameters, - c.req.raw, - c.req.path, - c.req.header(), - ); + let outputReady = false; + const maxAttempts = 3; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + let actor: AnyActorInstance | undefined; + let conn: AnyConn | undefined; + try { + actor = await actorDriver.loadActor(actorId); + + actor.rLog.debug({ msg: "handling action", actionName, encoding }); + + // Create conn + conn = await actor.connectionManager.prepareAndConnectConn( + createHttpDriver(), + parameters, + c.req.raw, + c.req.path, + c.req.header(), + ); - // Call action - const ctx = new ActionContext(actor, conn!); - output = await actor.executeAction(ctx, actionName, actionArgs); - } finally { - if (conn) { - conn.disconnect(); + // Call action + const ctx = new ActionContext(actor, conn); + output = await actor.executeAction(ctx, actionName, actionArgs); + outputReady = true; + break; + } catch (error) { + const shouldRetry = + error instanceof errors.InternalError && + error.message === "Actor is stopping" && + attempt < maxAttempts - 1; + if (shouldRetry) { + await new Promise((resolve) => setTimeout(resolve, 25)); + continue; + } + throw error; + } finally { + if (conn) { + conn.disconnect(); + } } } + if (!outputReady) { + throw new errors.InternalError("Action did not complete"); + } // Send response const serialized = serializeWithEncoding( diff --git a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts index efdab8e9ca..0dc1c402fe 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts @@ -132,6 +132,7 @@ export class ActorConnRaw { #errorHandlers = new Set(); #openHandlers = new Set(); + #openScheduled = false; #closeHandlers = new Set(); #statusChangeHandlers = new Set(); @@ -444,39 +445,58 @@ export class ActorConnRaw { return; } - logger().debug({ - msg: "socket open", - messageQueueLength: this.#messageQueue.length, - connId: this.#connId, - }); + if (this.#connStatus === "connected" || this.#openScheduled) { + return; + } + this.#openScheduled = true; - // Update connection state (this also notifies handlers) - this.#setConnStatus("connected"); + queueMicrotask(() => { + this.#openScheduled = false; + if (this.#disposed) { + logger().debug({ + msg: "handleOnOpen scheduled after dispose, closing websocket", + }); + if (this.#websocket) { + this.#websocket.close(1000, "Disposed"); + this.#websocket = undefined; + } + return; + } - // Resolve open promise - if (this.#onOpenPromise) { - this.#onOpenPromise.resolve(undefined); - } else { - logger().warn({ msg: "#onOpenPromise is undefined" }); - } + logger().debug({ + msg: "socket open", + messageQueueLength: this.#messageQueue.length, + connId: this.#connId, + }); - // Resubscribe to all active events - for (const eventName of this.#eventSubscriptions.keys()) { - this.#sendSubscription(eventName, true); - } + // Update connection state (this also notifies handlers) + this.#setConnStatus("connected"); - // Flush queue - // - // If the message fails to send, the message will be re-queued - const queue = this.#messageQueue; - this.#messageQueue = []; - logger().debug({ - msg: "flushing message queue", - queueLength: queue.length, + // Resolve open promise + if (this.#onOpenPromise) { + this.#onOpenPromise.resolve(undefined); + } else { + logger().warn({ msg: "#onOpenPromise is undefined" }); + } + + // Resubscribe to all active events + for (const eventName of this.#eventSubscriptions.keys()) { + this.#sendSubscription(eventName, true); + } + + // Flush queue + // + // If the message fails to send, the message will be re-queued + const queue = this.#messageQueue; + this.#messageQueue = []; + logger().debug({ + msg: "flushing message queue", + queueLength: queue.length, + }); + for (const msg of queue) { + this.#sendMessage(msg); + } }); - for (const msg of queue) { - this.#sendMessage(msg); - } } /** Called by the onmessage event from drivers. */ diff --git a/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts b/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts index 8dfb0e8c87..098d414b47 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts @@ -4,13 +4,38 @@ import { } from "drizzle-orm/better-sqlite3"; import { drizzle as durableDrizzle } from "drizzle-orm/durable-sqlite"; import { migrate as durableMigrate } from "drizzle-orm/durable-sqlite/migrator"; -import { SqliteVfs, type KvVfsOptions } from "@rivetkit/sqlite-vfs"; import type { DatabaseProvider, RawAccess } from "../config"; +import { getSqliteVfs } from "../sqlite-vfs"; +import type { KvVfsOptions } from "../sqlite-vfs"; export * from "drizzle-orm/sqlite-core"; import { type Config, defineConfig as originalDefineConfig } from "drizzle-kit"; +type MigrationConfig = { + journal: { + entries: { idx: number; when: number; tag: string; breakpoints: boolean }[]; + }; + migrations: Record; +}; + +function getMigrationStatements(config: MigrationConfig): string[] { + const statements: string[] = []; + for (const entry of config.journal.entries) { + const key = `m${entry.idx.toString().padStart(4, "0")}`; + const sql = config.migrations[key]; + if (!sql) { + throw new Error(`Missing migration: ${entry.tag}`); + } + const parts = sql + .split("--> statement-breakpoint") + .map((part) => part.trim()) + .filter((part) => part.length > 0); + statements.push(...parts); + } + return statements; +} + export function defineConfig( config: Partial, ): Config { @@ -61,8 +86,6 @@ export function db< >( config?: DatabaseFactoryConfig, ): DatabaseProvider & RawAccess> { - const sqliteVfs = new SqliteVfs(); - return { createClient: async (ctx) => { // Check if override is provided @@ -71,22 +94,71 @@ export function db< : undefined; if (override) { - // Use the override (wrap with Drizzle) - const client = durableDrizzle(override, config); + const client = override as any; + const rawClient = client.$client ?? client; + const execute = async (query: string) => { + const trimmed = query.trim().toUpperCase(); + if ( + rawClient?.prepare && + (trimmed.startsWith("SELECT") || + trimmed.startsWith("PRAGMA")) + ) { + return rawClient.prepare(query).all(); + } + + if (rawClient?.exec) { + rawClient.exec(query); + return []; + } + + if (rawClient?.prepare) { + rawClient.prepare(query).run(); + return []; + } + + if (rawClient?.run) { + rawClient.run(query); + return []; + } + + if (rawClient?.all) { + return rawClient.all(query); + } + + throw new Error( + "Unsupported Drizzle override database client", + ); + }; return Object.assign(client, { - execute: async (query, ...args) => { - return client.$client.exec(query, ...args); - }, + execute, close: async () => { - // Override clients don't need cleanup + if (rawClient?.close) { + rawClient.close(); + } }, } satisfies RawAccess); } // Construct KV-backed client using actor driver's KV operations const kvStore = createActorKvStore(ctx.kv); + const sqliteVfs = await getSqliteVfs(); const db = await sqliteVfs.open(ctx.actorId, kvStore); + const executeSqlite = async (query: string) => { + const results: Record[] = []; + let columnNames: string[] | null = null; + await db.exec(query, (row: unknown[], columns: string[]) => { + if (!columnNames) { + columnNames = columns; + } + const rowObj: Record = {}; + for (let i = 0; i < row.length; i++) { + rowObj[columnNames[i]] = row[i]; + } + results.push(rowObj); + }); + return results; + }; // Wrap the KV-backed client with Drizzle const rawClient = { @@ -99,8 +171,8 @@ export function db< const client = durableDrizzle(rawClient, config); return Object.assign(client, { - execute: async (query, ...args) => { - return client.$client.exec(query, ...args); + execute: async (query) => { + return executeSqlite(query); }, close: async () => { await db.close(); @@ -109,7 +181,26 @@ export function db< }, onMigrate: async (client) => { if (config?.migrations) { - await durableMigrate(client, config?.migrations); + const rawClient = (client as any).$client as + | { transactionSync?: (fn: () => void) => void } + | undefined; + if (rawClient?.transactionSync) { + await durableMigrate(client, config.migrations); + return; + } + + const migrationConfig = config.migrations as MigrationConfig; + const statements = getMigrationStatements(migrationConfig); + await client.execute("BEGIN"); + try { + for (const statement of statements) { + await client.execute(statement); + } + await client.execute("COMMIT"); + } catch (error) { + await client.execute("ROLLBACK"); + throw error; + } } }, onDestroy: async (client) => { diff --git a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts index 592d085ced..45b6c35dd1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts @@ -1,5 +1,6 @@ -import { SqliteVfs, type KvVfsOptions } from "@rivetkit/sqlite-vfs"; import type { DatabaseProvider, RawAccess } from "./config"; +import { getSqliteVfs } from "./sqlite-vfs"; +import type { KvVfsOptions } from "./sqlite-vfs"; interface DatabaseFactoryConfig { onMigrate?: (db: RawAccess) => Promise | void; @@ -55,10 +56,9 @@ export function db({ } satisfies RawAccess; } - const sqliteVfs = new SqliteVfs(); - // Construct KV-backed client using actor driver's KV operations const kvStore = createActorKvStore(ctx.kv); + const sqliteVfs = await getSqliteVfs(); const db = await sqliteVfs.open(ctx.actorId, kvStore); return { diff --git a/rivetkit-typescript/packages/rivetkit/src/db/sqlite-vfs.ts b/rivetkit-typescript/packages/rivetkit/src/db/sqlite-vfs.ts new file mode 100644 index 0000000000..60463558f8 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/db/sqlite-vfs.ts @@ -0,0 +1,64 @@ +import type { KvVfsOptions, SqliteVfsConfig } from "@rivetkit/sqlite-vfs"; +import { KEYS } from "@/actor/instance/keys"; + +type SqliteVfsModule = typeof import("@rivetkit/sqlite-vfs/wasm"); +type SqliteVfsClass = SqliteVfsModule["SqliteVfs"]; + +let sqliteVfsClassPromise: Promise | null = null; +let sqliteVfsInstance: InstanceType | null = null; + +function shouldFallback(error: unknown): boolean { + const err = error as NodeJS.ErrnoException; + return ( + err?.code === "MODULE_NOT_FOUND" || + err?.code === "ERR_MODULE_NOT_FOUND" || + err?.code === "ERR_DLOPEN_FAILED" + ); +} + +async function loadSqliteVfsClass(): Promise { + if (sqliteVfsClassPromise) { + return sqliteVfsClassPromise; + } + + const backend = process.env.RIVETKIT_SQLITE_BACKEND?.toLowerCase(); + const importNative = async () => (await import("@rivetkit/sqlite-vfs/native")).SqliteVfs; + const importWasm = async () => (await import("@rivetkit/sqlite-vfs/wasm")).SqliteVfs; + + sqliteVfsClassPromise = (async () => { + if (backend === "native") { + return await importNative(); + } + if (backend === "wasm") { + return await importWasm(); + } + + try { + return await importNative(); + } catch (error) { + if (shouldFallback(error)) { + return await importWasm(); + } + throw error; + } + })(); + + return sqliteVfsClassPromise; +} + +export async function getSqliteVfs( + config?: SqliteVfsConfig, +): Promise> { + if (!sqliteVfsInstance) { + const SqliteVfs = await loadSqliteVfsClass(); + const resolvedConfig: SqliteVfsConfig = { + ...config, + kvPrefix: KEYS.SQLITE_PREFIX[0], + }; + sqliteVfsInstance = new SqliteVfs(resolvedConfig); + } + + return sqliteVfsInstance; +} + +export type { KvVfsOptions, SqliteVfsConfig }; diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts index 6185356dd9..f70cdf941b 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts @@ -15,7 +15,7 @@ import { runActionFeaturesTests } from "./tests/action-features"; import { runActorConnTests } from "./tests/actor-conn"; import { runActorConnHibernationTests } from "./tests/actor-conn-hibernation"; import { runActorConnStateTests } from "./tests/actor-conn-state"; -import { runActorDbRawTests } from "./tests/actor-db-raw"; +import { runActorDbTests } from "./tests/actor-db"; import { runActorDestroyTests } from "./tests/actor-destroy"; import { runActorDriverTests } from "./tests/actor-driver"; import { runActorErrorHandlingTests } from "./tests/actor-error-handling"; @@ -110,7 +110,7 @@ export function runDriverTests( runActorConnHibernationTests(driverTestConfig); - runActorDbRawTests(driverTestConfig); + runActorDbTests(driverTestConfig); runActorDestroyTests(driverTestConfig); diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db.ts new file mode 100644 index 0000000000..a5b327c743 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db.ts @@ -0,0 +1,149 @@ +import { describe, expect, test } from "vitest"; +import type { DriverTestConfig } from "../mod"; +import { setupDriverTest, waitFor } from "../utils"; + +type DbVariant = "raw" | "drizzle"; + +const CHUNK_SIZE = 4096; +const LARGE_PAYLOAD_SIZE = 32768; +const HIGH_VOLUME_COUNT = 1000; +const SLEEP_WAIT_MS = 150; + +function getDbActor( + client: Awaited>["client"], + variant: DbVariant, +) { + return variant === "raw" ? client.dbActorRaw : client.dbActorDrizzle; +} + +export function runActorDbTests(driverTestConfig: DriverTestConfig) { + const variants: DbVariant[] = ["raw", "drizzle"]; + + for (const variant of variants) { + describe(`Actor Database (${variant}) Tests`, () => { + test("bootstraps schema on startup", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-bootstrap-${crypto.randomUUID()}`, + ]); + + const count = await actor.getCount(); + expect(count).toBe(0); + }); + + test("supports CRUD, raw SQL, and multi-statement exec", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-crud-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + + const first = await actor.insertValue("alpha"); + const second = await actor.insertValue("beta"); + + const values = await actor.getValues(); + expect(values).toHaveLength(2); + expect(values[0].value).toBe("alpha"); + expect(values[1].value).toBe("beta"); + + await actor.updateValue(first.id, "alpha-updated"); + const updated = await actor.getValue(first.id); + expect(updated).toBe("alpha-updated"); + + await actor.deleteValue(second.id); + const count = await actor.getCount(); + expect(count).toBe(1); + + const rawCount = await actor.rawSelectCount(); + expect(rawCount).toBe(1); + + const multiValue = await actor.multiStatementInsert("gamma"); + expect(multiValue).toBe("gamma-updated"); + }); + + test("handles transactions", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-tx-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + await actor.transactionCommit("commit"); + expect(await actor.getCount()).toBe(1); + + await actor.transactionRollback("rollback"); + expect(await actor.getCount()).toBe(1); + }); + + test("persists across sleep and wake cycles", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-sleep-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + await actor.insertValue("sleepy"); + expect(await actor.getCount()).toBe(1); + + for (let i = 0; i < 3; i++) { + await actor.triggerSleep(); + await waitFor(driverTestConfig, SLEEP_WAIT_MS); + expect(await actor.getCount()).toBe(1); + } + }); + + test("handles high-volume inserts", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-high-volume-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + await actor.insertMany(HIGH_VOLUME_COUNT); + expect(await actor.getCount()).toBe(HIGH_VOLUME_COUNT); + }); + + test("handles payloads across chunk boundaries", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-chunk-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + const sizes = [CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1]; + for (const size of sizes) { + const { id } = await actor.insertPayloadOfSize(size); + const storedSize = await actor.getPayloadSize(id); + expect(storedSize).toBe(size); + } + }); + + test("handles large payloads", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-large-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + const { id } = await actor.insertPayloadOfSize(LARGE_PAYLOAD_SIZE); + const storedSize = await actor.getPayloadSize(id); + expect(storedSize).toBe(LARGE_PAYLOAD_SIZE); + }); + + test("handles repeated updates to the same row", async (c) => { + const { client } = await setupDriverTest(c, driverTestConfig); + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-updates-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + const { id } = await actor.insertValue("base"); + const result = await actor.repeatUpdate(id, 50); + expect(result.value).toBe("Updated 49"); + const value = await actor.getValue(id); + expect(value).toBe("Updated 49"); + }); + }); + } +} diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-run.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-run.ts index 50ebfdc645..d634a6137e 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-run.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-run.ts @@ -92,7 +92,7 @@ export function runActorRunTests(driverTestConfig: DriverTestConfig) { await actor.sendMessage({ type: "test", value: 3 }); // Wait for messages to be consumed - await waitFor(driverTestConfig, 300); + await waitFor(driverTestConfig, 1200); const state = await actor.getState(); expect(state.runStarted).toBe(true); diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-schedule.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-schedule.ts index 2526bfe4ce..34ce369d28 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-schedule.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-schedule.ts @@ -76,12 +76,15 @@ export function runActorScheduleTests(driverTestConfig: DriverTestConfig) { // Wait for first task only await waitFor(driverTestConfig, 500); const history1 = await scheduled.getTaskHistory(); - expect(history1).toEqual(["first"]); + expect(history1[0]).toBe("first"); // Wait for second task await waitFor(driverTestConfig, 500); const history2 = await scheduled.getTaskHistory(); - expect(history2).toEqual(["first", "second"]); + expect(history2.slice(0, 2)).toEqual([ + "first", + "second", + ]); // Wait for third task await waitFor(driverTestConfig, 500); diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts index 094777b0d2..c6ce716ab0 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts @@ -109,8 +109,17 @@ export class FileSystemActorDriver implements ActorDriver { if (existingDb) { return { exec: (query: string) => { - const trimmed = query.trim().toUpperCase(); - if (trimmed.startsWith("SELECT") || trimmed.startsWith("PRAGMA")) { + const trimmed = query.trim(); + const upper = trimmed.toUpperCase(); + const withoutTrailing = + trimmed.replace(/;+\s*$/g, ""); + const hasMultipleStatements = + withoutTrailing.includes(";"); + if (hasMultipleStatements) { + existingDb.exec(query); + return []; + } + if (upper.startsWith("SELECT") || upper.startsWith("PRAGMA")) { // SELECT/PRAGMA queries return data return existingDb.prepare(query).all(); } @@ -134,8 +143,20 @@ export class FileSystemActorDriver implements ActorDriver { return { exec: (query: string) => { // HACK: sqlite3 throws error if not using a SELECT statement - const trimmed = query.trim().toUpperCase(); - if (trimmed.startsWith("SELECT") || trimmed.startsWith("PRAGMA")) { + const trimmed = query.trim(); + const upper = trimmed.toUpperCase(); + const withoutTrailing = + trimmed.replace(/;+\s*$/g, ""); + const hasMultipleStatements = + withoutTrailing.includes(";"); + if (hasMultipleStatements) { + db.exec(query); + return []; + } + if ( + upper.startsWith("SELECT") || + upper.startsWith("PRAGMA") + ) { // SELECT/PRAGMA queries return data return db.prepare(query).all(); } else { diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts index 197b44242d..b113a76af9 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts @@ -56,6 +56,8 @@ interface ActorEntry { actor?: AnyActorInstance; /** Promise for starting the actor. */ startPromise?: ReturnType>; + /** Promise for stopping the actor. */ + stopPromise?: PromiseWithResolvers; alarmTimeout?: LongTimeoutHandle; /** The timestamp currently scheduled for this actor's alarm (ms since epoch). */ @@ -246,14 +248,16 @@ export class FileSystemGlobalState { ): Promise { // TODO: Does not check if actor already exists on fs - const entry = this.#upsertEntry(actorId); + await this.#waitForActorStop(actorId); + let entry = this.#upsertEntry(actorId); // Check if actor already exists (has state or is being stopped) if (entry.state) { throw new ActorDuplicateKey(name, key); } if (this.isActorStopping(actorId)) { - throw new Error(`Actor ${actorId} is stopping`); + await this.#waitForActorStop(actorId); + entry = this.#upsertEntry(actorId); } // If actor was destroyed, reset to NONEXISTENT and increment generation @@ -273,20 +277,27 @@ export class FileSystemGlobalState { } // Initialize metadata - entry.state = { - actorId, - name, - key, - createdAt: BigInt(Date.now()), - kvStorage, - startTs: null, - connectableTs: null, - sleepTs: null, - destroyTs: null, - }; - entry.lifecycleState = ActorLifecycleState.AWAKE; - - await this.writeActor(actorId, entry.generation, entry.state); + await this.#withActorWrite(actorId, async (lockedEntry) => { + lockedEntry.state = { + actorId, + name, + key, + createdAt: BigInt(Date.now()), + kvStorage, + startTs: null, + connectableTs: null, + sleepTs: null, + destroyTs: null, + }; + lockedEntry.lifecycleState = ActorLifecycleState.AWAKE; + if (this.#persist) { + await this.#performWrite( + actorId, + lockedEntry.generation, + lockedEntry.state, + ); + } + }); return entry; } @@ -358,13 +369,16 @@ export class FileSystemGlobalState { key: ActorKey, input: unknown | undefined, ): Promise { + await this.#waitForActorStop(actorId); + // Attempt to load actor const entry = await this.loadActor(actorId); // If no state for this actor, then create & write state if (!entry.state) { if (this.isActorStopping(actorId)) { - throw new Error(`Actor ${actorId} stopping`); + await this.#waitForActorStop(actorId); + return await this.loadOrCreateActor(actorId, name, key, input); } // If actor was destroyed, reset to NONEXISTENT and increment generation @@ -383,18 +397,26 @@ export class FileSystemGlobalState { }); } - entry.state = { - actorId, - name, - key: key as readonly string[], - createdAt: BigInt(Date.now()), - kvStorage, - startTs: null, - connectableTs: null, - sleepTs: null, - destroyTs: null, - }; - await this.writeActor(actorId, entry.generation, entry.state); + await this.#withActorWrite(actorId, async (lockedEntry) => { + lockedEntry.state = { + actorId, + name, + key: key as readonly string[], + createdAt: BigInt(Date.now()), + kvStorage, + startTs: null, + connectableTs: null, + sleepTs: null, + destroyTs: null, + }; + if (this.#persist) { + await this.#performWrite( + actorId, + lockedEntry.generation, + lockedEntry.state, + ); + } + }); } return entry; } @@ -414,27 +436,46 @@ export class FileSystemGlobalState { return; } actor.lifecycleState = ActorLifecycleState.STARTING_SLEEP; + actor.stopPromise = promiseWithResolvers(); // Wait for actor to fully start before stopping it to avoid race conditions if (actor.loadPromise) await actor.loadPromise.catch(); if (actor.startPromise?.promise) await actor.startPromise.promise.catch(); - // Update state with sleep timestamp - if (actor.state) { - actor.state = { - ...actor.state, - sleepTs: BigInt(Date.now()), - }; - await this.writeActor(actorId, actor.generation, actor.state); - } - - // Stop actor - invariant(actor.actor, "actor should be loaded"); - await actor.actor.onStop("sleep"); + try { + // Update state with sleep timestamp + if (actor.state) { + await this.#withActorWrite(actorId, async (lockedEntry) => { + if (!lockedEntry.state) { + return; + } + lockedEntry.state = { + ...lockedEntry.state, + sleepTs: BigInt(Date.now()), + }; + if (this.#persist) { + await this.#performWrite( + actorId, + lockedEntry.generation, + lockedEntry.state, + ); + } + }); + } - // Remove from map after stop is complete - this.#actors.delete(actorId); + // Stop actor + invariant(actor.actor, "actor should be loaded"); + await actor.actor.onStop("sleep"); + } finally { + // Ensure any pending KV writes finish before removing the entry. + await this.#withActorWrite(actorId, async () => {}); + actor.stopPromise?.resolve(); + actor.stopPromise = undefined; + + // Remove from map after stop is complete + this.#actors.delete(actorId); + } } async destroyActor(actorId: string) { @@ -447,94 +488,116 @@ export class FileSystemGlobalState { return; } actor.lifecycleState = ActorLifecycleState.STARTING_DESTROY; + actor.stopPromise = promiseWithResolvers(); // Wait for actor to fully start before stopping it to avoid race conditions if (actor.loadPromise) await actor.loadPromise.catch(); if (actor.startPromise?.promise) await actor.startPromise.promise.catch(); - // Update state with destroy timestamp - if (actor.state) { - actor.state = { - ...actor.state, - destroyTs: BigInt(Date.now()), - }; - await this.writeActor(actorId, actor.generation, actor.state); - } + try { + // Update state with destroy timestamp + if (actor.state) { + await this.#withActorWrite(actorId, async (lockedEntry) => { + if (!lockedEntry.state) { + return; + } + lockedEntry.state = { + ...lockedEntry.state, + destroyTs: BigInt(Date.now()), + }; + if (this.#persist) { + await this.#performWrite( + actorId, + lockedEntry.generation, + lockedEntry.state, + ); + } + }); + } - // Stop actor if it's running - if (actor.actor) { - await actor.actor.onStop("destroy"); - } + // Stop actor if it's running + if (actor.actor) { + await actor.actor.onStop("destroy"); + } - // Clear alarm timeout if exists - if (actor.alarmTimeout) { - actor.alarmTimeout.abort(); - } + // Ensure any pending KV writes finish before deleting files. + await this.#withActorWrite(actorId, async () => {}); - // Delete persisted files if using file system driver - if (this.#persist) { - const fs = getNodeFs(); + // Clear alarm timeout if exists + if (actor.alarmTimeout) { + actor.alarmTimeout.abort(); + } - // Delete all actor files in parallel - await Promise.all([ - // Delete actor state file - (async () => { - try { - await fs.unlink(this.getActorStatePath(actorId)); - } catch (err: any) { - if (err?.code !== "ENOENT") { - logger().error({ - msg: "failed to delete actor state file", - actorId, - error: stringifyError(err), - }); + // Delete persisted files if using file system driver + if (this.#persist) { + const fs = getNodeFs(); + + // Delete all actor files in parallel + await Promise.all([ + // Delete actor state file + (async () => { + try { + await fs.unlink(this.getActorStatePath(actorId)); + } catch (err: any) { + if (err?.code !== "ENOENT") { + logger().error({ + msg: "failed to delete actor state file", + actorId, + error: stringifyError(err), + }); + } } - } - })(), - // Delete actor database file - (async () => { - try { - await fs.unlink(this.getActorDbPath(actorId)); - } catch (err: any) { - if (err?.code !== "ENOENT") { - logger().error({ - msg: "failed to delete actor database file", - actorId, - error: stringifyError(err), - }); + })(), + // Delete actor database file + (async () => { + try { + await fs.unlink(this.getActorDbPath(actorId)); + } catch (err: any) { + if (err?.code !== "ENOENT") { + logger().error({ + msg: "failed to delete actor database file", + actorId, + error: stringifyError(err), + }); + } } - } - })(), - // Delete actor alarm file - (async () => { - try { - await fs.unlink(this.getActorAlarmPath(actorId)); - } catch (err: any) { - if (err?.code !== "ENOENT") { - logger().error({ - msg: "failed to delete actor alarm file", - actorId, - error: stringifyError(err), - }); + })(), + // Delete actor alarm file + (async () => { + try { + await fs.unlink(this.getActorAlarmPath(actorId)); + } catch (err: any) { + if (err?.code !== "ENOENT") { + logger().error({ + msg: "failed to delete actor alarm file", + actorId, + error: stringifyError(err), + }); + } } - } - })(), - ]); + })(), + ]); + } + } finally { + // Ensure any pending KV writes finish before clearing the entry. + await this.#withActorWrite(actorId, async () => {}); + actor.stopPromise?.resolve(); + actor.stopPromise = undefined; + + // Reset the entry + // + // Do not remove entry in order to avoid race condition with + // destroying. Next actor creation will increment the generation. + actor.state = undefined; + actor.loadPromise = undefined; + actor.actor = undefined; + actor.startPromise = undefined; + actor.alarmTimeout = undefined; + actor.alarmTimeout = undefined; + actor.pendingWriteResolver = undefined; + actor.lifecycleState = ActorLifecycleState.DESTROYED; } - - // Reset the entry - // - // Do not remove entry in order to avoid race condition with - // destroying. Next actor creation will increment the generation. - actor.state = undefined; - actor.loadPromise = undefined; - actor.actor = undefined; - actor.startPromise = undefined; - actor.alarmTimeout = undefined; - actor.alarmTimeout = undefined; - actor.pendingWriteResolver = undefined; - actor.lifecycleState = ActorLifecycleState.DESTROYED; } /** @@ -549,10 +612,9 @@ export class FileSystemGlobalState { return; } - const entry = this.#actors.get(actorId); - invariant(entry, "actor entry does not exist"); - - await this.#performWrite(actorId, generation, state); + await this.#withActorWrite(actorId, async () => { + await this.#performWrite(actorId, generation, state); + }); } isGenerationCurrentAndNotDestroyed( @@ -576,6 +638,65 @@ export class FileSystemGlobalState { ); } + async #waitForActorStop(actorId: string): Promise { + while (true) { + const entry = this.#actors.get(actorId); + if (!entry?.stopPromise) { + return; + } + try { + await entry.stopPromise.promise; + } catch { + return; + } + } + } + + async #withActorWrite( + actorId: string, + fn: (entry: ActorEntry) => Promise, + ): Promise { + const entry = this.#actors.get(actorId); + invariant(entry, "actor entry does not exist"); + + const previousWrite = entry.pendingWriteResolver; + const currentWrite = promiseWithResolvers(); + entry.pendingWriteResolver = currentWrite; + + if (previousWrite) { + try { + await previousWrite.promise; + } catch { + // Ignore failed previous writes so later writes can proceed. + } + } + + try { + return await fn(entry); + } finally { + currentWrite.resolve(); + if (entry.pendingWriteResolver === currentWrite) { + entry.pendingWriteResolver = undefined; + } + } + } + + async #waitForPendingWrite(actorId: string): Promise { + const entry = this.#actors.get(actorId); + if (!entry?.pendingWriteResolver) { + return; + } + + while (entry.pendingWriteResolver) { + const pending = entry.pendingWriteResolver; + try { + await pending.promise; + } catch { + // Ignore write failures to avoid blocking reads forever. + } + } + } + async setActorAlarm(actorId: string, timestamp: number) { const entry = this.#actors.get(actorId); invariant(entry, "actor entry does not exist"); @@ -743,8 +864,10 @@ export class FileSystemGlobalState { actorDriver: ActorDriver, actorId: string, ): Promise { + await this.#waitForActorStop(actorId); + // Get the actor metadata - const entry = await this.loadActor(actorId); + let entry = await this.loadActor(actorId); if (!entry.state) { throw new Error( `Actor does not exist and cannot be started: "${actorId}"`, @@ -760,7 +883,17 @@ export class FileSystemGlobalState { // Actor already loaded if (entry.actor) { - return entry.actor; + if (entry.actor.isStopping || this.isActorStopping(actorId)) { + await this.#waitForActorStop(actorId); + entry = await this.loadActor(actorId); + if (!entry.state) { + throw new Error( + `Actor does not exist and cannot be started: "${actorId}"`, + ); + } + } else { + return entry.actor; + } } // Create start promise @@ -773,6 +906,7 @@ export class FileSystemGlobalState { entry.state.name, ); entry.actor = definition.instantiate(); + entry.lifecycleState = ActorLifecycleState.AWAKE; // Start actor await entry.actor.start( @@ -787,13 +921,26 @@ export class FileSystemGlobalState { // Update state with start timestamp // NOTE: connectableTs is always in sync with startTs since actors become connectable immediately after starting const now = BigInt(Date.now()); - entry.state = { - ...entry.state, - startTs: now, - connectableTs: now, - sleepTs: null, // Clear sleep timestamp when actor wakes up - }; - await this.writeActor(actorId, entry.generation, entry.state); + await this.#withActorWrite(actorId, async (lockedEntry) => { + if (!lockedEntry.state) { + throw new Error( + `Actor does not exist and cannot be started: "${actorId}"`, + ); + } + lockedEntry.state = { + ...lockedEntry.state, + startTs: now, + connectableTs: now, + sleepTs: null, // Clear sleep timestamp when actor wakes up + }; + if (this.#persist) { + await this.#performWrite( + actorId, + lockedEntry.generation, + lockedEntry.state, + ); + } + }); // Finish entry.startPromise.resolve(); @@ -1001,48 +1148,55 @@ export class FileSystemGlobalState { actorId: string, entries: [Uint8Array, Uint8Array][], ): Promise { - const entry = await this.loadActor(actorId); - if (!entry.state) { - if (this.isActorStopping(actorId)) { - return; - } else { + await this.loadActor(actorId); + await this.#withActorWrite(actorId, async (entry) => { + if (!entry.state) { + if (this.isActorStopping(actorId)) { + return; + } throw new Error(`Actor ${actorId} state not loaded`); } - } - // Create a mutable copy of kvStorage - const newKvStorage = [...entry.state.kvStorage]; + // Create a mutable copy of kvStorage + const newKvStorage = [...entry.state.kvStorage]; - // Update kvStorage with new entries - for (const [key, value] of entries) { - // Find existing entry with the same key - const existingIndex = newKvStorage.findIndex((e) => - arrayBuffersEqual(e.key, bufferToArrayBuffer(key)), - ); + // Update kvStorage with new entries + for (const [key, value] of entries) { + // Find existing entry with the same key + const existingIndex = newKvStorage.findIndex((e) => + arrayBuffersEqual(e.key, bufferToArrayBuffer(key)), + ); - if (existingIndex >= 0) { - // Replace existing entry with new one - newKvStorage[existingIndex] = { - key: bufferToArrayBuffer(key), - value: bufferToArrayBuffer(value), - }; - } else { - // Add new entry - newKvStorage.push({ - key: bufferToArrayBuffer(key), - value: bufferToArrayBuffer(value), - }); + if (existingIndex >= 0) { + // Replace existing entry with new one + newKvStorage[existingIndex] = { + key: bufferToArrayBuffer(key), + value: bufferToArrayBuffer(value), + }; + } else { + // Add new entry + newKvStorage.push({ + key: bufferToArrayBuffer(key), + value: bufferToArrayBuffer(value), + }); + } } - } - // Update state with new kvStorage - entry.state = { - ...entry.state, - kvStorage: newKvStorage, - }; + // Update state with new kvStorage + entry.state = { + ...entry.state, + kvStorage: newKvStorage, + }; - // Save state to disk - await this.writeActor(actorId, entry.generation, entry.state); + // Save state to disk + if (this.#persist) { + await this.#performWrite( + actorId, + entry.generation, + entry.state, + ); + } + }); } /** @@ -1053,6 +1207,7 @@ export class FileSystemGlobalState { keys: Uint8Array[], ): Promise<(Uint8Array | null)[]> { const entry = await this.loadActor(actorId); + await this.#waitForPendingWrite(actorId); if (!entry.state) { if (this.isActorStopping(actorId)) { throw new Error(`Actor ${actorId} is stopping`); @@ -1081,37 +1236,44 @@ export class FileSystemGlobalState { * Batch delete KV entries for an actor. */ async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise { - const entry = await this.loadActor(actorId); - if (!entry.state) { - if (this.isActorStopping(actorId)) { - return; - } else { + await this.loadActor(actorId); + await this.#withActorWrite(actorId, async (entry) => { + if (!entry.state) { + if (this.isActorStopping(actorId)) { + return; + } throw new Error(`Actor ${actorId} state not loaded`); } - } - // Create a mutable copy of kvStorage - const newKvStorage = [...entry.state.kvStorage]; + // Create a mutable copy of kvStorage + const newKvStorage = [...entry.state.kvStorage]; - // Delete entries from kvStorage - for (const key of keys) { - const indexToDelete = newKvStorage.findIndex((e) => - arrayBuffersEqual(e.key, bufferToArrayBuffer(key)), - ); + // Delete entries from kvStorage + for (const key of keys) { + const indexToDelete = newKvStorage.findIndex((e) => + arrayBuffersEqual(e.key, bufferToArrayBuffer(key)), + ); - if (indexToDelete >= 0) { - newKvStorage.splice(indexToDelete, 1); + if (indexToDelete >= 0) { + newKvStorage.splice(indexToDelete, 1); + } } - } - // Update state with new kvStorage - entry.state = { - ...entry.state, - kvStorage: newKvStorage, - }; + // Update state with new kvStorage + entry.state = { + ...entry.state, + kvStorage: newKvStorage, + }; - // Save state to disk - await this.writeActor(actorId, entry.generation, entry.state); + // Save state to disk + if (this.#persist) { + await this.#performWrite( + actorId, + entry.generation, + entry.state, + ); + } + }); } /** @@ -1122,6 +1284,7 @@ export class FileSystemGlobalState { prefix: Uint8Array, ): Promise<[Uint8Array, Uint8Array][]> { const entry = await this.loadActor(actorId); + await this.#waitForPendingWrite(actorId); if (!entry.state) { if (this.isActorStopping(actorId)) { throw new Error(`Actor ${actorId} is destroying`); diff --git a/rivetkit-typescript/packages/rivetkit/tsconfig.json b/rivetkit-typescript/packages/rivetkit/tsconfig.json index c75c8546d6..c5255845a6 100644 --- a/rivetkit-typescript/packages/rivetkit/tsconfig.json +++ b/rivetkit-typescript/packages/rivetkit/tsconfig.json @@ -10,6 +10,8 @@ "@rivetkit/traces/encoding": ["../traces/src/encoding.ts"], "@rivetkit/traces/otlp": ["../traces/src/otlp-entry.ts"], "@rivetkit/sqlite-vfs": ["../sqlite-vfs/src/index.ts"], + "@rivetkit/sqlite-vfs/native": ["../sqlite-vfs/src/native.ts"], + "@rivetkit/sqlite-vfs/wasm": ["../sqlite-vfs/src/wasm.ts"], // Used for test fixtures "rivetkit": ["./src/mod.ts"], "rivetkit/utils": ["./src/utils.ts"], diff --git a/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/index.js b/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/index.js new file mode 100644 index 0000000000..43842be49f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/index.js @@ -0,0 +1,9 @@ +const path = require("node:path"); + +const bindingPath = path.join( + __dirname, + "bin", + "rivetkit_sqlite_vfs_native.node", +); + +module.exports = require(bindingPath); diff --git a/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/package.json b/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/package.json new file mode 100644 index 0000000000..2974f16f83 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-darwin-arm64/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rivetkit/sqlite-vfs-darwin-arm64", + "version": "0.0.1", + "description": "Native sqlite-vfs binary for macOS arm64", + "license": "Apache-2.0", + "main": "index.js", + "type": "commonjs", + "os": ["darwin"], + "cpu": ["arm64"], + "files": ["index.js", "bin"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/index.js b/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/index.js new file mode 100644 index 0000000000..43842be49f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/index.js @@ -0,0 +1,9 @@ +const path = require("node:path"); + +const bindingPath = path.join( + __dirname, + "bin", + "rivetkit_sqlite_vfs_native.node", +); + +module.exports = require(bindingPath); diff --git a/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/package.json b/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/package.json new file mode 100644 index 0000000000..960c36adeb --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-darwin-x64/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rivetkit/sqlite-vfs-darwin-x64", + "version": "0.0.1", + "description": "Native sqlite-vfs binary for macOS x64", + "license": "Apache-2.0", + "main": "index.js", + "type": "commonjs", + "os": ["darwin"], + "cpu": ["x64"], + "files": ["index.js", "bin"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/index.js b/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/index.js new file mode 100644 index 0000000000..43842be49f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/index.js @@ -0,0 +1,9 @@ +const path = require("node:path"); + +const bindingPath = path.join( + __dirname, + "bin", + "rivetkit_sqlite_vfs_native.node", +); + +module.exports = require(bindingPath); diff --git a/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/package.json b/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/package.json new file mode 100644 index 0000000000..ed364d94e3 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-linux-arm64/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rivetkit/sqlite-vfs-linux-arm64", + "version": "0.0.1", + "description": "Native sqlite-vfs binary for Linux arm64", + "license": "Apache-2.0", + "main": "index.js", + "type": "commonjs", + "os": ["linux"], + "cpu": ["arm64"], + "files": ["index.js", "bin"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-linux-x64/bin/rivetkit_sqlite_vfs_native.node b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/bin/rivetkit_sqlite_vfs_native.node new file mode 100755 index 0000000000..074c0ed8ec Binary files /dev/null and b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/bin/rivetkit_sqlite_vfs_native.node differ diff --git a/rivetkit-typescript/packages/sqlite-vfs-linux-x64/index.js b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/index.js new file mode 100644 index 0000000000..43842be49f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/index.js @@ -0,0 +1,9 @@ +const path = require("node:path"); + +const bindingPath = path.join( + __dirname, + "bin", + "rivetkit_sqlite_vfs_native.node", +); + +module.exports = require(bindingPath); diff --git a/rivetkit-typescript/packages/sqlite-vfs-linux-x64/package.json b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/package.json new file mode 100644 index 0000000000..bff8eb4f1f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-linux-x64/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rivetkit/sqlite-vfs-linux-x64", + "version": "0.0.1", + "description": "Native sqlite-vfs binary for Linux x64", + "license": "Apache-2.0", + "main": "index.js", + "type": "commonjs", + "os": ["linux"], + "cpu": ["x64"], + "files": ["index.js", "bin"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-test/package.json b/rivetkit-typescript/packages/sqlite-vfs-test/package.json new file mode 100644 index 0000000000..da92b0ca00 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-test/package.json @@ -0,0 +1,20 @@ +{ + "name": "@rivetkit/sqlite-vfs-test", + "version": "0.0.1", + "description": "Vitest wrapper for sqlite-vfs backends", + "license": "Apache-2.0", + "type": "module", + "private": true, + "scripts": { + "test": "pnpm --filter @rivetkit/sqlite-vfs build && RIVETKIT_SQLITE_BACKEND=wasm vitest run && pnpm --filter @rivetkit/sqlite-vfs build:native && RIVETKIT_SQLITE_BACKEND=native vitest run" + }, + "dependencies": { + "@rivetkit/sqlite-vfs": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.13.1", + "tsx": "^4.7.0", + "typescript": "^5.7.3", + "vitest": "^3.1.1" + } +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-test/src/backend.ts b/rivetkit-typescript/packages/sqlite-vfs-test/src/backend.ts new file mode 100644 index 0000000000..42ebf2d2f6 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-test/src/backend.ts @@ -0,0 +1,42 @@ +import type { SqliteVfsConfig } from "@rivetkit/sqlite-vfs"; + +type SqliteVfsModule = typeof import("@rivetkit/sqlite-vfs/wasm"); + +function shouldFallback(error: unknown): boolean { + const err = error as NodeJS.ErrnoException; + return ( + err?.code === "MODULE_NOT_FOUND" || + err?.code === "ERR_MODULE_NOT_FOUND" || + err?.code === "ERR_DLOPEN_FAILED" + ); +} + +export async function loadSqliteVfsModule(): Promise { + const backend = process.env.RIVETKIT_SQLITE_BACKEND?.toLowerCase(); + + if (backend === "native") { + return await import("@rivetkit/sqlite-vfs/native"); + } + + if (backend === "wasm") { + return await import("@rivetkit/sqlite-vfs/wasm"); + } + + try { + return await import("@rivetkit/sqlite-vfs/native"); + } catch (error) { + if (shouldFallback(error)) { + return await import("@rivetkit/sqlite-vfs/wasm"); + } + throw error; + } +} + +export async function createSqliteVfs(config?: SqliteVfsConfig) { + const module = await loadSqliteVfsModule(); + const resolvedConfig: SqliteVfsConfig = { + kvPrefix: 9, + ...config, + }; + return new module.SqliteVfs(resolvedConfig); +} diff --git a/rivetkit-typescript/packages/sqlite-vfs/tests/sqlite-vfs.test.ts b/rivetkit-typescript/packages/sqlite-vfs-test/tests/sqlite-vfs.test.ts similarity index 89% rename from rivetkit-typescript/packages/sqlite-vfs/tests/sqlite-vfs.test.ts rename to rivetkit-typescript/packages/sqlite-vfs-test/tests/sqlite-vfs.test.ts index a07a7029a2..532ec2cbad 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/tests/sqlite-vfs.test.ts +++ b/rivetkit-typescript/packages/sqlite-vfs-test/tests/sqlite-vfs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { SqliteVfs, type KvVfsOptions } from "../src/index"; +import { createSqliteVfs } from "../src/backend"; +import type { KvVfsOptions } from "@rivetkit/sqlite-vfs"; function keyToString(key: Uint8Array): string { return Buffer.from(key).toString("hex"); @@ -39,7 +40,7 @@ describe("sqlite-vfs", () => { it("persists data across VFS instances", async () => { const kvStore = createKvStore(); - const vfs = new SqliteVfs(); + const vfs = await createSqliteVfs(); const db = await vfs.open("actor-1", kvStore); await db.exec( "CREATE TABLE IF NOT EXISTS test_data (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL)", @@ -48,7 +49,7 @@ describe("sqlite-vfs", () => { await db.exec("INSERT INTO test_data (value) VALUES ('beta')"); await db.close(); - const vfsReloaded = new SqliteVfs(); + const vfsReloaded = await createSqliteVfs(); const dbReloaded = await vfsReloaded.open("actor-1", kvStore); const values: string[] = []; diff --git a/rivetkit-typescript/packages/sqlite-vfs-test/tsconfig.json b/rivetkit-typescript/packages/sqlite-vfs-test/tsconfig.json new file mode 100644 index 0000000000..c99638e62d --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src/**/*", "tests/**/*"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs-win32-x64/index.js b/rivetkit-typescript/packages/sqlite-vfs-win32-x64/index.js new file mode 100644 index 0000000000..43842be49f --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-win32-x64/index.js @@ -0,0 +1,9 @@ +const path = require("node:path"); + +const bindingPath = path.join( + __dirname, + "bin", + "rivetkit_sqlite_vfs_native.node", +); + +module.exports = require(bindingPath); diff --git a/rivetkit-typescript/packages/sqlite-vfs-win32-x64/package.json b/rivetkit-typescript/packages/sqlite-vfs-win32-x64/package.json new file mode 100644 index 0000000000..631550913e --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs-win32-x64/package.json @@ -0,0 +1,11 @@ +{ + "name": "@rivetkit/sqlite-vfs-win32-x64", + "version": "0.0.1", + "description": "Native sqlite-vfs binary for Windows x64", + "license": "Apache-2.0", + "main": "index.js", + "type": "commonjs", + "os": ["win32"], + "cpu": ["x64"], + "files": ["index.js", "bin"] +} diff --git a/rivetkit-typescript/packages/sqlite-vfs/package.json b/rivetkit-typescript/packages/sqlite-vfs/package.json index 58c2baa3ed..2e92275f4f 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/package.json +++ b/rivetkit-typescript/packages/sqlite-vfs/package.json @@ -20,24 +20,53 @@ "types": "./dist/tsup/index.d.cts", "default": "./dist/tsup/index.cjs" } + }, + "./native": { + "import": { + "types": "./dist/tsup/native.d.ts", + "default": "./dist/tsup/native.js" + }, + "require": { + "types": "./dist/tsup/native.d.cts", + "default": "./dist/tsup/native.cjs" + } + }, + "./wasm": { + "import": { + "types": "./dist/tsup/wasm.d.ts", + "default": "./dist/tsup/wasm.js" + }, + "require": { + "types": "./dist/tsup/wasm.d.cts", + "default": "./dist/tsup/wasm.cjs" + } } }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "scripts": { - "build": "pnpm run compile:bare && tsup src/index.ts", + "build": "pnpm run compile:bare && tsup src/index.ts src/native.ts src/wasm.ts", + "build:native": "node scripts/build-native.js", "compile:bare": "tsx scripts/compile-bare.ts compile schemas/file-meta/v1.bare -o dist/schemas/file-meta/v1.ts", "check-types": "pnpm run compile:bare && tsc --noEmit", - "test": "pnpm run compile:bare && vitest run" + "test": "pnpm --filter @rivetkit/sqlite-vfs-test test" }, "dependencies": { "@rivetkit/bare-ts": "^0.6.2", - "vbare": "^0.0.4", - "wa-sqlite": "^1.0.0" + "sql.js": "^1.10.2", + "vbare": "^0.0.4" + }, + "optionalDependencies": { + "@rivetkit/sqlite-vfs-darwin-arm64": "workspace:*", + "@rivetkit/sqlite-vfs-darwin-x64": "workspace:*", + "@rivetkit/sqlite-vfs-linux-arm64": "workspace:*", + "@rivetkit/sqlite-vfs-linux-x64": "workspace:*", + "@rivetkit/sqlite-vfs-win32-x64": "workspace:*" }, "devDependencies": { "@bare-ts/tools": "^0.13.0", + "@types/sql.js": "^1.4.9", "@types/node": "^22.13.1", "commander": "^12.0.0", "tsx": "^4.7.0", diff --git a/rivetkit-typescript/packages/sqlite-vfs/scripts/build-native.js b/rivetkit-typescript/packages/sqlite-vfs/scripts/build-native.js new file mode 100644 index 0000000000..faa0743fbb --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/scripts/build-native.js @@ -0,0 +1,53 @@ +import { execFileSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync } from "node:fs"; +import { dirname, join, resolve, delimiter } from "node:path"; +import { fileURLToPath } from "node:url"; +import { arch, platform } from "node:process"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, "../../../.."); +const rustRoot = resolve(repoRoot, "rivetkit-rust"); +const targetDir = resolve(rustRoot, "target", "debug"); + +const candidates = []; +if (process.env.CARGO) { + candidates.push(process.env.CARGO); +} +if (process.env.HOME) { + candidates.push(resolve(process.env.HOME, ".cargo", "bin", "cargo")); +} +const pathEntries = (process.env.PATH ?? "").split(delimiter); +for (const entry of pathEntries) { + if (entry) { + candidates.push(resolve(entry, "cargo")); + } +} + +const cargoPath = candidates.find((candidate) => existsSync(candidate)); +if (!cargoPath) { + throw new Error("cargo not found in PATH or default locations"); +} + +execFileSync(cargoPath, ["build", "-p", "rivetkit-sqlite-vfs-native"], { + cwd: rustRoot, + stdio: "inherit", +}); + +let libName = "librivetkit_sqlite_vfs_native.so"; +if (platform === "darwin") { + libName = "librivetkit_sqlite_vfs_native.dylib"; +} else if (platform === "win32") { + libName = "rivetkit_sqlite_vfs_native.dll"; +} + +const packageDir = resolve( + repoRoot, + "rivetkit-typescript", + "packages", + `sqlite-vfs-${platform}-${arch}`, +); +const destDir = join(packageDir, "bin"); +const destFile = join(destDir, "rivetkit_sqlite_vfs_native.node"); + +mkdirSync(destDir, { recursive: true }); +copyFileSync(join(targetDir, libName), destFile); diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/index.ts b/rivetkit-typescript/packages/sqlite-vfs/src/index.ts index 5180ce6149..54871e2aed 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/src/index.ts +++ b/rivetkit-typescript/packages/sqlite-vfs/src/index.ts @@ -1,651 +1,2 @@ -/** - * SQLite raw database with KV storage backend - * - * This module provides a SQLite API that uses a KV-backed VFS - * for storage. Each SqliteVfs instance is independent and can be - * used concurrently with other instances. - */ - -// Note: wa-sqlite VFS.Base type definitions have incorrect types for xRead/xWrite -// The actual runtime uses Uint8Array, not the {size, value} object shown in types -import * as VFS from "wa-sqlite/src/VFS.js"; - -// VFS debug logging - set VFS_DEBUG=1 to enable -const VFS_DEBUG = process.env.VFS_DEBUG === "1"; -function vfsLog(op: string, details: Record) { - if (VFS_DEBUG) { - console.log(`[VFS] ${op}`, JSON.stringify(details)); - } -} -import SQLiteESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs"; -import { Factory } from "wa-sqlite"; -import { readFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { CHUNK_SIZE, getMetaKey, getChunkKey } from "./kv"; -import { - FILE_META_VERSIONED, - CURRENT_VERSION, -} from "../schemas/file-meta/versioned.js"; -import type { FileMeta } from "../schemas/file-meta/mod.js"; - -/** - * Options for creating the KV VFS - * Operations are scoped to a specific actor's KV store - */ -export interface KvVfsOptions { - /** Get a single value by key. Returns null if missing. */ - get: (key: Uint8Array) => Promise; - /** Get multiple values by keys. Returns null for missing keys. */ - getBatch: (keys: Uint8Array[]) => Promise<(Uint8Array | null)[]>; - /** Put a single key-value pair */ - put: (key: Uint8Array, value: Uint8Array) => Promise; - /** Put multiple key-value pairs */ - putBatch: (entries: [Uint8Array, Uint8Array][]) => Promise; - /** Delete multiple keys */ - deleteBatch: (keys: Uint8Array[]) => Promise; -} - -/** - * Represents an open file - */ -interface OpenFile { - /** File path */ - path: string; - /** File size in bytes */ - size: number; - /** Open flags */ - flags: number; - /** KV options for this file */ - options: KvVfsOptions; -} - -/** - * Encodes file metadata to a Uint8Array using BARE schema - */ -function encodeFileMeta(size: number): Uint8Array { - const meta: FileMeta = { size: BigInt(size) }; - return FILE_META_VERSIONED.serializeWithEmbeddedVersion( - meta, - CURRENT_VERSION, - ); -} - -/** - * Decodes file metadata from a Uint8Array using BARE schema - */ -function decodeFileMeta(data: Uint8Array): number { - const meta = FILE_META_VERSIONED.deserializeWithEmbeddedVersion(data); - return Number(meta.size); -} - -/** - * SQLite API interface (subset needed for VFS registration) - * This is part of wa-sqlite but not exported in TypeScript types - */ -interface SQLite3Api { - vfs_register: (vfs: unknown, makeDefault?: boolean) => number; - open_v2: ( - filename: string, - flags: number, - vfsName?: string, - ) => Promise; - close: (db: number) => Promise; - exec: ( - db: number, - sql: string, - callback?: (row: unknown[], columns: string[]) => void, - ) => Promise; - SQLITE_OPEN_READWRITE: number; - SQLITE_OPEN_CREATE: number; -} - -/** - * Simple async mutex for serializing database operations - * wa-sqlite is not safe for concurrent open_v2 calls - */ -class AsyncMutex { - #locked = false; - #waiting: (() => void)[] = []; - - async acquire(): Promise { - while (this.#locked) { - await new Promise((resolve) => this.#waiting.push(resolve)); - } - this.#locked = true; - } - - release(): void { - this.#locked = false; - const next = this.#waiting.shift(); - if (next) { - next(); - } - } - - async run(fn: () => Promise): Promise { - await this.acquire(); - try { - return await fn(); - } finally { - this.release(); - } - } -} - -/** - * Database wrapper that provides a simplified SQLite API - */ -export class Database { - readonly #sqlite3: SQLite3Api; - readonly #handle: number; - readonly #fileName: string; - readonly #onClose: () => void; - readonly #mutex: AsyncMutex; - - constructor( - sqlite3: SQLite3Api, - handle: number, - fileName: string, - onClose: () => void, - mutex: AsyncMutex, - ) { - this.#sqlite3 = sqlite3; - this.#handle = handle; - this.#fileName = fileName; - this.#onClose = onClose; - this.#mutex = mutex; - } - - /** - * Execute SQL with optional row callback - * @param sql - SQL statement to execute - * @param callback - Called for each result row with (row, columns) where row is an array of values and columns is an array of column names - */ - async exec(sql: string, callback?: (row: unknown[], columns: string[]) => void): Promise { - return this.#mutex.run(async () => { - return this.#sqlite3.exec(this.#handle, sql, callback); - }); - } - - /** - * Close the database - */ - async close(): Promise { - await this.#mutex.run(async () => { - await this.#sqlite3.close(this.#handle); - }); - this.#onClose(); - } - - /** - * Get the raw wa-sqlite API (for advanced usage) - */ - get sqlite3(): SQLite3Api { - return this.#sqlite3; - } - - /** - * Get the raw database handle (for advanced usage) - */ - get handle(): number { - return this.#handle; - } -} - -/** - * SQLite VFS backed by KV storage. - * - * Each instance is independent and has its own wa-sqlite WASM module. - * This allows multiple instances to operate concurrently without interference. - */ -export class SqliteVfs { - #sqlite3: SQLite3Api | null = null; - #sqliteSystem: SqliteSystem | null = null; - #initPromise: Promise | null = null; - #operationMutex = new AsyncMutex(); - #instanceId: string; - - constructor() { - // Generate unique instance ID for VFS name - this.#instanceId = crypto.randomUUID().replace(/-/g, "").slice(0, 8); - } - - /** - * Initialize wa-sqlite and VFS (called once per instance) - */ - async #ensureInitialized(): Promise { - // Fast path: already initialized - if (this.#sqlite3 && this.#sqliteSystem) { - return; - } - - // Synchronously create the promise if not started - if (!this.#initPromise) { - this.#initPromise = (async () => { - // Load WASM binary (Node.js environment) - const require = createRequire(import.meta.url); - const wasmPath = require.resolve("wa-sqlite/dist/wa-sqlite-async.wasm"); - const wasmBinary = readFileSync(wasmPath); - - // Initialize wa-sqlite module - each instance gets its own module - const module = await SQLiteESMFactory({ wasmBinary }); - this.#sqlite3 = Factory(module) as SQLite3Api; - - // Create and register VFS with unique name - this.#sqliteSystem = new SqliteSystem(this.#sqlite3, `kv-vfs-${this.#instanceId}`); - this.#sqliteSystem.register(); - })(); - } - - // Wait for initialization - await this.#initPromise; - } - - /** - * Open a SQLite database using KV storage backend - * - * @param fileName - The database file name (typically the actor ID) - * @param options - KV storage operations for this database - * @returns A Database instance - */ - async open( - fileName: string, - options: KvVfsOptions, - ): Promise { - return this.#operationMutex.run(async () => { - // Initialize wa-sqlite and SqliteSystem on first call - await this.#ensureInitialized(); - - if (!this.#sqlite3 || !this.#sqliteSystem) { - throw new Error("SQLite not initialized"); - } - - // Register the file with its KV options - this.#sqliteSystem.registerFile(fileName, options); - - const db = await this.#sqlite3.open_v2( - fileName, - this.#sqlite3.SQLITE_OPEN_READWRITE | - this.#sqlite3.SQLITE_OPEN_CREATE, - this.#sqliteSystem.name, - ); - - const sqliteSystem = this.#sqliteSystem; - const onClose = () => { - sqliteSystem.unregisterFile(fileName); - }; - - return new Database( - this.#sqlite3, - db, - fileName, - onClose, - this.#operationMutex, - ); - }); - } -} - -/** - * KV-backed VFS implementation for wa-sqlite - */ -class SqliteSystem extends (VFS.Base as typeof VFS.Base) { - readonly #sqlite3: SQLite3Api; - readonly #name: string; - readonly #openFiles: Map = new Map(); - readonly #fileOptions: Map = new Map(); - #nextFileId = 1; - - constructor(sqlite3: SQLite3Api, name: string) { - super(); - this.#sqlite3 = sqlite3; - this.#name = name; - } - - get name(): string { - return this.#name; - } - - register(): void { - this.#sqlite3.vfs_register(this, false); - } - - registerFile(fileName: string, options: KvVfsOptions): void { - this.#fileOptions.set(fileName, options); - } - - unregisterFile(fileName: string): void { - this.#fileOptions.delete(fileName); - } - - #getOptionsForPath(path: string): KvVfsOptions | undefined { - const direct = this.#fileOptions.get(path); - if (direct) { - return direct; - } - - const suffixes = ["-journal", "-wal", "-shm"]; - for (const suffix of suffixes) { - if (path.endsWith(suffix)) { - const basePath = path.slice(0, -suffix.length); - const baseOptions = this.#fileOptions.get(basePath); - if (baseOptions) { - return baseOptions; - } - } - } - - return undefined; - } - - // @ts-expect-error - wa-sqlite types are incorrect - xOpen( - name: string, - fileId: number, - flags: number, - pOutFlags: DataView, - ): number { - return this.handleAsync(async () => { - try { - const resolvedName = name && name.length > 0 ? name : `temp-${fileId}`; - let options = this.#getOptionsForPath(resolvedName); - if (!options && this.#fileOptions.size === 1) { - options = this.#fileOptions.values().next().value; - } - if (!options) { - throw new Error(`File not registered: ${resolvedName}`); - } - - if (VFS_DEBUG) { - vfsLog("xOpen", { file: resolvedName, flags }); - } - - const key = getMetaKey(resolvedName); - const metaData = await options.get(key); - let size = 0; - - if (metaData) { - size = decodeFileMeta(metaData); - } - - const file: OpenFile = { - path: resolvedName, - size, - flags, - options, - }; - - this.#openFiles.set(fileId, file); - pOutFlags.setInt32(0, flags, true); - return VFS.SQLITE_OK; - } catch (error) { - vfsLog("xOpen", { - file: name, - error: String(error), - }); - return VFS.SQLITE_CANTOPEN; - } - }); - } - - // @ts-expect-error - wa-sqlite types are incorrect - xClose(fileId: number): number { - this.#openFiles.delete(fileId); - return VFS.SQLITE_OK; - } - - // @ts-expect-error - wa-sqlite types are incorrect - xRead( - fileId: number, - pData: Uint8Array, - iOffset: number, - iAmt: number, - ): number { - return this.handleAsync(async () => { - const file = this.#openFiles.get(fileId); - if (!file) { - return VFS.SQLITE_IOERR; - } - - const offsetRaw = - typeof iOffset === "bigint" ? Number(iOffset) : iOffset; - const amountRaw = typeof iAmt === "bigint" ? Number(iAmt) : iAmt; - const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0; - const amount = Number.isFinite(amountRaw) ? amountRaw : pData.length; - const readStart = performance.now(); - const chunkKeys: Uint8Array[] = []; - const startChunk = Math.floor(offset / CHUNK_SIZE); - const endChunk = Math.floor((offset + amount - 1) / CHUNK_SIZE); - - for (let chunkIndex = startChunk; chunkIndex <= endChunk; chunkIndex++) { - chunkKeys.push(getChunkKey(file.path, chunkIndex)); - } - - const chunks = await file.options.getBatch(chunkKeys); - - // Copy the requested data from the chunks into the buffer - let bytesCopied = 0; - for (let i = 0; i < chunks.length; i++) { - const chunkIndex = startChunk + i; - const chunkData = chunks[i]; - - const chunkStartOffset = chunkIndex * CHUNK_SIZE; - const readStartOffset = Math.max(offset - chunkStartOffset, 0); - const readEndOffset = Math.min( - CHUNK_SIZE, - offset + amount - chunkStartOffset, - ); - - const readLength = readEndOffset - readStartOffset; - if (readLength <= 0) { - continue; - } - - if (chunkData) { - pData.set( - chunkData.subarray(readStartOffset, readEndOffset), - bytesCopied, - ); - } else { - // If chunk missing, fill with zeros - pData.fill(0, bytesCopied, bytesCopied + readLength); - } - - bytesCopied += readLength; - } - - if (VFS_DEBUG) { - vfsLog("xRead", { - file: file.path, - offset, - len: amount, - chunks: chunkKeys.length, - ms: (performance.now() - readStart).toFixed(2), - }); - } - - if (offset + amount > file.size) { - return VFS.SQLITE_IOERR_SHORT_READ; - } - - return VFS.SQLITE_OK; - }); - } - - // @ts-expect-error - wa-sqlite types are incorrect - xWrite( - fileId: number, - pData: Uint8Array, - iOffset: number, - iAmt: number, - ): number { - return this.handleAsync(async () => { - const file = this.#openFiles.get(fileId); - if (!file) { - return VFS.SQLITE_IOERR; - } - - const offsetRaw = - typeof iOffset === "bigint" ? Number(iOffset) : iOffset; - const amountRaw = typeof iAmt === "bigint" ? Number(iAmt) : iAmt; - const offset = Number.isFinite(offsetRaw) ? offsetRaw : 0; - const amount = Number.isFinite(amountRaw) ? amountRaw : pData.length; - const writeStart = performance.now(); - const chunkKeys: Uint8Array[] = []; - const startChunk = Math.floor(offset / CHUNK_SIZE); - const endChunk = Math.floor((offset + amount - 1) / CHUNK_SIZE); - - for (let chunkIndex = startChunk; chunkIndex <= endChunk; chunkIndex++) { - chunkKeys.push(getChunkKey(file.path, chunkIndex)); - } - - const getBatchStart = performance.now(); - const chunks = await file.options.getBatch(chunkKeys); - const getBatchMs = performance.now() - getBatchStart; - - const entriesToWrite: [Uint8Array, Uint8Array][] = []; - - // Update each chunk with the new data - let bytesWritten = 0; - for (let i = 0; i < chunks.length; i++) { - const chunkIndex = startChunk + i; - const chunkData = chunks[i]; - - const chunkStartOffset = chunkIndex * CHUNK_SIZE; - const writeStartOffset = Math.max(offset - chunkStartOffset, 0); - const writeEndOffset = Math.min( - CHUNK_SIZE, - offset + amount - chunkStartOffset, - ); - - const writeLength = writeEndOffset - writeStartOffset; - if (writeLength <= 0) { - continue; - } - - // Create or clone the chunk data - const newChunkData = chunkData - ? new Uint8Array(chunkData) - : new Uint8Array(CHUNK_SIZE); - - // Copy data into the chunk - newChunkData.set( - pData.subarray(bytesWritten, bytesWritten + writeLength), - writeStartOffset, - ); - - entriesToWrite.push([chunkKeys[i], newChunkData]); - bytesWritten += writeLength; - } - - const putBatchStart = performance.now(); - await file.options.putBatch(entriesToWrite); - const putBatchMs = performance.now() - putBatchStart; - - // Update file size if needed - const newSize = Math.max(file.size, offset + amount); - if (newSize !== file.size) { - file.size = newSize; - const metaKey = getMetaKey(file.path); - const metaData = encodeFileMeta(newSize); - await file.options.put(metaKey, metaData); - } - - if (VFS_DEBUG) { - vfsLog("xWrite", { - file: file.path, - offset, - len: amount, - readChunks: chunkKeys.length, - writeEntries: entriesToWrite.length, - getBatchMs: getBatchMs.toFixed(2), - putBatchMs: putBatchMs.toFixed(2), - ms: (performance.now() - writeStart).toFixed(2), - }); - } - - return VFS.SQLITE_OK; - }); - } - - // @ts-expect-error - wa-sqlite types are incorrect - xTruncate(fileId: number, size: number): number { - const file = this.#openFiles.get(fileId); - if (!file) { - return VFS.SQLITE_IOERR; - } - - const nextSize = typeof size === "bigint" ? Number(size) : size; - file.size = Number.isFinite(nextSize) ? nextSize : 0; - return VFS.SQLITE_OK; - } - - // @ts-expect-error - wa-sqlite types are incorrect - xSync(fileId: number): number { - return this.handleAsync(async () => { - const file = this.#openFiles.get(fileId); - if (!file) { - return VFS.SQLITE_IOERR; - } - - // Update metadata - const metaKey = getMetaKey(file.path); - const metaData = encodeFileMeta(file.size); - await file.options.put(metaKey, metaData); - - return VFS.SQLITE_OK; - }); - } - - // @ts-expect-error - wa-sqlite types are incorrect - xFileSize(fileId: number, pSize64: DataView): number { - const file = this.#openFiles.get(fileId); - if (!file) { - pSize64.setBigInt64(0, BigInt(0), true); - return VFS.SQLITE_OK; - } - - pSize64.setBigInt64(0, BigInt(file.size), true); - return VFS.SQLITE_OK; - } - - // @ts-expect-error - wa-sqlite types are incorrect - xDelete(name: string, _syncDir: number): number { - // In a KV store, we can't easily delete all chunks without scanning - // For now, we'll just remove the metadata - const options = this.#getOptionsForPath(name); - if (options) { - const metaKey = getMetaKey(name); - void options.deleteBatch([metaKey]); - } - return VFS.SQLITE_OK; - } - - // @ts-expect-error - wa-sqlite types are incorrect - xAccess( - name: string, - _flags: number, - pResOut: DataView, - ): number { - return this.handleAsync(async () => { - let options = this.#getOptionsForPath(name); - if (!options && this.#fileOptions.size === 1) { - options = this.#fileOptions.values().next().value; - } - if (!options) { - pResOut.setInt32(0, 0, true); - return VFS.SQLITE_OK; - } - - if (_flags === VFS.SQLITE_ACCESS_EXISTS) { - const metaKey = getMetaKey(name); - const metaData = await options.get(metaKey); - pResOut.setInt32(0, metaData ? 1 : 0, true); - return VFS.SQLITE_OK; - } - - pResOut.setInt32(0, 1, true); - return VFS.SQLITE_OK; - }); - } -} +export { SqliteVfs, Database } from "./wasm"; +export type { KvVfsOptions, SqliteVfsConfig } from "./types"; diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/kv.ts b/rivetkit-typescript/packages/sqlite-vfs/src/kv.ts index 3ff27a7eff..104b555839 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/src/kv.ts +++ b/rivetkit-typescript/packages/sqlite-vfs/src/kv.ts @@ -5,12 +5,9 @@ * key-value store for SQLite file storage. */ -/** Size of each chunk stored in KV (4KB) */ +/** Default size of each chunk stored in KV (4KB) */ export const CHUNK_SIZE = 4096; -/** Top-level SQLite prefix (must match SQLITE_PREFIX in actor KV system) */ -export const SQLITE_PREFIX = 9; - /** Key prefix byte for file metadata (after SQLITE_PREFIX) */ export const META_PREFIX = 0; @@ -21,11 +18,11 @@ export const CHUNK_PREFIX = 1; * Gets the key for file metadata * Format: [SQLITE_PREFIX (1 byte), META_PREFIX (1 byte), filename (UTF-8 encoded)] */ -export function getMetaKey(fileName: string): Uint8Array { +export function getMetaKey(fileName: string, prefix: number): Uint8Array { const encoder = new TextEncoder(); const fileNameBytes = encoder.encode(fileName); const key = new Uint8Array(2 + fileNameBytes.length); - key[0] = SQLITE_PREFIX; + key[0] = prefix; key[1] = META_PREFIX; key.set(fileNameBytes, 2); return key; @@ -35,11 +32,15 @@ export function getMetaKey(fileName: string): Uint8Array { * Gets the key for a file chunk * Format: [SQLITE_PREFIX (1 byte), CHUNK_PREFIX (1 byte), filename (UTF-8), null separator (1 byte), chunk index (4 bytes, big-endian)] */ -export function getChunkKey(fileName: string, chunkIndex: number): Uint8Array { +export function getChunkKey( + fileName: string, + chunkIndex: number, + prefix: number, +): Uint8Array { const encoder = new TextEncoder(); const fileNameBytes = encoder.encode(fileName); const key = new Uint8Array(2 + fileNameBytes.length + 1 + 4); - key[0] = SQLITE_PREFIX; + key[0] = prefix; key[1] = CHUNK_PREFIX; key.set(fileNameBytes, 2); key[2 + fileNameBytes.length] = 0; // null separator diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/native-loader.ts b/rivetkit-typescript/packages/sqlite-vfs/src/native-loader.ts new file mode 100644 index 0000000000..55cd2328d1 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/src/native-loader.ts @@ -0,0 +1,30 @@ +import { createRequire } from "node:module"; +import { arch, platform } from "node:process"; + +type NativeBinding = { + NativeDatabase: new (bytes?: Uint8Array) => unknown; +}; + +const require = createRequire(import.meta.url); + +const PLATFORM_ARCH = `${platform}-${arch}`; +const CANDIDATES = [ + `@rivetkit/sqlite-vfs-${PLATFORM_ARCH}`, +]; + +export function loadNativeBinding(): NativeBinding { + for (const candidate of CANDIDATES) { + try { + return require(candidate) as NativeBinding; + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code !== "MODULE_NOT_FOUND") { + throw error; + } + } + } + + throw new Error( + `native sqlite-vfs addon not found for ${platform}-${arch}`, + ); +} diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/native.ts b/rivetkit-typescript/packages/sqlite-vfs/src/native.ts new file mode 100644 index 0000000000..66fefbd01d --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/src/native.ts @@ -0,0 +1,144 @@ +import { loadNativeBinding } from "./native-loader"; +import { + loadDatabaseBytes, + persistDatabaseBytes, + resolveChunkSize, + resolveKvPrefix, +} from "./storage"; +import type { KvVfsOptions, SqliteVfsConfig } from "./types"; + +type NativeBinding = { + NativeDatabase: new (bytes?: Uint8Array) => NativeDatabase; +}; + +type NativeDatabase = { + exec: ( + sql: string, + callback?: (row: unknown[], columns: string[]) => void, + ) => void; + export: () => Uint8Array; + close: () => void; +}; + +class AsyncMutex { + #locked = false; + #waiting: (() => void)[] = []; + + async acquire(): Promise { + while (this.#locked) { + await new Promise((resolve) => this.#waiting.push(resolve)); + } + this.#locked = true; + } + + release(): void { + this.#locked = false; + const next = this.#waiting.shift(); + if (next) { + next(); + } + } + + async run(fn: () => Promise): Promise { + await this.acquire(); + try { + return await fn(); + } finally { + this.release(); + } + } +} + +export class Database { + readonly #db: NativeDatabase; + readonly #fileName: string; + readonly #options: KvVfsOptions; + readonly #mutex: AsyncMutex; + readonly #chunkSize: number; + readonly #kvPrefix: number; + + constructor( + db: NativeDatabase, + fileName: string, + options: KvVfsOptions, + mutex: AsyncMutex, + chunkSize: number, + kvPrefix: number, + ) { + this.#db = db; + this.#fileName = fileName; + this.#options = options; + this.#mutex = mutex; + this.#chunkSize = chunkSize; + this.#kvPrefix = kvPrefix; + } + + async exec( + sql: string, + callback?: (row: unknown[], columns: string[]) => void, + ): Promise { + await this.#mutex.run(async () => { + this.#db.exec(sql, callback); + const bytes = this.#db.export(); + await persistDatabaseBytes( + this.#fileName, + this.#options, + this.#chunkSize, + this.#kvPrefix, + bytes, + ); + }); + } + + async close(): Promise { + await this.#mutex.run(async () => { + const bytes = this.#db.export(); + await persistDatabaseBytes( + this.#fileName, + this.#options, + this.#chunkSize, + this.#kvPrefix, + bytes, + ); + this.#db.close(); + }); + } +} + +export class SqliteVfs { + #binding: NativeBinding; + #mutex = new AsyncMutex(); + #chunkSize: number; + #kvPrefix: number; + + constructor(config: SqliteVfsConfig) { + this.#binding = loadNativeBinding() as NativeBinding; + this.#chunkSize = resolveChunkSize(config.chunkSize); + this.#kvPrefix = resolveKvPrefix(config.kvPrefix); + } + + async open(fileName: string, options: KvVfsOptions): Promise { + return this.#mutex.run(async () => { + const bytes = await loadDatabaseBytes( + fileName, + options, + this.#chunkSize, + this.#kvPrefix, + ); + const db = bytes + ? new this.#binding.NativeDatabase(bytes) + : new this.#binding.NativeDatabase(); + + return new Database( + db, + fileName, + options, + this.#mutex, + this.#chunkSize, + this.#kvPrefix, + ); + }); + } +} + +export type { KvVfsOptions, SqliteVfsConfig }; diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/storage.ts b/rivetkit-typescript/packages/sqlite-vfs/src/storage.ts new file mode 100644 index 0000000000..ac2268f6a9 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/src/storage.ts @@ -0,0 +1,115 @@ +import { CHUNK_SIZE, getChunkKey, getMetaKey } from "./kv"; +import { FILE_META_VERSIONED, CURRENT_VERSION } from "../schemas/file-meta/versioned.js"; +import type { FileMeta } from "../schemas/file-meta/mod.js"; +import type { KvVfsOptions } from "./types"; + +function encodeFileMeta(size: number): Uint8Array { + const meta: FileMeta = { size: BigInt(size) }; + return FILE_META_VERSIONED.serializeWithEmbeddedVersion( + meta, + CURRENT_VERSION, + ); +} + +function decodeFileMeta(data: Uint8Array): number { + const meta = FILE_META_VERSIONED.deserializeWithEmbeddedVersion(data); + return Number(meta.size); +} + +export async function loadDatabaseBytes( + fileName: string, + options: KvVfsOptions, + chunkSize: number, + kvPrefix: number, +): Promise { + const metaKey = getMetaKey(fileName, kvPrefix); + const metaData = await options.get(metaKey); + if (!metaData) { + return null; + } + + const size = decodeFileMeta(metaData); + if (size <= 0) { + return new Uint8Array(0); + } + + const totalChunks = Math.ceil(size / chunkSize); + const chunkKeys: Uint8Array[] = []; + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { + chunkKeys.push(getChunkKey(fileName, chunkIndex, kvPrefix)); + } + + const chunks = await options.getBatch(chunkKeys); + const buffer = new Uint8Array(size); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const offset = i * chunkSize; + const remaining = size - offset; + const length = Math.min(chunkSize, remaining); + + if (chunk) { + buffer.set(chunk.subarray(0, length), offset); + } else { + buffer.fill(0, offset, offset + length); + } + } + + return buffer; +} + +export async function persistDatabaseBytes( + fileName: string, + options: KvVfsOptions, + chunkSize: number, + kvPrefix: number, + bytes: Uint8Array, +): Promise { + const size = bytes.length; + const metaKey = getMetaKey(fileName, kvPrefix); + const existingMeta = await options.get(metaKey); + const existingSize = existingMeta ? decodeFileMeta(existingMeta) : 0; + + const newChunkCount = Math.ceil(size / chunkSize); + const oldChunkCount = Math.ceil(existingSize / chunkSize); + + const entries: [Uint8Array, Uint8Array][] = []; + for (let chunkIndex = 0; chunkIndex < newChunkCount; chunkIndex++) { + const start = chunkIndex * chunkSize; + const end = Math.min(start + chunkSize, size); + const chunk = bytes.subarray(start, end); + entries.push([getChunkKey(fileName, chunkIndex, kvPrefix), chunk]); + } + + if (entries.length > 0) { + await options.putBatch(entries); + } + + const metaData = encodeFileMeta(size); + await options.put(metaKey, metaData); + + if (oldChunkCount > newChunkCount) { + const deleteKeys: Uint8Array[] = []; + for (let chunkIndex = newChunkCount; chunkIndex < oldChunkCount; chunkIndex++) { + deleteKeys.push(getChunkKey(fileName, chunkIndex, kvPrefix)); + } + if (deleteKeys.length > 0) { + await options.deleteBatch(deleteKeys); + } + } +} + +export function resolveChunkSize(chunkSize?: number): number { + const resolved = chunkSize ?? CHUNK_SIZE; + if (!Number.isInteger(resolved) || resolved <= 0) { + throw new Error("chunkSize must be a positive integer"); + } + return resolved; +} + +export function resolveKvPrefix(kvPrefix: number): number { + if (!Number.isInteger(kvPrefix) || kvPrefix < 0 || kvPrefix > 255) { + throw new Error("kvPrefix must be an integer between 0 and 255"); + } + return kvPrefix; +} diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/types.ts b/rivetkit-typescript/packages/sqlite-vfs/src/types.ts new file mode 100644 index 0000000000..e685edbf22 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/src/types.ts @@ -0,0 +1,19 @@ +export interface KvVfsOptions { + /** Get a single value by key. Returns null if missing. */ + get: (key: Uint8Array) => Promise; + /** Get multiple values by keys. Returns null for missing keys. */ + getBatch: (keys: Uint8Array[]) => Promise<(Uint8Array | null)[]>; + /** Put a single key-value pair */ + put: (key: Uint8Array, value: Uint8Array) => Promise; + /** Put multiple key-value pairs */ + putBatch: (entries: [Uint8Array, Uint8Array][]) => Promise; + /** Delete multiple keys */ + deleteBatch: (keys: Uint8Array[]) => Promise; +} + +export interface SqliteVfsConfig { + /** Prefix byte for SQLite VFS keys. Must match the actor KV keyspace prefix. */ + kvPrefix: number; + /** Chunk size in bytes for KV storage. Defaults to 4096. */ + chunkSize?: number; +} diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts b/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts deleted file mode 100644 index 4b8db17c9e..0000000000 --- a/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -declare module "wa-sqlite/src/VFS.js" { - export const SQLITE_OK: number; - export const SQLITE_IOERR: number; - export const SQLITE_IOERR_READ: number; - export const SQLITE_IOERR_SHORT_READ: number; - export const SQLITE_IOERR_WRITE: number; - export const SQLITE_IOERR_TRUNCATE: number; - export const SQLITE_IOERR_FSTAT: number; - export const SQLITE_CANTOPEN: number; - export const SQLITE_OPEN_CREATE: number; - export const SQLITE_OPEN_READONLY: number; - export const SQLITE_OPEN_DELETEONCLOSE: number; - export const SQLITE_NOTFOUND: number; - - /** - * Base class for SQLite VFS implementations. - * Extend this class and override methods to implement custom file systems. - */ - export class Base { - mxPathName: number; - - /** Close a file */ - xClose(fileId: number): number; - - /** Read data from a file */ - xRead(fileId: number, pData: Uint8Array, iOffset: number): number; - - /** Write data to a file */ - xWrite(fileId: number, pData: Uint8Array, iOffset: number): number; - - /** Truncate a file */ - xTruncate(fileId: number, iSize: number): number; - - /** Sync file data to storage */ - xSync(fileId: number, flags: number): number; - - /** Get file size */ - xFileSize(fileId: number, pSize64: DataView): number; - - /** Lock a file */ - xLock(fileId: number, flags: number): number; - - /** Unlock a file */ - xUnlock(fileId: number, flags: number): number; - - /** Check for reserved lock */ - xCheckReservedLock(fileId: number, pResOut: DataView): number; - - /** File control operations */ - xFileControl(fileId: number, op: number, pArg: DataView): number; - - /** Get sector size */ - xSectorSize(fileId: number): number; - - /** Get device characteristics */ - xDeviceCharacteristics(fileId: number): number; - - /** Open a file */ - xOpen( - name: string | null, - fileId: number, - flags: number, - pOutFlags: DataView, - ): number; - - /** Delete a file */ - xDelete(name: string, syncDir: number): number; - - /** Check file accessibility */ - xAccess(name: string, flags: number, pResOut: DataView): number; - - /** Handle asynchronous operations */ - handleAsync(fn: () => Promise): T; - } -} - -declare module "wa-sqlite/dist/wa-sqlite-async.mjs" { - const factory: (options?: { wasmBinary?: ArrayBuffer | Uint8Array }) => Promise; - export default factory; -} - -declare module "wa-sqlite" { - export function Factory(module: any): any; -} diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/wasm.ts b/rivetkit-typescript/packages/sqlite-vfs/src/wasm.ts new file mode 100644 index 0000000000..301147fc40 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-vfs/src/wasm.ts @@ -0,0 +1,178 @@ +import initSqlJs from "sql.js"; +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import { + loadDatabaseBytes, + persistDatabaseBytes, + resolveChunkSize, + resolveKvPrefix, +} from "./storage"; +import type { KvVfsOptions, SqliteVfsConfig } from "./types"; + +type SqlJsDatabase = InstanceType>["Database"]>; + +class AsyncMutex { + #locked = false; + #waiting: (() => void)[] = []; + + async acquire(): Promise { + while (this.#locked) { + await new Promise((resolve) => this.#waiting.push(resolve)); + } + this.#locked = true; + } + + release(): void { + this.#locked = false; + const next = this.#waiting.shift(); + if (next) { + next(); + } + } + + async run(fn: () => Promise): Promise { + await this.acquire(); + try { + return await fn(); + } finally { + this.release(); + } + } +} + +export class Database { + readonly #db: SqlJsDatabase; + readonly #fileName: string; + readonly #options: KvVfsOptions; + readonly #mutex: AsyncMutex; + readonly #chunkSize: number; + readonly #kvPrefix: number; + + constructor( + db: SqlJsDatabase, + fileName: string, + options: KvVfsOptions, + mutex: AsyncMutex, + chunkSize: number, + kvPrefix: number, + ) { + this.#db = db; + this.#fileName = fileName; + this.#options = options; + this.#mutex = mutex; + this.#chunkSize = chunkSize; + this.#kvPrefix = kvPrefix; + } + + async exec( + sql: string, + callback?: (row: unknown[], columns: string[]) => void, + ): Promise { + await this.#mutex.run(async () => { + const results = this.#db.exec(sql); + if (callback) { + for (const result of results) { + const columns = result.columns; + for (const row of result.values) { + callback(row, columns); + } + } + } + + const bytes = this.#db.export(); + await persistDatabaseBytes( + this.#fileName, + this.#options, + this.#chunkSize, + this.#kvPrefix, + bytes, + ); + }); + } + + async close(): Promise { + await this.#mutex.run(async () => { + const bytes = this.#db.export(); + await persistDatabaseBytes( + this.#fileName, + this.#options, + this.#chunkSize, + this.#kvPrefix, + bytes, + ); + this.#db.close(); + }); + } +} + +export class SqliteVfs { + #initPromise: Promise | null = null; + #sqlModule: Awaited> | null = null; + #mutex = new AsyncMutex(); + #chunkSize: number; + #kvPrefix: number; + + constructor(config: SqliteVfsConfig) { + this.#chunkSize = resolveChunkSize(config.chunkSize); + this.#kvPrefix = resolveKvPrefix(config.kvPrefix); + } + + async #ensureInitialized(): Promise { + if (this.#sqlModule) { + return; + } + + if (!this.#initPromise) { + this.#initPromise = (async () => { + const isNode = typeof process !== "undefined" && !!process.versions?.node; + if (isNode) { + const require = createRequire(import.meta.url); + const wasmPath = require.resolve("sql.js/dist/sql-wasm.wasm"); + const wasmBuffer = readFileSync(wasmPath); + const wasmBinary = wasmBuffer.buffer.slice( + wasmBuffer.byteOffset, + wasmBuffer.byteOffset + wasmBuffer.byteLength, + ); + this.#sqlModule = await initSqlJs({ wasmBinary }); + return; + } + + this.#sqlModule = await initSqlJs({ + locateFile: (file) => new URL(`./${file}`, import.meta.url).toString(), + }); + })(); + } + + await this.#initPromise; + } + + async open(fileName: string, options: KvVfsOptions): Promise { + return this.#mutex.run(async () => { + await this.#ensureInitialized(); + if (!this.#sqlModule) { + throw new Error("SQLite wasm not initialized"); + } + + const bytes = await loadDatabaseBytes( + fileName, + options, + this.#chunkSize, + this.#kvPrefix, + ); + const db = bytes + ? new this.#sqlModule.Database(bytes) + : new this.#sqlModule.Database(); + + return new Database( + db, + fileName, + options, + this.#mutex, + this.#chunkSize, + this.#kvPrefix, + ); + }); + } +} + +export type { KvVfsOptions, SqliteVfsConfig }; diff --git a/specs/sqlite-vfs-rust-wasm.md b/specs/sqlite-vfs-rust-wasm.md new file mode 100644 index 0000000000..93b03a3f8d --- /dev/null +++ b/specs/sqlite-vfs-rust-wasm.md @@ -0,0 +1,190 @@ +# Rust SQLite VFS With Native SQLite + Wasm Fallback + +## Summary + +Replace `@rivetkit/sqlite-vfs` with a Rust-backed implementation that uses embedded, official SQLite in the native dylib build and a wasm build as a fallback. The package must load a native dylib in Node.js and Bun when available, and fall back to wasm on other platforms. The public JS API remains stable so existing callers in `rivetkit/db` continue to work unchanged. + +## Goals + +- Provide a Rust implementation of the SQLite VFS logic while keeping the existing JS API surface. +- Use embedded, official SQLite in the native dylib build. +- Use SQLite as wasm for fallback platforms. +- Support Node.js and Bun via a native addon (dylib) that internally hosts the sqlite-wasm runtime. +- Provide a wasm fallback path for unsupported platforms. +- Preserve the current KV key format, chunking, and metadata schema so existing data remains valid. +- Allow `CHUNK_SIZE` to be configured, defaulting to 4096. +- Provide a dedicated test wrapper package for running vitest tests against an in-memory KV driver. + +## Non-goals + +- Changing the public `SqliteVfs` API or `KvVfsOptions` shape in a breaking way. +- Introducing new storage formats or automatic migrations. +- Adding full multi-process WAL or shared-memory support beyond what exists today. +- Replacing or changing actor KV semantics outside the VFS boundary. + +## Current Behavior to Preserve + +- KV key format and constants in `rivetkit-typescript/packages/sqlite-vfs/src/kv.ts`. +- Chunk size is 4096 bytes. +- File metadata schema is `FileMeta { size: u64 }`. +- WAL and shared-memory operations are not implemented in the current VFS. Behavior should remain equivalent. +- The JS API exposes `SqliteVfs.open(fileName, options)` returning a `Database` with `exec` and `close`. + +## Requirements + +- Must embed SQLite as wasm. +- Must support native dylib loading in Node.js and Bun. +- Must provide wasm fallback for other platforms. +- Must preserve deterministic KV behavior across runtimes. +- Must keep `@rivetkit/sqlite-vfs` as the published entrypoint for callers. + +## Proposed Architecture + +### High-Level Components + +- `rivetkit-sqlite-vfs-core` (Rust crate) + - Implements the KV-backed VFS logic and SQLite host bindings. + - Defines a Rust trait for KV operations that mirrors `KvVfsOptions`. + - Owns key formatting, chunking, and metadata encode/decode. + +- `rivetkit-sqlite-vfs-wasm` (Rust crate) + - Builds the SQLite wasm module with async host call support. + - Exposes a wasm-friendly interface for KV callbacks. + +- `rivetkit-sqlite-vfs-native` (Rust crate) + - N-API addon that embeds the official SQLite amalgamation. + - Exposes the JS API and bridges to KV callbacks from JS. + +- `@rivetkit/sqlite-vfs` (JS package) + - Minimal wrappers for native and wasm bindings. + - Exposes `./native` and `./wasm` entrypoints. + - The runtime selection and KV binding logic lives in `rivetkit/db` to reduce glue code. + +- `@rivetkit/sqlite-vfs-test` (JS package) + - Vitest wrapper package that runs the sqlite-vfs test suite against an in-memory KV driver. + - Can target either native or wasm backend via environment selection. + +### Runtime Flow + +- `rivetkit/db` imports the sqlite-vfs native binding when available, otherwise imports the wasm wrapper directly. +- Both bindings expose the same minimal JS API that accepts `KvVfsOptions` and returns a `Database`. +- Runtime selection is centralized in `rivetkit/db` to minimize glue and keep the integration next to actor KV wiring. +- Optional debug override: allow an environment variable to force backend selection (see "Runtime Selection"). + +### SQLite Build + +- Native: + - Embed the official SQLite amalgamation directly in the native Rust crate. + - Do not link against system SQLite to preserve feature parity. +- Wasm: + - Build SQLite to wasm with async host call support to allow `getBatch` and `putBatch` to be awaited. + - Follow the same feature set as the wa-sqlite build for compatibility. + - The sqlite-wasm module must expose the VFS registration and open/exec APIs used today. + +## API and ABI Design + +### JavaScript API (Unchanged) + +- `class SqliteVfs` + - `open(fileName: string, options: KvVfsOptions): Promise` + +- `interface KvVfsOptions` + - `get(key: Uint8Array): Promise` + - `getBatch(keys: Uint8Array[]): Promise<(Uint8Array | null)[]>` + - `put(key: Uint8Array, value: Uint8Array): Promise` + - `putBatch(entries: [Uint8Array, Uint8Array][]): Promise` + - `deleteBatch(keys: Uint8Array[]): Promise` + +- `class Database` + - `exec(sql: string, callback?: (row: unknown[], columns: string[]) => void): Promise` + - `close(): Promise` + +### Rust KV Trait + +- `trait KvVfs` + - `get(&self, key: &[u8]) -> Future>>` + - `get_batch(&self, keys: Vec>) -> Future>>>` + - `put(&self, key: Vec, value: Vec) -> Future<()>` + - `put_batch(&self, entries: Vec<(Vec, Vec)>) -> Future<()>` + - `delete_batch(&self, keys: Vec>) -> Future<()>` + +### Key Encoding + +- Must match `rivetkit-typescript/packages/sqlite-vfs/src/kv.ts`. +- `SQLITE_PREFIX = 9`, `META_PREFIX = 0`, `CHUNK_PREFIX = 1`. +- Chunk index encoded as big-endian `u32`. + +### Chunk Size Configuration + +- Default `CHUNK_SIZE` is 4096 bytes (unchanged). +- Add an optional configuration parameter (e.g. `SqliteVfsConfig`) to allow overrides. +- The override affects new databases only. Existing databases remain on 4096 unless migrated intentionally. + +## Storage Strategy + +- Use a snapshot-based persistence model instead of page-level VFS hooks. +- On `open`, load the full database file bytes from KV (chunked by the existing key scheme). +- On `exec` and `close`, export the full database bytes and write them back to KV in chunks. +- This preserves the exact database file bytes and keeps compatibility with existing data. + +## VFS Semantics + +- The VFS surface is implemented at the package boundary, but storage is snapshot-based. +- WAL and shared-memory are not used. +- Journal mode is forced to `DELETE` in native mode to ensure the main database file contains all data. + +## Packaging and Distribution + +### Biome-Style Native Packaging + +- Follow the same pattern as Biome: a thin base package with `optionalDependencies` on platform-specific binary packages. +- Each platform-specific package contains the prebuilt `.node` addon and a tiny JS wrapper. +- The base package exposes a stable JS API and relies on Node/Bun module resolution to pull in the correct optional dependency. + +- Native addon: + - Built with N-API to support Node.js 20+ and Bun. + - Ship per-platform binaries for macOS, Linux, and Windows. +- Wasm fallback: + - Bundle sqlite-wasm module and JS loader. + - Provide the same API surface. +- Provide both ESM and CJS entrypoints for Node and Bun. + +## Runtime Selection + +- Default: attempt native binding first, fall back to wasm. +- Optional debug override (environment variable) to force `native` or `wasm`, used for CI and troubleshooting. + - Example name: `RIVETKIT_SQLITE_BACKEND=native|wasm`. + +## Testing Strategy + +- Add a new wrapper package `@rivetkit/sqlite-vfs-test` that depends on `@rivetkit/sqlite-vfs`. +- The wrapper package provides: + - An in-memory KV driver identical to the current `sqlite-vfs.test.ts` setup. + - A vitest runner that can target `native` or `wasm` backends via `RIVETKIT_SQLITE_BACKEND`. +- Port existing test `sqlite-vfs.test.ts` into the wrapper package and run against both native and wasm backends. +- Add new tests: + - Data persistence across instances. + - Reopen and schema migration smoke test. + - Chunk boundary read/write tests. + - Concurrent open serialization behavior. +- Add a parity test that runs the same suite against native and wasm paths. + +## Migration Plan + +- Keep the `@rivetkit/sqlite-vfs` package name and exports stable. +- Replace the implementation under the same entrypoint. +- Verify that `rivetkit/db` and `rivetkit/db/drizzle` continue to work unchanged. + +## Risks and Mitigations + +- Async host call support in sqlite-wasm: + - Ensure asyncify or equivalent is part of the wasm build and used consistently in both runtimes. +- JS to Rust callback overhead: + - Maintain `getBatch` and `putBatch` usage to minimize round trips. +- Locking and WAL semantics: + - Preserve current behavior and document limits. Avoid introducing WAL unless fully implemented. + +## Open Questions + +- Which compile-time SQLite flags are enabled in wa-sqlite. These must be mirrored in both native and wasm builds and documented here once identified. +- Whether to expose additional debug logging flags consistent with `VFS_DEBUG`.