From 7f81e1bac57462f112192a82496947d07f280dc3 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 24 Mar 2026 03:23:07 -0700 Subject: [PATCH] feat: native SQLite backend via napi-rs with KV channel protocol --- .cargo/config.toml | 2 + CLAUDE.md | 26 +- Cargo.lock | 44 +- Cargo.toml | 15 +- .../rivetkit-typescript/SQLITE_VFS.md | 12 + engine/artifacts/config-schema.json | 9 + .../actor.kv_storage_quota_exceeded.json | 5 + .../errors/guard.missing_query_parameter.json | 5 + engine/packages/config/src/config/pegboard.rs | 11 + engine/packages/guard/Cargo.toml | 2 + engine/packages/guard/src/errors.rs | 11 + engine/packages/guard/src/routing/envoy.rs | 28 +- .../packages/guard/src/routing/kv_channel.rs | 54 + engine/packages/guard/src/routing/mod.rs | 55 +- .../guard/src/routing/pegboard_gateway/mod.rs | 8 +- engine/packages/guard/src/routing/runner.rs | 31 +- engine/packages/pegboard-envoy/src/conn.rs | 33 +- .../pegboard-envoy/src/tunnel_to_ws_task.rs | 25 +- .../packages/pegboard-kv-channel/Cargo.toml | 35 + .../packages/pegboard-kv-channel/src/lib.rs | 870 +++++++++++ .../pegboard-kv-channel/src/metrics.rs | 26 + engine/packages/pegboard-outbound/src/lib.rs | 15 +- engine/packages/pegboard-runner/Cargo.toml | 1 - engine/packages/pegboard-runner/src/lib.rs | 43 +- .../packages/pegboard-runner/src/metrics.rs | 4 +- .../pegboard-runner/src/tunnel_to_ws_task.rs | 2 +- .../pegboard-runner/src/ws_to_tunnel_task.rs | 59 +- .../packages/pegboard/src/actor_kv/metrics.rs | 19 + engine/packages/pegboard/src/actor_kv/mod.rs | 44 +- .../packages/pegboard/src/actor_kv/preload.rs | 363 +++++ .../packages/pegboard/src/actor_kv/utils.rs | 12 +- engine/packages/pegboard/src/errors.rs | 7 + .../pegboard/src/ops/actor/get_for_runner.rs | 12 +- .../pegboard/src/workflows/actor2/runtime.rs | 56 +- .../sdks/rust/kv-channel-protocol/Cargo.toml | 14 + engine/sdks/rust/kv-channel-protocol/build.rs | 157 ++ .../rust/kv-channel-protocol/src/generated.rs | 1 + .../sdks/rust/kv-channel-protocol/src/lib.rs | 138 ++ engine/sdks/schemas/envoy-protocol/v1.bare | 15 + .../sdks/schemas/kv-channel-protocol/v1.bare | 146 ++ .../typescript/envoy-client/src/config.ts | 1 + .../envoy-client/src/tasks/actor.ts | 8 +- .../envoy-client/src/tasks/envoy/commands.ts | 1 + .../envoy-client/src/tasks/envoy/index.ts | 4 +- .../typescript/envoy-client/src/websocket.ts | 2 +- .../typescript/envoy-protocol/src/index.ts | 145 +- .../kv-channel-protocol/package.json | 36 + .../kv-channel-protocol/src/index.ts | 524 +++++++ .../kv-channel-protocol/tsconfig.json | 9 + .../kv-channel-protocol/tsup.config.ts | 4 + examples/kitchen-sink/scripts/bench-report.ts | 183 +++ examples/kitchen-sink/scripts/bench-sqlite.ts | 719 +++++++++ .../kitchen-sink/scripts/diag-cold-reads.ts | 65 + examples/kitchen-sink/scripts/diag-perf.ts | 83 + .../kitchen-sink/src/actors/sqlite-bench.ts | 742 +++++++++ examples/kitchen-sink/src/index.ts | 2 + pnpm-lock.yaml | 1329 +++++++++++----- pnpm-workspace.yaml | 1 + .../src/actor-handler-do.ts | 33 +- .../packages/cloudflare-workers/src/config.ts | 4 +- .../cloudflare-workers/src/handler.ts | 4 +- .../cloudflare-workers/src/manager-driver.ts | 57 + .../fixtures/driver-test-suite/db-stress.ts | 104 ++ .../fixtures/driver-test-suite/registry.ts | 24 +- .../packages/rivetkit/package.json | 1 + .../rivetkit/scripts/manager-openapi-gen.ts | 4 + .../packages/rivetkit/src/actor/config.ts | 24 + .../packages/rivetkit/src/actor/driver.ts | 22 +- .../rivetkit/src/actor/instance/mod.ts | 33 + .../src/actor/instance/preload-map.ts | 4 +- .../packages/rivetkit/src/db/config.ts | 13 + .../packages/rivetkit/src/db/mod.ts | 25 + .../packages/rivetkit/src/db/native-sqlite.ts | 399 +++++ .../packages/rivetkit/src/db/shared.ts | 3 + .../rivetkit/src/driver-test-suite/mod.ts | 46 + .../test-inline-client-driver.ts | 29 +- .../driver-test-suite/tests/actor-agent-os.ts | 13 +- .../tests/actor-db-stress.ts | 234 +++ .../src/driver-test-suite/tests/actor-db.ts | 87 +- .../tests/cross-backend-vfs.ts | 166 ++ .../rivetkit/src/driver-test-suite/utils.ts | 15 +- .../src/drivers/engine/actor-driver.ts | 247 +-- .../rivetkit/src/drivers/engine/config.ts | 6 +- .../rivetkit/src/drivers/file-system/actor.ts | 10 +- .../src/drivers/file-system/global-state.ts | 86 +- .../src/drivers/file-system/kv-limits.ts | 18 +- .../src/drivers/file-system/manager.ts | 44 + .../packages/rivetkit/src/manager/driver.ts | 45 + .../packages/rivetkit/src/manager/gateway.ts | 5 + .../rivetkit/src/manager/kv-channel.ts | 709 +++++++++ .../packages/rivetkit/src/manager/router.ts | 64 + .../rivetkit/src/registry/config/envoy.ts | 2 +- .../rivetkit/src/registry/config/index.ts | 48 +- .../rivetkit/src/remote-manager-driver/mod.ts | 33 + .../rivetkit/tests/db-closed-race.test.ts | 2 +- .../rivetkit/tests/driver-engine.test.ts | 51 +- .../rivetkit/tests/driver-file-system.test.ts | 2 +- .../packages/rivetkit/tsconfig.json | 1 + .../packages/rivetkit/vitest.config.ts | 5 + .../packages/sqlite-native/Cargo.lock | 988 ++++++++++++ .../packages/sqlite-native/Cargo.toml | 34 + .../packages/sqlite-native/build.rs | 5 + .../packages/sqlite-native/index.d.ts | 180 +++ .../packages/sqlite-native/index.js | 324 ++++ .../npm/darwin-arm64/package.json | 13 + .../sqlite-native/npm/darwin-x64/package.json | 13 + .../npm/linux-arm64-gnu/package.json | 13 + .../npm/linux-x64-gnu/package.json | 13 + .../npm/win32-x64-msvc/package.json | 13 + .../packages/sqlite-native/package.json | 43 + .../sqlite-native.linux-x64-gnu.node | Bin 0 -> 4497345 bytes .../packages/sqlite-native/src/channel.rs | 891 +++++++++++ .../sqlite-native/src/integration_tests.rs | 1123 +++++++++++++ .../packages/sqlite-native/src/kv.rs | 200 +++ .../packages/sqlite-native/src/lib.rs | 965 ++++++++++++ .../packages/sqlite-native/src/vfs.rs | 1388 +++++++++++++++++ .../sqlite-vfs-test/tests/sqlite-vfs.test.ts | 9 + .../packages/sqlite-vfs/src/kv.ts | 22 +- .../packages/sqlite-vfs/src/types.ts | 2 + .../packages/sqlite-vfs/src/vfs.ts | 35 +- scripts/release/sdk.ts | 61 +- scripts/release/update_version.ts | 15 + website/src/content/docs/actors/limits.mdx | 11 + 123 files changed, 14596 insertions(+), 760 deletions(-) create mode 100644 engine/artifacts/errors/actor.kv_storage_quota_exceeded.json create mode 100644 engine/artifacts/errors/guard.missing_query_parameter.json create mode 100644 engine/packages/guard/src/routing/kv_channel.rs create mode 100644 engine/packages/pegboard-kv-channel/Cargo.toml create mode 100644 engine/packages/pegboard-kv-channel/src/lib.rs create mode 100644 engine/packages/pegboard-kv-channel/src/metrics.rs create mode 100644 engine/packages/pegboard/src/actor_kv/metrics.rs create mode 100644 engine/packages/pegboard/src/actor_kv/preload.rs create mode 100644 engine/sdks/rust/kv-channel-protocol/Cargo.toml create mode 100644 engine/sdks/rust/kv-channel-protocol/build.rs create mode 100644 engine/sdks/rust/kv-channel-protocol/src/generated.rs create mode 100644 engine/sdks/rust/kv-channel-protocol/src/lib.rs create mode 100644 engine/sdks/schemas/kv-channel-protocol/v1.bare create mode 100644 engine/sdks/typescript/kv-channel-protocol/package.json create mode 100644 engine/sdks/typescript/kv-channel-protocol/src/index.ts create mode 100644 engine/sdks/typescript/kv-channel-protocol/tsconfig.json create mode 100644 engine/sdks/typescript/kv-channel-protocol/tsup.config.ts create mode 100644 examples/kitchen-sink/scripts/bench-report.ts create mode 100644 examples/kitchen-sink/scripts/bench-sqlite.ts create mode 100644 examples/kitchen-sink/scripts/diag-cold-reads.ts create mode 100644 examples/kitchen-sink/scripts/diag-perf.ts create mode 100644 examples/kitchen-sink/src/actors/sqlite-bench.ts create mode 100644 rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db-stress.ts create mode 100644 rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts create mode 100644 rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db-stress.ts create mode 100644 rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/cross-backend-vfs.ts create mode 100644 rivetkit-typescript/packages/rivetkit/src/manager/kv-channel.ts create mode 100644 rivetkit-typescript/packages/sqlite-native/Cargo.lock create mode 100644 rivetkit-typescript/packages/sqlite-native/Cargo.toml create mode 100644 rivetkit-typescript/packages/sqlite-native/build.rs create mode 100644 rivetkit-typescript/packages/sqlite-native/index.d.ts create mode 100644 rivetkit-typescript/packages/sqlite-native/index.js create mode 100644 rivetkit-typescript/packages/sqlite-native/npm/darwin-arm64/package.json create mode 100644 rivetkit-typescript/packages/sqlite-native/npm/darwin-x64/package.json create mode 100644 rivetkit-typescript/packages/sqlite-native/npm/linux-arm64-gnu/package.json create mode 100644 rivetkit-typescript/packages/sqlite-native/npm/linux-x64-gnu/package.json create mode 100644 rivetkit-typescript/packages/sqlite-native/npm/win32-x64-msvc/package.json create mode 100644 rivetkit-typescript/packages/sqlite-native/package.json create mode 100755 rivetkit-typescript/packages/sqlite-native/sqlite-native.linux-x64-gnu.node create mode 100644 rivetkit-typescript/packages/sqlite-native/src/channel.rs create mode 100644 rivetkit-typescript/packages/sqlite-native/src/integration_tests.rs create mode 100644 rivetkit-typescript/packages/sqlite-native/src/kv.rs create mode 100644 rivetkit-typescript/packages/sqlite-native/src/lib.rs create mode 100644 rivetkit-typescript/packages/sqlite-native/src/vfs.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index bbfc28a440..a439e96d4c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ [build] rustflags = ["--cfg", "tokio_unstable"] +[env] +LIBSQLITE3_FLAGS = "SQLITE_ENABLE_BATCH_ATOMIC_WRITE" diff --git a/CLAUDE.md b/CLAUDE.md index ba0b2b7ccc..cbd229a4d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,18 @@ The `rivet.gg` domain is deprecated and should never be used in this codebase. - Add a new versioned schema instead, then migrate `versioned.rs` and related compatibility code to bridge old versions forward. - When bumping the protocol version, update `PROTOCOL_MK2_VERSION` in `engine/sdks/rust/runner-protocol/src/lib.rs` and `PROTOCOL_VERSION` in `engine/sdks/typescript/runner/src/mod.ts` together. Both must match the latest schema version. +**Keep the KV API in sync between the runner protocol and the KV channel protocol.** + +- The runner protocol (`engine/sdks/schemas/runner-protocol/`) and KV channel protocol (`engine/sdks/schemas/kv-channel-protocol/`) both expose KV operations. When adding, removing, or changing KV request/response types in one protocol, update the other to match. + +**Keep KV channel protocol versions in sync.** + +- When bumping the KV channel protocol version, update these two locations together: + - `engine/sdks/rust/kv-channel-protocol/src/lib.rs` (`PROTOCOL_VERSION`) + - `engine/sdks/rust/kv-channel-protocol/build.rs` (TypeScript `PROTOCOL_VERSION` in post-processing) +- All consumers (pegboard-kv-channel, sqlite-native, TS manager) get the version from the shared crate. +- The TypeScript SDK at `engine/sdks/typescript/kv-channel-protocol/src/index.ts` is auto-generated from the BARE schema during the Rust build. Do not edit it by hand. + ## Commands ### Build Commands @@ -92,6 +104,7 @@ git commit -m "chore(my-pkg): foo bar" ### SQLite Package - Use `@rivetkit/sqlite` for SQLite WebAssembly support. - Do not use the legacy upstream package directly. `@rivetkit/sqlite` is the maintained fork used in this repository and is sourced from `rivet-dev/wa-sqlite`. +- The native SQLite addon (`@rivetkit/sqlite-native`) statically links SQLite via `libsqlite3-sys` with the `bundled` feature. The bundled SQLite version must match the version used by `@rivetkit/sqlite` (WASM). When upgrading either, upgrade both. ### RivetKit Package Resolutions The root `/package.json` contains `resolutions` that map RivetKit packages to their local workspace versions: @@ -236,6 +249,16 @@ Key points: - If available, use the workspace dependency (e.g., `anyhow.workspace = true`) - If you need to add a dependency and can't find it in the Cargo.toml of the workspace, add it to the workspace dependencies in Cargo.toml (`[workspace.dependencies]`) and then add it to the package you need with `{dependency}.workspace = true` +**Native SQLite & KV Channel** +- Native SQLite (`rivetkit-typescript/packages/sqlite-native/`) is a napi-rs addon that statically links SQLite and uses a custom VFS backed by KV over a WebSocket KV channel. The WASM implementation (`@rivetkit/sqlite-vfs`) is the fallback. +- The KV channel (`engine/sdks/schemas/kv-channel-protocol/`) is independent of the runner protocol. It authenticates with `admin_token` (engine) or `config.token` (manager), not the runner key. +- The KV channel enforces single-writer locks per actor. Open/close are optimistic (no round-trip wait). +- The native VFS uses the same 4 KiB chunk layout and KV key encoding as the WASM VFS. Data is compatible between backends. +- **The native Rust VFS and the WASM TypeScript VFS must match 1:1.** This includes: KV key layout and encoding, chunk size, PRAGMA settings, VFS callback-to-KV-operation mapping, delete/truncate strategy (both must use `deleteRange`), and journal mode. When changing any VFS behavior in one implementation, update the other. The relevant files are: + - Native: `rivetkit-typescript/packages/sqlite-native/src/vfs.rs`, `kv.rs` + - WASM: `rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts`, `kv.ts` +- Full spec: `docs-internal/engine/NATIVE_SQLITE_DATA_CHANNEL.md` + **Inspector HTTP API** - When updating the WebSocket inspector (`rivetkit-typescript/packages/rivetkit/src/inspector/`), also update the HTTP inspector endpoints in `rivetkit-typescript/packages/rivetkit/src/actor/router.ts`. The HTTP API mirrors the WebSocket inspector for agent-based debugging. - When adding or modifying inspector endpoints, also update the driver test at `rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-inspector.ts` to cover all inspector HTTP endpoints. @@ -275,7 +298,8 @@ Data structures often include: ## Logging Patterns ### Structured Logging -- Use tracing for logging. Do not format parameters into the main message, instead use tracing's structured logging. +- Use tracing for logging. Never use `eprintln!` or `println!` for logging in Rust code. Always use tracing macros (`tracing::info!`, `tracing::warn!`, `tracing::error!`, etc.). +- Do not format parameters into the main message, instead use tracing's structured logging. - For example, instead of `tracing::info!("foo {x}")`, do `tracing::info!(?x, "foo")` - Log messages should be lowercase unless mentioning specific code symbols. For example, `tracing::info!("inserted UserRow")` instead of `tracing::info!("Inserted UserRow")` diff --git a/Cargo.lock b/Cargo.lock index 361603eedb..2d0284d236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3474,6 +3474,37 @@ dependencies = [ "vbare", ] +[[package]] +name = "pegboard-kv-channel" +version = "2.2.1" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures-util", + "gasoline", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-tungstenite", + "lazy_static", + "namespace", + "pegboard", + "rivet-config", + "rivet-error", + "rivet-guard-core", + "rivet-kv-channel-protocol", + "rivet-metrics", + "rivet-runtime", + "rivet-util", + "tokio", + "tokio-tungstenite", + "tracing", + "universaldb", + "url", + "uuid", +] + [[package]] name = "pegboard-outbound" version = "2.2.1" @@ -3517,7 +3548,6 @@ dependencies = [ "rand 0.8.5", "rivet-config", "rivet-data", - "rivet-envoy-protocol", "rivet-error", "rivet-guard-core", "rivet-metrics", @@ -4685,6 +4715,7 @@ dependencies = [ "pegboard-envoy", "pegboard-gateway", "pegboard-gateway2", + "pegboard-kv-channel", "pegboard-runner", "regex", "rivet-api-builder", @@ -4706,6 +4737,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "subtle", "tokio", "tokio-tungstenite", "tower 0.5.2", @@ -4760,6 +4792,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "rivet-kv-channel-protocol" +version = "2.2.1" +dependencies = [ + "serde", + "serde_bare", + "vbare", + "vbare-compiler", +] + [[package]] name = "rivet-logs" version = "2.2.1" diff --git a/Cargo.toml b/Cargo.toml index 0782f47fca..96308985a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "engine/packages/pegboard-envoy", "engine/packages/pegboard-gateway", "engine/packages/pegboard-gateway2", + "engine/packages/pegboard-kv-channel", "engine/packages/pegboard-outbound", "engine/packages/pegboard-runner", "engine/packages/pools", @@ -53,6 +54,7 @@ members = [ "engine/sdks/rust/envoy-client", "engine/sdks/rust/envoy-protocol", "engine/sdks/rust/epoxy-protocol", + "engine/sdks/rust/kv-channel-protocol", "engine/sdks/rust/runner-protocol", "engine/sdks/rust/ups-protocol" ] @@ -124,6 +126,7 @@ members = [ slog-async = "2.8" slog-term = "2.9" statrs = "0.18" + subtle = "2" sysinfo = "0.37.2" tabled = "0.17.0" tempfile = "3.13.0" @@ -432,6 +435,9 @@ members = [ [workspace.dependencies.pegboard-gateway2] path = "engine/packages/pegboard-gateway2" + [workspace.dependencies.pegboard-kv-channel] + path = "engine/packages/pegboard-kv-channel" + [workspace.dependencies.pegboard-outbound] path = "engine/packages/pegboard-outbound" @@ -493,6 +499,12 @@ members = [ [workspace.dependencies.rivet-data] path = "engine/sdks/rust/data" + [workspace.dependencies.epoxy-protocol] + path = "engine/sdks/rust/epoxy-protocol" + + [workspace.dependencies.rivet-kv-channel-protocol] + path = "engine/sdks/rust/kv-channel-protocol" + [workspace.dependencies.rivet-engine-runner] path = "engine/sdks/rust/engine-runner" @@ -502,9 +514,6 @@ members = [ [workspace.dependencies.rivet-envoy-protocol] path = "engine/sdks/rust/envoy-protocol" - [workspace.dependencies.epoxy-protocol] - path = "engine/sdks/rust/epoxy-protocol" - [workspace.dependencies.rivet-runner-protocol] path = "engine/sdks/rust/runner-protocol" diff --git a/docs-internal/rivetkit-typescript/SQLITE_VFS.md b/docs-internal/rivetkit-typescript/SQLITE_VFS.md index 2407bd94d7..f4af322489 100644 --- a/docs-internal/rivetkit-typescript/SQLITE_VFS.md +++ b/docs-internal/rivetkit-typescript/SQLITE_VFS.md @@ -36,6 +36,18 @@ - Do NOT enable `journal_mode=MEMORY`, `journal_mode=OFF`, or `synchronous=OFF` - `journal_mode=PERSIST` is safe to switch to later (no migration needed) +## Native SQLite Backend + +The WASM VFS described above has a native Rust counterpart (`@rivetkit/sqlite-native`) that statically links SQLite via napi-rs and routes VFS callbacks over a WebSocket-based KV channel protocol. The native backend shares one SQLite library across all actors (vs. one WASM module instance per actor), reducing memory overhead and removing JS from the I/O hot path. Data is fully compatible between backends. An actor can switch between WASM and native without migration. + +Key implementation files: + +- `rivetkit-typescript/packages/sqlite-native/` — napi-rs addon (Rust): `vfs.rs`, `kv.rs`, `channel.rs`, `protocol.rs`, `lib.rs` +- `engine/sdks/schemas/kv-channel-protocol/` — BARE schema and TypeScript codec +- `engine/packages/pegboard-kv-channel/` — engine-side KV channel WebSocket server +- `rivetkit-typescript/packages/rivetkit/src/manager/kv-channel.ts` — manager-side KV channel handler +- `rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts` — integration and WASM fallback logic + ## Future Work - **PITR / fork**: implement at KV layer (immutable chunk versions, manifests, branch heads, GC) with SQLite layer providing snapshot boundary coordination - **Remove double mutex** once profiled diff --git a/engine/artifacts/config-schema.json b/engine/artifacts/config-schema.json index 594bd3cf2a..fb1fbfc681 100644 --- a/engine/artifacts/config-schema.json +++ b/engine/artifacts/config-schema.json @@ -731,6 +731,15 @@ "format": "uint32", "minimum": 0.0 }, + "preload_max_total_bytes": { + "description": "Maximum total size of all preloaded KV data sent with the actor start command. Setting to 0 disables all preloading.\n\nUnit is in bytes. Default: 1,048,576 (1 MiB).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "reschedule_backoff_max_exponent": { "description": "Maximum exponent for the reschedule backoff calculation.\n\nThis controls the maximum backoff duration when rescheduling actors.", "type": [ diff --git a/engine/artifacts/errors/actor.kv_storage_quota_exceeded.json b/engine/artifacts/errors/actor.kv_storage_quota_exceeded.json new file mode 100644 index 0000000000..1915d5fdf0 --- /dev/null +++ b/engine/artifacts/errors/actor.kv_storage_quota_exceeded.json @@ -0,0 +1,5 @@ +{ + "code": "kv_storage_quota_exceeded", + "group": "actor", + "message": "Not enough space left in storage." +} \ No newline at end of file diff --git a/engine/artifacts/errors/guard.missing_query_parameter.json b/engine/artifacts/errors/guard.missing_query_parameter.json new file mode 100644 index 0000000000..2804c8d9f2 --- /dev/null +++ b/engine/artifacts/errors/guard.missing_query_parameter.json @@ -0,0 +1,5 @@ +{ + "code": "missing_query_parameter", + "group": "guard", + "message": "Missing query parameter required for routing." +} \ No newline at end of file diff --git a/engine/packages/config/src/config/pegboard.rs b/engine/packages/config/src/config/pegboard.rs index e5bae9c70d..69a0a7e630 100644 --- a/engine/packages/config/src/config/pegboard.rs +++ b/engine/packages/config/src/config/pegboard.rs @@ -146,6 +146,13 @@ pub struct Pegboard { /// /// Unit is in milliseconds. pub serverless_drain_grace_period: Option, + + // === KV Preload Settings === + /// Maximum total size of all preloaded KV data sent with the actor start command. + /// Setting to 0 disables all preloading. + /// + /// Unit is in bytes. Default: 1,048,576 (1 MiB). + pub preload_max_total_bytes: Option, } impl Pegboard { @@ -306,4 +313,8 @@ impl Pegboard { pub fn serverless_drain_grace_period(&self) -> u64 { self.serverless_drain_grace_period.unwrap_or(10_000) } + + pub fn preload_max_total_bytes(&self) -> u64 { + self.preload_max_total_bytes.unwrap_or(1_048_576) + } } diff --git a/engine/packages/guard/Cargo.toml b/engine/packages/guard/Cargo.toml index a2b1ea7ad5..29cd4a71be 100644 --- a/engine/packages/guard/Cargo.toml +++ b/engine/packages/guard/Cargo.toml @@ -30,6 +30,7 @@ once_cell.workspace = true pegboard-envoy.workspace = true pegboard-gateway.workspace = true pegboard-gateway2.workspace = true +pegboard-kv-channel.workspace = true pegboard-runner.workspace = true pegboard.workspace = true regex.workspace = true @@ -52,6 +53,7 @@ rustls-pemfile.workspace = true rustls.workspace = true serde_json.workspace = true serde.workspace = true +subtle.workspace = true tokio-tungstenite.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/engine/packages/guard/src/errors.rs b/engine/packages/guard/src/errors.rs index 1686d9082e..61db04f0de 100644 --- a/engine/packages/guard/src/errors.rs +++ b/engine/packages/guard/src/errors.rs @@ -13,6 +13,17 @@ pub struct MissingHeader { pub header: String, } +#[derive(RivetError, Serialize)] +#[error( + "guard", + "missing_query_parameter", + "Missing query parameter required for routing.", + "Missing {parameter} query parameter." +)] +pub struct MissingQueryParameter { + pub parameter: String, +} + #[derive(RivetError, Serialize)] #[error( "guard", diff --git a/engine/packages/guard/src/routing/envoy.rs b/engine/packages/guard/src/routing/envoy.rs index 9c0d263988..5ee4cedf47 100644 --- a/engine/packages/guard/src/routing/envoy.rs +++ b/engine/packages/guard/src/routing/envoy.rs @@ -3,7 +3,7 @@ use gas::prelude::*; use rivet_guard_core::{RoutingOutput, request_context::RequestContext}; use std::sync::Arc; -use super::{SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_TOKEN, X_RIVET_TOKEN}; +use super::{SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_TOKEN, X_RIVET_TOKEN, validate_regional_host}; /// Route requests to the envoy service using header-based routing #[tracing::instrument(skip_all)] @@ -45,31 +45,7 @@ async fn route_envoy_internal( ctx: &StandaloneCtx, req_ctx: &RequestContext, ) -> Result { - // Validate that the host is valid for the current datacenter - let current_dc = ctx.config().topology().current_dc()?; - if !current_dc.is_valid_regional_host(req_ctx.hostname()) { - tracing::warn!(hostname=%req_ctx.hostname(), datacenter=?current_dc.name, "invalid host for current datacenter"); - - // Determine valid hosts for error message - let valid_hosts = if let Some(hosts) = ¤t_dc.valid_hosts { - hosts.join(", ") - } else { - current_dc - .public_url - .host_str() - .map(|h| h.to_string()) - .unwrap_or_else(|| "unknown".to_string()) - }; - - return Err(crate::errors::MustUseRegionalHost { - host: req_ctx.hostname().to_string(), - datacenter: current_dc.name.clone(), - valid_hosts, - } - .build()); - } - - tracing::debug!(datacenter = ?current_dc.name, "validated host for datacenter"); + validate_regional_host(ctx, req_ctx)?; // Check auth (if enabled) if let Some(auth) = &ctx.config().auth { diff --git a/engine/packages/guard/src/routing/kv_channel.rs b/engine/packages/guard/src/routing/kv_channel.rs new file mode 100644 index 0000000000..b0bdd46549 --- /dev/null +++ b/engine/packages/guard/src/routing/kv_channel.rs @@ -0,0 +1,54 @@ +use anyhow::*; +use gas::prelude::*; +use rivet_guard_core::{RoutingOutput, request_context::RequestContext}; +use std::sync::Arc; +use subtle::ConstantTimeEq; + +use super::validate_regional_host; + +/// Route requests to the KV channel service using path-based routing. +/// Matches path: /kv/connect +#[tracing::instrument(skip_all)] +pub async fn route_request_path_based( + ctx: &StandaloneCtx, + req_ctx: &RequestContext, + handler: &Arc, +) -> Result> { + let path_without_query = req_ctx.path().split('?').next().unwrap_or(req_ctx.path()); + if path_without_query != "/kv/connect" && path_without_query != "/kv/connect/" { + return Ok(None); + } + + tracing::debug!( + hostname = %req_ctx.hostname(), + path = %req_ctx.path(), + "routing to kv channel via path" + ); + + validate_regional_host(ctx, req_ctx)?; + + // Check auth (if enabled). + if let Some(auth) = &ctx.config().auth { + // Extract token from query params. + let url = url::Url::parse(&format!("ws://placeholder{}", req_ctx.path())) + .context("failed to parse URL for auth")?; + let token = url + .query_pairs() + .find(|(k, _)| k == "token") + .map(|(_, v)| v.to_string()) + .ok_or_else(|| { + crate::errors::MissingQueryParameter { + parameter: "token".to_string(), + } + .build() + })?; + + if token.as_bytes().ct_ne(auth.admin_token.read().as_bytes()).into() { + return Err(rivet_api_builder::ApiForbidden.build()); + } + + tracing::debug!("authenticated kv channel connection"); + } + + Ok(Some(RoutingOutput::CustomServe(handler.clone()))) +} diff --git a/engine/packages/guard/src/routing/mod.rs b/engine/packages/guard/src/routing/mod.rs index 73e068cf1a..db025d59a7 100644 --- a/engine/packages/guard/src/routing/mod.rs +++ b/engine/packages/guard/src/routing/mod.rs @@ -1,14 +1,16 @@ use std::sync::Arc; +use anyhow::Result; use gas::prelude::*; use hyper::header::HeaderName; -use rivet_guard_core::RoutingFn; +use rivet_guard_core::{RoutingFn, request_context::RequestContext}; use crate::{errors, metrics, shared_state::SharedState}; mod api_public; pub mod actor_path; mod envoy; +mod kv_channel; pub(crate) mod matrix_param_deserializer; pub mod pegboard_gateway; mod runner; @@ -25,9 +27,13 @@ pub(crate) const WS_PROTOCOL_TOKEN: &str = "rivet_token."; #[tracing::instrument(skip_all)] pub fn create_routing_function(ctx: &StandaloneCtx, shared_state: SharedState) -> RoutingFn { let ctx = ctx.clone(); + let kv_channel_handler = Arc::new( + pegboard_kv_channel::PegboardKvChannelCustomServe::new(ctx.clone()), + ); Arc::new(move |req_ctx| { let ctx = ctx.with_ray(req_ctx.ray_id(), req_ctx.req_id()).unwrap(); let shared_state = shared_state.clone(); + let kv_channel_handler = kv_channel_handler.clone(); let hostname = req_ctx.hostname().to_string(); let path = req_ctx.path().to_string(); @@ -71,6 +77,18 @@ pub fn create_routing_function(ctx: &StandaloneCtx, shared_state: SharedState) - return Ok(routing_output); } + // Route KV channel + if let Some(routing_output) = + kv_channel::route_request_path_based(&ctx, req_ctx, &kv_channel_handler) + .await? + { + metrics::ROUTE_TOTAL + .with_label_values(&["kv_channel"]) + .inc(); + + return Ok(routing_output); + } + // MARK: Header- & protocol-based routing (X-Rivet-Target) // Determine target let target = if req_ctx.is_websocket() { @@ -150,3 +168,38 @@ pub fn create_routing_function(ctx: &StandaloneCtx, shared_state: SharedState) - ) }) } + +/// Validates that the request hostname is valid for the current datacenter. +/// Returns an error if the host does not match a valid regional host. +pub(crate) fn validate_regional_host( + ctx: &StandaloneCtx, + req_ctx: &RequestContext, +) -> Result<()> { + let current_dc = ctx.config().topology().current_dc()?; + if !current_dc.is_valid_regional_host(req_ctx.hostname()) { + tracing::warn!( + hostname = %req_ctx.hostname(), + datacenter = ?current_dc.name, + "invalid host for current datacenter" + ); + + let valid_hosts = if let Some(hosts) = ¤t_dc.valid_hosts { + hosts.join(", ") + } else { + current_dc + .public_url + .host_str() + .map(|h| h.to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }; + + return Err(errors::MustUseRegionalHost { + host: req_ctx.hostname().to_string(), + datacenter: current_dc.name.clone(), + valid_hosts, + } + .build()); + } + + Ok(()) +} diff --git a/engine/packages/guard/src/routing/pegboard_gateway/mod.rs b/engine/packages/guard/src/routing/pegboard_gateway/mod.rs index 1be69f773e..5070bf44a9 100644 --- a/engine/packages/guard/src/routing/pegboard_gateway/mod.rs +++ b/engine/packages/guard/src/routing/pegboard_gateway/mod.rs @@ -122,9 +122,7 @@ pub async fn route_request( // Find actor to route to let actor_id = Id::parse(&actor_id_str).context("invalid x-rivet-actor header")?; - route_request_inner(ctx, shared_state, req_ctx, actor_id, req_ctx.path(), token) - .await - .map(Some) + route_request_inner(ctx, shared_state, req_ctx, actor_id, req_ctx.path(), token).await } #[derive(Debug)] @@ -200,7 +198,7 @@ async fn route_request_inner( actor_id: Id, stripped_path: &str, _token: Option<&str>, -) -> Result { +) -> Result> { // NOTE: Token validation implemented in EE // Route to peer dc where the actor lives @@ -281,6 +279,7 @@ async fn route_request_inner( destroy_sub2, ) .await + .map(Some) } 1 => { handle_actor_v1( @@ -300,6 +299,7 @@ async fn route_request_inner( destroy_sub2, ) .await + .map(Some) } _ => bail!("unknown actor version"), } diff --git a/engine/packages/guard/src/routing/runner.rs b/engine/packages/guard/src/routing/runner.rs index 820acc6777..071715d214 100644 --- a/engine/packages/guard/src/routing/runner.rs +++ b/engine/packages/guard/src/routing/runner.rs @@ -2,8 +2,9 @@ use anyhow::Result; use gas::prelude::*; use rivet_guard_core::{RoutingOutput, request_context::RequestContext}; use std::sync::Arc; +use subtle::ConstantTimeEq; -use super::{SEC_WEBSOCKET_PROTOCOL, X_RIVET_TOKEN}; +use super::{SEC_WEBSOCKET_PROTOCOL, X_RIVET_TOKEN, validate_regional_host}; pub(crate) const WS_PROTOCOL_TOKEN: &str = "rivet_token."; /// Route requests to the runner service using header-based routing @@ -46,31 +47,7 @@ async fn route_runner_internal( ctx: &StandaloneCtx, req_ctx: &RequestContext, ) -> Result { - // Validate that the host is valid for the current datacenter - let current_dc = ctx.config().topology().current_dc()?; - if !current_dc.is_valid_regional_host(req_ctx.hostname()) { - tracing::warn!(hostname=%req_ctx.hostname(), datacenter=?current_dc.name, "invalid host for current datacenter"); - - // Determine valid hosts for error message - let valid_hosts = if let Some(hosts) = ¤t_dc.valid_hosts { - hosts.join(", ") - } else { - current_dc - .public_url - .host_str() - .map(|h| h.to_string()) - .unwrap_or_else(|| "unknown".to_string()) - }; - - return Err(crate::errors::MustUseRegionalHost { - host: req_ctx.hostname().to_string(), - datacenter: current_dc.name.clone(), - valid_hosts, - } - .build()); - } - - tracing::debug!(datacenter = ?current_dc.name, "validated host for datacenter"); + validate_regional_host(ctx, req_ctx)?; // Check auth (if enabled) if let Some(auth) = &ctx.config().auth { @@ -106,7 +83,7 @@ async fn route_runner_internal( }; // Validate token - if token != auth.admin_token.read() { + if token.as_bytes().ct_ne(auth.admin_token.read().as_bytes()).into() { return Err(rivet_api_builder::ApiForbidden.build()); } diff --git a/engine/packages/pegboard-envoy/src/conn.rs b/engine/packages/pegboard-envoy/src/conn.rs index 8c3e6ad8d0..9f47c426c9 100644 --- a/engine/packages/pegboard-envoy/src/conn.rs +++ b/engine/packages/pegboard-envoy/src/conn.rs @@ -126,7 +126,7 @@ pub async fn handle_init( let envoy_key = &conn.envoy_key; let pool_name = &conn.pool_name; let protocol_version = conn.protocol_version; - let (pool_res, missed_commands) = tokio::try_join!( + let (pool_res, mut missed_commands) = tokio::try_join!( ctx.op(pegboard::ops::runner_config::get::Input { runners: vec![(namespace_id, pool_name.clone())], bypass_cache: false, @@ -353,8 +353,37 @@ pub async fn handle_init( // Send missed commands if !missed_commands.is_empty() { + let db = ctx.udb()?; let msg = - versioned::ToEnvoy::wrap_latest(protocol::ToEnvoy::ToEnvoyCommands(missed_commands)); + { + for cmd_wrapper in &mut missed_commands { + if let protocol::Command::CommandStartActor(ref mut start) = + cmd_wrapper.inner + { + let actor_id = cmd_wrapper + .checkpoint + .actor_id + .parse::() + .context( + "failed to parse actor_id from missed envoy command", + )?; + let preloaded = + pegboard::actor_kv::preload::fetch_preloaded_kv( + &db, + pb, + actor_id, + conn.namespace_id, + &start.config.name, + ) + .await?; + start.preloaded_kv = preloaded; + } + } + + versioned::ToEnvoy::wrap_latest(protocol::ToEnvoy::ToEnvoyCommands( + missed_commands, + )) + }; let msg_serialized = msg.serialize(conn.protocol_version)?; conn.ws_handle .send(Message::Binary(msg_serialized.into())) diff --git a/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs b/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs index e50f3c8c13..32f230fdff 100644 --- a/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs +++ b/engine/packages/pegboard-envoy/src/tunnel_to_ws_task.rs @@ -126,25 +126,38 @@ async fn handle_message( protocol::ToEnvoyConn::ToEnvoyCommands(mut command_wrappers) => { // TODO: Parallelize for command_wrapper in &mut command_wrappers { - if let protocol::Command::CommandStartActor(protocol::CommandStartActor { - hibernating_requests, - .. - }) = &mut command_wrapper.inner + if let protocol::Command::CommandStartActor(start) = + &mut command_wrapper.inner { + let actor_id = Id::parse(&command_wrapper.checkpoint.actor_id)?; + let actor_name = start.config.name.clone(); let ids = ctx .op(pegboard::ops::actor::hibernating_request::list::Input { - actor_id: Id::parse(&command_wrapper.checkpoint.actor_id)?, + actor_id, }) .await?; // Dynamically populate hibernating request ids - *hibernating_requests = ids + start.hibernating_requests = ids .into_iter() .map(|x| protocol::HibernatingRequest { gateway_id: x.gateway_id, request_id: x.request_id, }) .collect(); + + if start.preloaded_kv.is_none() { + let db = ctx.udb()?; + start.preloaded_kv = + pegboard::actor_kv::preload::fetch_preloaded_kv( + &db, + ctx.config().pegboard(), + actor_id, + conn.namespace_id, + &actor_name, + ) + .await?; + } } } diff --git a/engine/packages/pegboard-kv-channel/Cargo.toml b/engine/packages/pegboard-kv-channel/Cargo.toml new file mode 100644 index 0000000000..5c0ef864e3 --- /dev/null +++ b/engine/packages/pegboard-kv-channel/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "pegboard-kv-channel" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +lazy_static.workspace = true +bytes.workspace = true +futures-util.workspace = true +gas.workspace = true +http-body.workspace = true +http-body-util.workspace = true +# TODO: Make this use workspace version +hyper = "1.6" +hyper-tungstenite.workspace = true +rivet-config.workspace = true +rivet-error.workspace = true +rivet-guard-core.workspace = true +rivet-metrics.workspace = true +rivet-runtime.workspace = true +rivet-kv-channel-protocol.workspace = true +tokio.workspace = true +tokio-tungstenite.workspace = true +tracing.workspace = true +universaldb.workspace = true +url.workspace = true +uuid.workspace = true + +pegboard.workspace = true +namespace.workspace = true +util.workspace = true diff --git a/engine/packages/pegboard-kv-channel/src/lib.rs b/engine/packages/pegboard-kv-channel/src/lib.rs new file mode 100644 index 0000000000..79a45fcf2a --- /dev/null +++ b/engine/packages/pegboard-kv-channel/src/lib.rs @@ -0,0 +1,870 @@ +//! KV channel WebSocket handler for the engine. +//! +//! Serves the KV channel protocol at /kv/connect for native SQLite to route +//! page-level KV operations over WebSocket. See +//! docs-internal/engine/NATIVE_SQLITE_DATA_CHANNEL.md for the full spec. + +mod metrics; + +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use async_trait::async_trait; +use bytes::Bytes; +use futures_util::TryStreamExt; +use gas::prelude::*; +use http_body_util::Full; +use hyper::{Response, StatusCode}; +use hyper_tungstenite::tungstenite::Message; +use pegboard::actor_kv; +use rivet_guard_core::{ + ResponseBody, WebSocketHandle, custom_serve::CustomServeTrait, + request_context::RequestContext, +}; +use tokio::sync::{Mutex, mpsc, watch}; +use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame; +use uuid::Uuid; + +pub use rivet_kv_channel_protocol as protocol; + +use actor_kv::{MAX_KEY_SIZE, MAX_KEYS, MAX_PUT_PAYLOAD_SIZE, MAX_VALUE_SIZE}; + +/// Overhead added by KeyWrapper tuple packing (NESTED prefix byte + NIL suffix +/// byte). Must match `KeyWrapper::tuple_len` in +/// `engine/packages/pegboard/src/keys/actor_kv.rs`. +const KEY_WRAPPER_OVERHEAD: usize = 2; + +/// Maximum number of actors a single connection can have open simultaneously. +/// Prevents a malicious client from exhausting memory via unbounded actor_channels. +const MAX_ACTORS_PER_CONNECTION: usize = 1000; + +/// Shared state across all KV channel connections. +pub struct KvChannelState { + /// Maps actor_id string to the connection_id holding the single-writer lock and a reference + /// to that connection's open_actors set. The Arc reference allows lock eviction to remove the + /// actor from the old connection's set without acquiring the global lock on the KV hot path. + actor_locks: Mutex>>)>>, +} + +pub struct PegboardKvChannelCustomServe { + ctx: StandaloneCtx, + state: Arc, +} + +impl PegboardKvChannelCustomServe { + pub fn new(ctx: StandaloneCtx) -> Self { + Self { + ctx, + state: Arc::new(KvChannelState { + actor_locks: Mutex::new(HashMap::new()), + }), + } + } +} + +#[async_trait] +impl CustomServeTrait for PegboardKvChannelCustomServe { + #[tracing::instrument(skip_all)] + async fn handle_request( + &self, + _req: hyper::Request>, + _req_ctx: &mut RequestContext, + ) -> Result> { + let response = Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/plain") + .body(ResponseBody::Full(Full::new(Bytes::from( + "kv-channel WebSocket endpoint", + ))))?; + Ok(response) + } + + #[tracing::instrument(skip_all)] + async fn handle_websocket( + &self, + req_ctx: &mut RequestContext, + ws_handle: WebSocketHandle, + _after_hibernation: bool, + ) -> Result> { + let ctx = self.ctx.with_ray(req_ctx.ray_id(), req_ctx.req_id())?; + let state = self.state.clone(); + + // Parse URL params. + let url = url::Url::parse(&format!("ws://placeholder{}", req_ctx.path())) + .context("failed to parse WebSocket URL")?; + let params: HashMap = url + .query_pairs() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + // Validate protocol version. + let protocol_version: u32 = params + .get("protocol_version") + .context("missing protocol_version query param")? + .parse() + .context("invalid protocol_version")?; + anyhow::ensure!( + protocol_version == protocol::PROTOCOL_VERSION, + "unsupported protocol version: {protocol_version}, expected {}", + protocol::PROTOCOL_VERSION + ); + + // Resolve namespace. + let namespace_name = params + .get("namespace") + .context("missing namespace query param")? + .clone(); + let namespace = ctx + .op(namespace::ops::resolve_for_name_global::Input { + name: namespace_name.clone(), + }) + .await + .with_context(|| format!("failed to resolve namespace: {namespace_name}"))? + .ok_or_else(|| namespace::errors::Namespace::NotFound.build()) + .with_context(|| format!("namespace not found: {namespace_name}"))?; + + // Assign connection ID. Uses UUID to eliminate any possibility of ID collision. + let conn_id = Uuid::new_v4(); + let namespace_id = namespace.namespace_id; + + tracing::info!(%conn_id, %namespace_id, "kv channel connection established"); + + // Track actors opened by this connection for cleanup on disconnect. + let open_actors: Arc>> = Arc::new(Mutex::new(HashSet::new())); + let last_pong_ts = Arc::new(AtomicI64::new(util::timestamp::now())); + + // Run the connection loop. Any error triggers cleanup below. + let result = run_connection( + ctx.clone(), + state.clone(), + ws_handle, + conn_id, + namespace_id, + open_actors.clone(), + last_pong_ts, + ) + .await; + + // Release all locks held by this connection. Only remove entries where the lock is still + // held by this conn_id, since another connection may have evicted it via ActorOpenRequest. + { + let open = open_actors.lock().await; + let mut locks = state.actor_locks.lock().await; + for actor_id in open.iter() { + if let Some((lock_conn, _)) = locks.get(actor_id) { + if *lock_conn == conn_id { + locks.remove(actor_id); + tracing::debug!(%conn_id, %actor_id, "released actor lock on disconnect"); + } + } + } + } + + tracing::info!(%conn_id, "kv channel connection closed"); + + result.map(|_| None) + } +} + +// MARK: Connection lifecycle + +async fn run_connection( + ctx: StandaloneCtx, + state: Arc, + ws_handle: WebSocketHandle, + conn_id: Uuid, + namespace_id: Id, + open_actors: Arc>>, + last_pong_ts: Arc, +) -> Result<()> { + let ping_interval = + Duration::from_millis(ctx.config().pegboard().runner_update_ping_interval_ms()); + let ping_timeout_ms = ctx.config().pegboard().runner_ping_timeout_ms(); + + let (ping_abort_tx, ping_abort_rx) = watch::channel(()); + + // Spawn ping task. + let ping_ws = ws_handle.clone(); + let ping_last_pong = last_pong_ts.clone(); + let ping = tokio::spawn(async move { + ping_task( + ping_ws, + ping_last_pong, + ping_abort_rx, + ping_interval, + ping_timeout_ms, + ) + .await + }); + + // Run message loop. + let msg_result = message_loop( + &ctx, + &state, + &ws_handle, + conn_id, + namespace_id, + &open_actors, + &last_pong_ts, + ) + .await; + + // Signal ping task to stop and wait for it. + let _ = ping_abort_tx.send(()); + let _ = ping.await; + + msg_result +} + +// MARK: Ping task + +async fn ping_task( + ws_handle: WebSocketHandle, + last_pong_ts: Arc, + mut abort_rx: watch::Receiver<()>, + interval: Duration, + timeout_ms: i64, +) -> Result<()> { + loop { + tokio::select! { + _ = tokio::time::sleep(interval) => {} + _ = abort_rx.changed() => return Ok(()), + } + + // Check pong timeout. + let last = last_pong_ts.load(Ordering::Relaxed); + let now = util::timestamp::now(); + if now - last > timeout_ms { + tracing::warn!("kv channel ping timed out, closing connection"); + return Err(anyhow::anyhow!("ping timed out")); + } + + // Send ping. + let ping = protocol::ToClient::ToClientPing(protocol::ToClientPing { ts: now }); + let data = protocol::encode_to_client(&ping)?; + ws_handle.send(Message::Binary(data.into())).await?; + } +} + +// MARK: Message loop + +async fn message_loop( + ctx: &StandaloneCtx, + state: &Arc, + ws_handle: &WebSocketHandle, + conn_id: Uuid, + namespace_id: Id, + open_actors: &Arc>>, + last_pong_ts: &AtomicI64, +) -> Result<()> { + let ws_rx = ws_handle.recv(); + let mut ws_rx = ws_rx.lock().await; + let mut term_signal = rivet_runtime::TermSignal::get(); + + // Per-actor channel routing for concurrent cross-actor request processing. + // Each actor gets its own mpsc channel and a spawned task that drains it + // sequentially, preserving intra-actor ordering while allowing inter-actor + // parallelism. Do not use tokio::spawn per request as that would break + // optimistic pipelining and journal write ordering. + // See docs-internal/engine/NATIVE_SQLITE_REVIEW_FINDINGS.md Finding 2. + let mut actor_channels: HashMap> = + HashMap::new(); + let mut actor_tasks = tokio::task::JoinSet::new(); + + // Use an async block so that early returns (via ?) still run cleanup below. + let result = async { + loop { + let msg = tokio::select! { + res = ws_rx.try_next() => { + match res? { + Some(msg) => msg, + None => { + tracing::debug!("websocket closed"); + return Ok(()); + } + } + } + _ = term_signal.recv() => { + // Send ToClientClose before shutting down. + let close_msg = protocol::ToClient::ToClientClose; + let data = protocol::encode_to_client(&close_msg)?; + let _ = ws_handle.send(Message::Binary(data.into())).await; + return Ok(()); + } + }; + + match msg { + Message::Binary(data) => { + handle_binary_message( + ctx, + state, + ws_handle, + conn_id, + namespace_id, + open_actors, + last_pong_ts, + &data, + &mut actor_channels, + &mut actor_tasks, + ) + .await?; + } + Message::Close(_) => { + tracing::debug!("websocket close frame received"); + return Ok(()); + } + _ => {} + } + } + } + .await; + + // Drop all senders to signal per-actor tasks to stop, then wait for them + // to finish draining any in-flight requests. + actor_channels.clear(); + while actor_tasks.join_next().await.is_some() {} + + result +} + +async fn handle_binary_message( + ctx: &StandaloneCtx, + state: &Arc, + ws_handle: &WebSocketHandle, + conn_id: Uuid, + namespace_id: Id, + open_actors: &Arc>>, + last_pong_ts: &AtomicI64, + data: &[u8], + actor_channels: &mut HashMap>, + actor_tasks: &mut tokio::task::JoinSet<()>, +) -> Result<()> { + let msg = match protocol::decode_to_server(data) { + Ok(msg) => msg, + Err(err) => { + tracing::warn!( + ?err, + data_len = data.len(), + "failed to deserialize kv channel message" + ); + return Ok(()); + } + }; + + match msg { + protocol::ToServer::ToServerPong(pong) => { + last_pong_ts.store(util::timestamp::now(), Ordering::Relaxed); + tracing::trace!(ts = pong.ts, "received pong"); + } + protocol::ToServer::ToServerRequest(req) => { + let is_close = matches!(req.data, protocol::RequestData::ActorCloseRequest); + let actor_id = req.actor_id.clone(); + let request_id = req.request_id; + + // Create a per-actor channel and task on first request for this actor. + if !actor_channels.contains_key(&actor_id) { + let (tx, rx) = mpsc::channel(64); + actor_tasks.spawn(actor_request_task( + Clone::clone(ctx), + Clone::clone(state), + Clone::clone(ws_handle), + conn_id, + namespace_id, + Clone::clone(open_actors), + rx, + )); + actor_channels.insert(actor_id.clone(), tx); + } + + // Route request to the actor's channel for sequential processing. + if let Some(tx) = actor_channels.get(&actor_id) { + match tx.try_send(req) { + Ok(()) => {} + Err(mpsc::error::TrySendError::Full(_)) => { + tracing::warn!(%actor_id, "per-actor channel full, applying backpressure"); + send_response( + ws_handle, + request_id, + error_response( + "backpressure", + "too many in-flight requests for this actor", + ), + ) + .await; + } + Err(mpsc::error::TrySendError::Closed(_)) => { + tracing::warn!(%actor_id, "per-actor task channel closed, removing dead entry"); + actor_channels.remove(&actor_id); + send_response( + ws_handle, + request_id, + error_response( + "internal_error", + "internal error", + ), + ) + .await; + } + } + } + + // Remove the channel entry on close so the task exits after draining + // remaining requests and resources are freed. + if is_close { + actor_channels.remove(&actor_id); + } + } + } + + Ok(()) +} + +/// Processes requests for a single actor sequentially, preserving intra-actor +/// ordering. Spawned once per actor per connection. Exits when the sender is +/// dropped (connection end) or after processing an ActorCloseRequest. +async fn actor_request_task( + ctx: StandaloneCtx, + state: Arc, + ws_handle: WebSocketHandle, + conn_id: Uuid, + namespace_id: Id, + open_actors: Arc>>, + mut rx: mpsc::Receiver, +) { + // Cached actor resolution. Populated on first KV request, reused for all + // subsequent requests. Actor name is immutable so this never goes stale. + let mut cached_actor: Option<(Id, String)> = None; + + while let Some(req) = rx.recv().await { + let is_close = matches!(req.data, protocol::RequestData::ActorCloseRequest); + + let response_data = match &req.data { + // Open/close are lifecycle ops that don't need a resolved actor. + protocol::RequestData::ActorOpenRequest + | protocol::RequestData::ActorCloseRequest => { + handle_request(&ctx, &state, conn_id, namespace_id, &open_actors, &req).await + } + // KV ops: resolve once, cache, reuse. + _ => { + let is_open = open_actors.lock().await.contains(&req.actor_id); + if !is_open { + let locks = state.actor_locks.lock().await; + if locks.contains_key(&req.actor_id) { + error_response( + "actor_locked", + "actor is locked by another connection", + ) + } else { + error_response( + "actor_not_open", + "actor is not opened on this connection", + ) + } + } else { + // Lazy-resolve and cache. + if cached_actor.is_none() { + match resolve_actor(&ctx, &req.actor_id, namespace_id).await { + Ok(v) => { + cached_actor = Some(v); + } + Err(resp) => { + // Don't cache failures. Next request will retry. + send_response(&ws_handle, req.request_id, resp).await; + if is_close { + break; + } + continue; + } + } + } + let (parsed_id, actor_name) = cached_actor.as_ref().unwrap(); + + let recipient = actor_kv::Recipient { + actor_id: *parsed_id, + namespace_id, + name: actor_name.clone(), + }; + + match &req.data { + protocol::RequestData::KvGetRequest(body) => { + handle_kv_get(&ctx, &recipient, body).await + } + protocol::RequestData::KvPutRequest(body) => { + handle_kv_put(&ctx, &recipient, body).await + } + protocol::RequestData::KvDeleteRequest(body) => { + handle_kv_delete(&ctx, &recipient, body).await + } + protocol::RequestData::KvDeleteRangeRequest(body) => { + handle_kv_delete_range(&ctx, &recipient, body).await + } + _ => unreachable!(), + } + } + } + }; + + send_response(&ws_handle, req.request_id, response_data).await; + + // Stop processing after a close request. The sender is also removed + // from actor_channels by the message loop so no new requests arrive. + if is_close { + break; + } + } +} + +/// Encode and send a response to the client. Logs warnings on failure. +async fn send_response( + ws_handle: &WebSocketHandle, + request_id: u32, + data: protocol::ResponseData, +) { + let response = protocol::ToClient::ToClientResponse(protocol::ToClientResponse { + request_id, + data, + }); + + match protocol::encode_to_client(&response) { + Ok(encoded) => { + if let Err(err) = ws_handle.send(Message::Binary(encoded.into())).await { + tracing::warn!(?err, "failed to send kv channel response from actor task"); + } + } + Err(err) => { + tracing::warn!(?err, "failed to encode kv channel response"); + } + } +} + +// MARK: Request handling + +/// Handles actor lifecycle requests (open/close). KV operations are handled +/// directly in `actor_request_task` with cached actor resolution. +async fn handle_request( + _ctx: &StandaloneCtx, + state: &KvChannelState, + conn_id: Uuid, + _namespace_id: Id, + open_actors: &Arc>>, + req: &protocol::ToServerRequest, +) -> protocol::ResponseData { + match &req.data { + protocol::RequestData::ActorOpenRequest => { + handle_actor_open(state, conn_id, open_actors, &req.actor_id).await + } + protocol::RequestData::ActorCloseRequest => { + handle_actor_close(state, conn_id, open_actors, &req.actor_id).await + } + _ => unreachable!("KV operations are handled in actor_request_task"), + } +} + +// MARK: Actor open/close + +async fn handle_actor_open( + state: &KvChannelState, + conn_id: Uuid, + open_actors: &Arc>>, + actor_id: &str, +) -> protocol::ResponseData { + // Reject if this connection already has too many actors open. + { + let current_count = open_actors.lock().await.len(); + if current_count >= MAX_ACTORS_PER_CONNECTION { + return error_response( + "too_many_actors", + &format!( + "connection has too many open actors (max {MAX_ACTORS_PER_CONNECTION})" + ), + ); + } + } + + let mut locks = state.actor_locks.lock().await; + + // If the actor is locked by a different connection, unconditionally evict the old lock. + // This handles reconnection scenarios where the server hasn't detected the old connection's + // disconnect yet. The old connection's next KV request will fail the fast-path check + // (open_actors.contains) and return actor_not_open. + // See docs-internal/engine/NATIVE_SQLITE_REVIEW_FINDINGS.md Finding 4. + if let Some((existing_conn, old_open_actors)) = locks.get(actor_id) { + if *existing_conn != conn_id { + old_open_actors.lock().await.remove(actor_id); + tracing::info!( + %conn_id, + old_conn_id = %existing_conn, + %actor_id, + "evicted stale actor lock from old connection" + ); + } + } + + locks.insert(actor_id.to_string(), (conn_id, open_actors.clone())); + open_actors.lock().await.insert(actor_id.to_string()); + tracing::debug!(%conn_id, %actor_id, "actor lock acquired"); + protocol::ResponseData::ActorOpenResponse +} + +async fn handle_actor_close( + state: &KvChannelState, + conn_id: Uuid, + open_actors: &Arc>>, + actor_id: &str, +) -> protocol::ResponseData { + let mut locks = state.actor_locks.lock().await; + + if let Some((lock_conn, _)) = locks.get(actor_id) { + if *lock_conn == conn_id { + locks.remove(actor_id); + open_actors.lock().await.remove(actor_id); + tracing::debug!(%conn_id, %actor_id, "actor lock released"); + } + } + + protocol::ResponseData::ActorCloseResponse +} + +// MARK: KV operations + +async fn handle_kv_get( + ctx: &StandaloneCtx, + recipient: &actor_kv::Recipient, + body: &protocol::KvGetRequest, +) -> protocol::ResponseData { + let start = Instant::now(); + metrics::KV_CHANNEL_REQUESTS_TOTAL.with_label_values(&["get"]).inc(); + metrics::KV_CHANNEL_REQUEST_KEYS.with_label_values(&["get"]).observe(body.keys.len() as f64); + + if let Err(resp) = validate_keys(&body.keys) { + return resp; + } + + let udb = match ctx.udb() { + Ok(udb) => udb, + Err(err) => return internal_error(&err), + }; + + let result = match actor_kv::get(&*udb, recipient, body.keys.clone()).await { + Ok((keys, values, _metadata)) => { + protocol::ResponseData::KvGetResponse(protocol::KvGetResponse { keys, values }) + } + Err(err) => internal_error(&err), + }; + metrics::KV_CHANNEL_REQUEST_DURATION.with_label_values(&["get"]).observe(start.elapsed().as_secs_f64()); + result +} + +async fn handle_kv_put( + ctx: &StandaloneCtx, + recipient: &actor_kv::Recipient, + body: &protocol::KvPutRequest, +) -> protocol::ResponseData { + let start = Instant::now(); + metrics::KV_CHANNEL_REQUESTS_TOTAL.with_label_values(&["put"]).inc(); + metrics::KV_CHANNEL_REQUEST_KEYS.with_label_values(&["put"]).observe(body.keys.len() as f64); + + // Validate keys/values length match. + if body.keys.len() != body.values.len() { + return error_response( + "keys_values_length_mismatch", + "keys and values must have the same length", + ); + } + + // Validate batch size. + if body.keys.len() > MAX_KEYS { + return error_response( + "batch_too_large", + &format!("a maximum of {MAX_KEYS} entries is allowed"), + ); + } + + for key in &body.keys { + if key.len() + KEY_WRAPPER_OVERHEAD > MAX_KEY_SIZE { + return error_response( + "key_too_large", + &format!("key is too long (max {} bytes)", MAX_KEY_SIZE - KEY_WRAPPER_OVERHEAD), + ); + } + } + for value in &body.values { + if value.len() > MAX_VALUE_SIZE { + return error_response( + "value_too_large", + &format!("value is too large (max {} KiB)", MAX_VALUE_SIZE / 1024), + ); + } + } + + let payload_size: usize = body.keys.iter().map(|k| k.len() + KEY_WRAPPER_OVERHEAD).sum::() + + body.values.iter().map(|v| v.len()).sum::(); + if payload_size > MAX_PUT_PAYLOAD_SIZE { + return error_response( + "payload_too_large", + &format!( + "total payload is too large (max {} KiB)", + MAX_PUT_PAYLOAD_SIZE / 1024 + ), + ); + } + + let udb = match ctx.udb() { + Ok(udb) => udb, + Err(err) => return internal_error(&err), + }; + + let result = match actor_kv::put(&*udb, recipient, body.keys.clone(), body.values.clone()).await { + Ok(()) => protocol::ResponseData::KvPutResponse, + Err(err) => { + let rivet_err = rivet_error::RivetError::extract(&err); + if rivet_err.code() == "kv_storage_quota_exceeded" { + error_response("storage_quota_exceeded", rivet_err.message()) + } else { + internal_error(&err) + } + } + }; + metrics::KV_CHANNEL_REQUEST_DURATION.with_label_values(&["put"]).observe(start.elapsed().as_secs_f64()); + result +} + +async fn handle_kv_delete( + ctx: &StandaloneCtx, + recipient: &actor_kv::Recipient, + body: &protocol::KvDeleteRequest, +) -> protocol::ResponseData { + let start = Instant::now(); + metrics::KV_CHANNEL_REQUESTS_TOTAL.with_label_values(&["delete"]).inc(); + metrics::KV_CHANNEL_REQUEST_KEYS.with_label_values(&["delete"]).observe(body.keys.len() as f64); + + if let Err(resp) = validate_keys(&body.keys) { + return resp; + } + + let udb = match ctx.udb() { + Ok(udb) => udb, + Err(err) => return internal_error(&err), + }; + + let result = match actor_kv::delete(&*udb, recipient, body.keys.clone()).await { + Ok(()) => protocol::ResponseData::KvDeleteResponse, + Err(err) => internal_error(&err), + }; + metrics::KV_CHANNEL_REQUEST_DURATION.with_label_values(&["delete"]).observe(start.elapsed().as_secs_f64()); + result +} + +async fn handle_kv_delete_range( + ctx: &StandaloneCtx, + recipient: &actor_kv::Recipient, + body: &protocol::KvDeleteRangeRequest, +) -> protocol::ResponseData { + let start = Instant::now(); + metrics::KV_CHANNEL_REQUESTS_TOTAL.with_label_values(&["delete_range"]).inc(); + if body.start.len() + KEY_WRAPPER_OVERHEAD > MAX_KEY_SIZE { + return error_response( + "key_too_large", + &format!("start key is too long (max {} bytes)", MAX_KEY_SIZE - KEY_WRAPPER_OVERHEAD), + ); + } + if body.end.len() + KEY_WRAPPER_OVERHEAD > MAX_KEY_SIZE { + return error_response( + "key_too_large", + &format!("end key is too long (max {} bytes)", MAX_KEY_SIZE - KEY_WRAPPER_OVERHEAD), + ); + } + + let udb = match ctx.udb() { + Ok(udb) => udb, + Err(err) => return internal_error(&err), + }; + + let result = match actor_kv::delete_range(&*udb, recipient, body.start.clone(), body.end.clone()).await { + Ok(()) => protocol::ResponseData::KvDeleteResponse, + Err(err) => internal_error(&err), + }; + metrics::KV_CHANNEL_REQUEST_DURATION.with_label_values(&["delete_range"]).observe(start.elapsed().as_secs_f64()); + result +} + +// MARK: Helpers + +/// Look up an actor by ID and return the parsed ID and actor name. +/// +/// Defense-in-depth: verifies the actor belongs to the authenticated namespace. +/// The admin_token is a global credential, so this is not strictly necessary +/// today, but prevents cross-namespace access if a less-privileged auth +/// mechanism is introduced in the future. +async fn resolve_actor( + ctx: &StandaloneCtx, + actor_id: &str, + expected_namespace_id: Id, +) -> std::result::Result<(Id, String), protocol::ResponseData> { + let parsed_id = Id::parse(actor_id).map_err(|err| { + error_response( + "actor_not_found", + &format!("invalid actor id: {err}"), + ) + })?; + + let actor = ctx + .op(pegboard::ops::actor::get_for_runner::Input { + actor_id: parsed_id, + }) + .await + .map_err(|err| internal_error(&err))?; + + match actor { + Some(actor) => { + if actor.namespace_id != expected_namespace_id { + return Err(error_response( + "actor_not_found", + "actor does not exist or is not running", + )); + } + Ok((parsed_id, actor.name)) + } + None => Err(error_response( + "actor_not_found", + "actor does not exist or is not running", + )), + } +} + +/// Validate a list of KV keys against size and count limits. +fn validate_keys(keys: &[protocol::KvKey]) -> std::result::Result<(), protocol::ResponseData> { + if keys.len() > MAX_KEYS { + return Err(error_response( + "batch_too_large", + &format!("a maximum of {MAX_KEYS} keys is allowed"), + )); + } + for key in keys { + if key.len() + KEY_WRAPPER_OVERHEAD > MAX_KEY_SIZE { + return Err(error_response( + "key_too_large", + &format!("key is too long (max {} bytes)", MAX_KEY_SIZE - KEY_WRAPPER_OVERHEAD), + )); + } + } + Ok(()) +} + +fn error_response(code: &str, message: &str) -> protocol::ResponseData { + protocol::ResponseData::ErrorResponse(protocol::ErrorResponse { + code: code.to_string(), + message: message.to_string(), + }) +} + +/// Log an internal error with full details server-side and return a generic +/// error message to the client. Prevents leaking stack traces, database errors, +/// or other internal state over the wire. +fn internal_error(err: &anyhow::Error) -> protocol::ResponseData { + tracing::error!(?err, "kv channel internal error"); + error_response("internal_error", "internal error") +} diff --git a/engine/packages/pegboard-kv-channel/src/metrics.rs b/engine/packages/pegboard-kv-channel/src/metrics.rs new file mode 100644 index 0000000000..6c71756979 --- /dev/null +++ b/engine/packages/pegboard-kv-channel/src/metrics.rs @@ -0,0 +1,26 @@ +use rivet_metrics::{BUCKETS, REGISTRY, prometheus::*}; + +lazy_static::lazy_static! { + pub static ref KV_CHANNEL_REQUEST_DURATION: HistogramVec = register_histogram_vec_with_registry!( + "pegboard_kv_channel_request_duration_seconds", + "Duration of KV channel handler requests.", + &["op"], + BUCKETS.to_vec(), + *REGISTRY + ).unwrap(); + + pub static ref KV_CHANNEL_REQUEST_KEYS: HistogramVec = register_histogram_vec_with_registry!( + "pegboard_kv_channel_request_keys", + "Number of keys per KV channel request.", + &["op"], + vec![1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 128.0], + *REGISTRY + ).unwrap(); + + pub static ref KV_CHANNEL_REQUESTS_TOTAL: IntCounterVec = register_int_counter_vec_with_registry!( + "pegboard_kv_channel_requests_total", + "Total KV channel requests handled.", + &["op"], + *REGISTRY + ).unwrap(); +} diff --git a/engine/packages/pegboard-outbound/src/lib.rs b/engine/packages/pegboard-outbound/src/lib.rs index 3205262642..d45a1726ec 100644 --- a/engine/packages/pegboard-outbound/src/lib.rs +++ b/engine/packages/pegboard-outbound/src/lib.rs @@ -4,7 +4,7 @@ use gas::prelude::*; use pegboard::pubsub_subjects::ServerlessOutboundSubject; use reqwest::header::{HeaderName, HeaderValue}; use reqwest_eventsource as sse; -use rivet_envoy_protocol::{self as protocol, versioned}; +use rivet_envoy_protocol::{self as protocol, PROTOCOL_VERSION, versioned}; use rivet_runtime::TermSignal; use rivet_types::actor::RunnerPoolError; use rivet_types::runner_configs::RunnerConfigKind; @@ -192,6 +192,16 @@ async fn handle(ctx: &StandaloneCtx, packet: protocol::ToOutbound) -> Result<()> return Ok(()); }; + let udb = ctx.udb()?; + let preloaded_kv = pegboard::actor_kv::preload::fetch_preloaded_kv( + &udb, + ctx.config().pegboard(), + actor_id, + namespace_id, + &actor_config.name, + ) + .await?; + let payload = versioned::ToEnvoy::wrap_latest(protocol::ToEnvoy::ToEnvoyCommands(vec![ protocol::CommandWrapper { checkpoint, @@ -200,10 +210,11 @@ async fn handle(ctx: &StandaloneCtx, packet: protocol::ToOutbound) -> Result<()> // Empty because request ids are ephemeral. This is intercepted by guard and // populated before it reaches the envoy hibernating_requests: Vec::new(), + preloaded_kv, }), }, ])) - .serialize_with_embedded_version(pool.protocol_version.unwrap_or(1))?; + .serialize_with_embedded_version(pool.protocol_version.unwrap_or(PROTOCOL_VERSION))?; let RunnerConfigKind::Serverless { url, diff --git a/engine/packages/pegboard-runner/Cargo.toml b/engine/packages/pegboard-runner/Cargo.toml index 604038952b..3566407f40 100644 --- a/engine/packages/pegboard-runner/Cargo.toml +++ b/engine/packages/pegboard-runner/Cargo.toml @@ -21,7 +21,6 @@ lazy_static.workspace = true rand.workspace = true rivet-config.workspace = true rivet-data.workspace = true -rivet-envoy-protocol.workspace = true rivet-error.workspace = true rivet-guard-core.workspace = true rivet-metrics.workspace = true diff --git a/engine/packages/pegboard-runner/src/lib.rs b/engine/packages/pegboard-runner/src/lib.rs index fcd7a7f649..db97f070fd 100644 --- a/engine/packages/pegboard-runner/src/lib.rs +++ b/engine/packages/pegboard-runner/src/lib.rs @@ -26,7 +26,6 @@ mod ws_to_tunnel_task; enum LifecycleResult { Closed, Aborted, - Evicted, } pub struct PegboardRunnerWsCustomServe { @@ -223,40 +222,34 @@ impl CustomServeTrait for PegboardRunnerWsCustomServe { ); // Determine single result from all tasks - let mut lifecycle_res = match (tunnel_to_ws_res, ws_to_tunnel_res, ping_res) { + let lifecycle_res = match (tunnel_to_ws_res, ws_to_tunnel_res, ping_res) { // Prefer error (Err(err), _, _) => Err(err), (_, Err(err), _) => Err(err), (_, _, Err(err)) => Err(err), - // Prefer non aborted result + // Prefer non aborted result if both succeed (Ok(res), Ok(LifecycleResult::Aborted), _) => Ok(res), (Ok(LifecycleResult::Aborted), Ok(res), _) => Ok(res), // Unlikely case (res, _, _) => res, }; - if let Ok(LifecycleResult::Evicted) = &lifecycle_res { - lifecycle_res = Err(errors::WsError::Eviction.build()); - } - // Clear alloc idx if not evicted - else { - // Make runner immediately ineligible when it disconnects - let update_alloc_res = self - .ctx - .op(pegboard::ops::runner::update_alloc_idx::Input { - runners: vec![pegboard::ops::runner::update_alloc_idx::Runner { - runner_id: conn.runner_id, - action: Action::ClearIdx, - }], - }) - .await; - if let Err(err) = update_alloc_res { - tracing::error!( - runner_id=?conn.runner_id, - ?err, - "failed to evict runner from allocation index during disconnect" - ); - } + // Make runner immediately ineligible when it disconnects + let update_alloc_res = self + .ctx + .op(pegboard::ops::runner::update_alloc_idx::Input { + runners: vec![pegboard::ops::runner::update_alloc_idx::Runner { + runner_id: conn.runner_id, + action: Action::ClearIdx, + }], + }) + .await; + if let Err(err) = update_alloc_res { + tracing::error!( + runner_id=?conn.runner_id, + ?err, + "critical: failed to evict runner from allocation index during disconnect" + ); } tracing::debug!(%topic, "runner websocket closed"); diff --git a/engine/packages/pegboard-runner/src/metrics.rs b/engine/packages/pegboard-runner/src/metrics.rs index c5edab2c14..931d0c9077 100644 --- a/engine/packages/pegboard-runner/src/metrics.rs +++ b/engine/packages/pegboard-runner/src/metrics.rs @@ -31,13 +31,13 @@ lazy_static::lazy_static! { ).unwrap(); pub static ref EVENT_MULTIPLEXER_COUNT: IntGauge = register_int_gauge_with_registry!( - "pegboard_runner_event_multiplexer_count", + "pegboard_event_multiplexer_count", "Number of active actor event multiplexers.", *REGISTRY ).unwrap(); pub static ref INGESTED_EVENTS_TOTAL: IntCounter = register_int_counter_with_registry!( - "pegboard_runner_ingested_events_total", + "pegboard_ingested_events_total", "Count of actor events.", *REGISTRY ).unwrap(); diff --git a/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs b/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs index 6763e27519..1c8f2168ba 100644 --- a/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs +++ b/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs @@ -66,7 +66,7 @@ async fn recv_msg( ]) .inc(); - return Ok(Err(LifecycleResult::Evicted)); + return Err(errors::WsError::Eviction.build()); } _ = tunnel_to_ws_abort_rx.changed() => { tracing::debug!("task aborted"); diff --git a/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs b/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs index f3492a706f..2ab612070f 100644 --- a/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs +++ b/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs @@ -6,7 +6,6 @@ use gas::prelude::*; use hyper_tungstenite::tungstenite::Message; use pegboard::actor_kv; use pegboard::pubsub_subjects::GatewayReceiverSubject; -use rivet_envoy_protocol as ep; use rivet_guard_core::websocket_handle::WebSocketReceiver; use rivet_runner_protocol::{self as protocol, PROTOCOL_MK2_VERSION, versioned}; use std::sync::{Arc, atomic::Ordering}; @@ -54,7 +53,7 @@ pub async fn task_inner( event_demuxer: &mut ActorEventDemuxer, ) -> Result { let mut ws_rx = ws_rx.lock().await; - let mut term_signal = rivet_runtime::TermSignal::get(); + let mut term_signal = rivet_runtime::TermSignal::new().await; loop { match recv_msg( @@ -106,7 +105,7 @@ async fn recv_msg( ]) .inc(); - return Ok(Err(LifecycleResult::Evicted)); + return Err(errors::WsError::Eviction.build()); } _ = ws_to_tunnel_abort_rx.changed() => { tracing::debug!("task aborted"); @@ -240,13 +239,7 @@ async fn handle_message_mk2( protocol::mk2::KvGetResponse { keys, values, - metadata: metadata - .into_iter() - .map(|m| protocol::mk2::KvMetadata { - version: m.version, - update_ts: m.update_ts, - }) - .collect(), + metadata, }, ) } @@ -273,23 +266,7 @@ async fn handle_message_mk2( let res = actor_kv::list( &*ctx.udb()?, &recipient, - match body.query { - protocol::mk2::KvListQuery::KvListAllQuery => { - ep::KvListQuery::KvListAllQuery - } - protocol::mk2::KvListQuery::KvListRangeQuery(x) => { - ep::KvListQuery::KvListRangeQuery(ep::KvListRangeQuery { - start: x.start, - end: x.end, - exclusive: x.exclusive, - }) - } - protocol::mk2::KvListQuery::KvListPrefixQuery(x) => { - ep::KvListQuery::KvListPrefixQuery(ep::KvListPrefixQuery { - key: x.key, - }) - } - }, + body.query, body.reverse.unwrap_or_default(), body.limit .map(TryInto::try_into) @@ -308,13 +285,7 @@ async fn handle_message_mk2( protocol::mk2::KvListResponse { keys, values, - metadata: metadata - .into_iter() - .map(|m| protocol::mk2::KvMetadata { - version: m.version, - update_ts: m.update_ts, - }) - .collect(), + metadata, }, ) } @@ -632,19 +603,21 @@ async fn handle_message_mk1(ctx: &StandaloneCtx, conn: &Conn, msg: Bytes) -> Res &recipient, match body.query { protocol::KvListQuery::KvListAllQuery => { - ep::KvListQuery::KvListAllQuery + protocol::mk2::KvListQuery::KvListAllQuery } protocol::KvListQuery::KvListRangeQuery(q) => { - ep::KvListQuery::KvListRangeQuery(ep::KvListRangeQuery { - start: q.start, - end: q.end, - exclusive: q.exclusive, - }) + protocol::mk2::KvListQuery::KvListRangeQuery( + protocol::mk2::KvListRangeQuery { + start: q.start, + end: q.end, + exclusive: q.exclusive, + }, + ) } protocol::KvListQuery::KvListPrefixQuery(q) => { - ep::KvListQuery::KvListPrefixQuery(ep::KvListPrefixQuery { - key: q.key, - }) + protocol::mk2::KvListQuery::KvListPrefixQuery( + protocol::mk2::KvListPrefixQuery { key: q.key }, + ) } }, body.reverse.unwrap_or_default(), diff --git a/engine/packages/pegboard/src/actor_kv/metrics.rs b/engine/packages/pegboard/src/actor_kv/metrics.rs new file mode 100644 index 0000000000..7716a90342 --- /dev/null +++ b/engine/packages/pegboard/src/actor_kv/metrics.rs @@ -0,0 +1,19 @@ +use rivet_metrics::{BUCKETS, REGISTRY, prometheus::*}; + +lazy_static::lazy_static! { + pub static ref ACTOR_KV_OPERATION_DURATION: HistogramVec = register_histogram_vec_with_registry!( + "actor_kv_operation_duration_seconds", + "Duration of actor KV operations including UDB transaction.", + &["op"], + BUCKETS.to_vec(), + *REGISTRY + ).unwrap(); + + pub static ref ACTOR_KV_KEYS_PER_OP: HistogramVec = register_histogram_vec_with_registry!( + "actor_kv_keys_per_operation", + "Number of keys per actor KV operation.", + &["op"], + vec![1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0], + *REGISTRY + ).unwrap(); +} diff --git a/engine/packages/pegboard/src/actor_kv/mod.rs b/engine/packages/pegboard/src/actor_kv/mod.rs index 830041fd46..d4689e4268 100644 --- a/engine/packages/pegboard/src/actor_kv/mod.rs +++ b/engine/packages/pegboard/src/actor_kv/mod.rs @@ -10,16 +10,18 @@ use utils::{validate_entries, validate_keys, validate_range}; use crate::keys; mod entry; +mod metrics; +pub mod preload; mod utils; const VERSION: &str = env!("CARGO_PKG_VERSION"); // Keep the KV validation limits below in sync with // rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts. -const MAX_KEY_SIZE: usize = 2 * 1024; -const MAX_VALUE_SIZE: usize = 128 * 1024; -const MAX_KEYS: usize = 128; -const MAX_PUT_PAYLOAD_SIZE: usize = 976 * 1024; +pub const MAX_KEY_SIZE: usize = 2 * 1024; +pub const MAX_VALUE_SIZE: usize = 128 * 1024; +pub const MAX_KEYS: usize = 128; +pub const MAX_PUT_PAYLOAD_SIZE: usize = 976 * 1024; const MAX_STORAGE_SIZE: usize = 10 * 1024 * 1024 * 1024; // 10 GiB const VALUE_CHUNK_SIZE: usize = 10_000; // 10 KB, not KiB, see https://apple.github.io/foundationdb/blob.html @@ -46,9 +48,11 @@ pub async fn get( recipient: &Recipient, keys: Vec, ) -> Result<(Vec, Vec, Vec)> { + let start = std::time::Instant::now(); +metrics::ACTOR_KV_KEYS_PER_OP.with_label_values(&["get"]).observe(keys.len() as f64); validate_keys(&keys)?; - db.run(|tx| { + let result = db.run(|tx| { let keys = keys.clone(); async move { let tx = tx.with_subspace(keys::actor_kv::subspace(recipient.actor_id)); @@ -138,7 +142,9 @@ pub async fn get( }) .custom_instrument(tracing::info_span!("kv_get_tx")) .await - .map_err(Into::::into) + .map_err(Into::::into); + metrics::ACTOR_KV_OPERATION_DURATION.with_label_values(&["get"]).observe(start.elapsed().as_secs_f64()); + result } /// Gets keys from the KV store. @@ -261,9 +267,11 @@ pub async fn put( keys: Vec, values: Vec, ) -> Result<()> { + let start = std::time::Instant::now(); +metrics::ACTOR_KV_KEYS_PER_OP.with_label_values(&["put"]).observe(keys.len() as f64); let keys = &keys; let values = &values; - db.run(|tx| { + let result = db.run(|tx| { async move { let total_size = estimate_kv_size(&tx, recipient.actor_id).await? as usize; @@ -331,7 +339,9 @@ pub async fn put( }) .custom_instrument(tracing::info_span!("kv_put_tx")) .await - .map_err(Into::into) + .map_err(Into::into); + metrics::ACTOR_KV_OPERATION_DURATION.with_label_values(&["put"]).observe(start.elapsed().as_secs_f64()); + result } /// Deletes keys from the KV store. Cannot be undone. @@ -341,10 +351,12 @@ pub async fn delete( recipient: &Recipient, keys: Vec, ) -> Result<()> { + let start = std::time::Instant::now(); +metrics::ACTOR_KV_KEYS_PER_OP.with_label_values(&["delete"]).observe(keys.len() as f64); validate_keys(&keys)?; let keys = &keys; - db.run(|tx| { + let result = db.run(|tx| { async move { // Total written bytes (rounded up to nearest chunk) let total_size = keys.iter().fold(0, |s, key| s + key.len()); @@ -370,7 +382,9 @@ pub async fn delete( }) .custom_instrument(tracing::info_span!("kv_delete_tx")) .await - .map_err(Into::into) + .map_err(Into::into); + metrics::ACTOR_KV_OPERATION_DURATION.with_label_values(&["delete"]).observe(start.elapsed().as_secs_f64()); + result } /// Deletes all keys in the half-open range [start, end). Cannot be undone. @@ -381,12 +395,14 @@ pub async fn delete_range( start: ep::KvKey, end: ep::KvKey, ) -> Result<()> { - validate_range(&start, &end)?; + let timer = std::time::Instant::now(); +validate_range(&start, &end)?; if start >= end { + metrics::ACTOR_KV_OPERATION_DURATION.with_label_values(&["delete_range"]).observe(timer.elapsed().as_secs_f64()); return Ok(()); } - db.run(|tx| { + let result = db.run(|tx| { let start = start.clone(); let end = end.clone(); async move { @@ -417,7 +433,9 @@ pub async fn delete_range( }) .custom_instrument(tracing::info_span!("kv_delete_range_tx")) .await - .map_err(Into::into) + .map_err(Into::into); + metrics::ACTOR_KV_OPERATION_DURATION.with_label_values(&["delete_range"]).observe(timer.elapsed().as_secs_f64()); + result } /// Deletes all keys from the KV store. Cannot be undone. diff --git a/engine/packages/pegboard/src/actor_kv/preload.rs b/engine/packages/pegboard/src/actor_kv/preload.rs new file mode 100644 index 0000000000..3b7f4cd824 --- /dev/null +++ b/engine/packages/pegboard/src/actor_kv/preload.rs @@ -0,0 +1,363 @@ +use anyhow::Result; +use futures_util::TryStreamExt; +use gas::prelude::*; +use rivet_config::config::pegboard::Pegboard; +use rivet_envoy_protocol as ep; +use serde::Deserialize; +use universaldb::prelude::*; +use universaldb::tuple::Subspace; + +use super::entry::EntryBuilder; +use crate::keys; + +/// Request to preload a prefix range from the actor's KV store. +pub struct PreloadPrefixRequest { + /// The raw key prefix bytes (e.g., [2] for connections, [8] for SQLite). + pub prefix: ep::KvKey, + /// Maximum bytes to preload for this prefix. + pub max_bytes: u64, + /// If true, return whatever fits even if truncated (for per-key lookup subsystems + /// like SQLite VFS). If false, return nothing if the total data exceeds max_bytes + /// (for list-based subsystems like connections and workflows). + pub partial: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PreloadConfig { + #[serde(default)] + keys: Vec>, + #[serde(default)] + prefixes: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct PreloadPrefixConfig { + prefix: Vec, + max_bytes: u64, + #[serde(default)] + partial: bool, +} + +/// Fetches all preload data for an actor in a single FDB snapshot transaction. +/// +/// Reads exact get-keys and prefix ranges, reassembles chunked FDB values using +/// EntryBuilder, strips tuple encoding via KeyWrapper::unpack, and returns raw +/// byte key-value pairs ready for TypeScript consumption. +/// +/// Prefix requests should be passed in descending priority order (highest priority +/// first). When the global byte cap is reached, lower-priority prefixes are +/// truncated first. +#[tracing::instrument(skip_all)] +pub(crate) async fn batch_preload( + db: &universaldb::Database, + actor_id: Id, + get_keys: Vec, + prefix_requests: Vec, + max_total_bytes: u64, +) -> Result { + let subspace = keys::actor_kv::subspace(actor_id); + + // Break prefix_requests into separate vectors so they can be cloned for the + // FDB transaction closure (which may retry on conflicts). + let prefix_keys: Vec = prefix_requests.iter().map(|r| r.prefix.clone()).collect(); + let prefix_params: Vec<(u64, bool)> = prefix_requests + .iter() + .map(|r| (r.max_bytes, r.partial)) + .collect(); + + db.run(|tx| { + let subspace = subspace.clone(); + let get_keys = get_keys.clone(); + let prefix_keys = prefix_keys.clone(); + let prefix_params = prefix_params.clone(); + + async move { + let tx = tx.with_subspace(subspace.clone()); + let mut entries = Vec::new(); + let mut total_bytes: u64 = 0; + + // Build requested lists dynamically so they only contain keys/prefixes + // that were actually scanned. Keys or prefixes skipped due to budget + // exhaustion or disabled config must not appear, otherwise the actor + // would mistake "not scanned" for "scanned and not found". + let mut requested_get_keys: Vec = Vec::new(); + let mut requested_prefixes: Vec = Vec::new(); + + // 1. Read exact get-keys. Each key maps to a single logical entry + // (or nothing if the key doesn't exist in FDB). + for key in &get_keys { + if total_bytes >= max_total_bytes { + tracing::debug!( + skipped_keys = get_keys.len() - requested_get_keys.len(), + "preload get-keys skipped due to global budget exhaustion" + ); + break; + } + + // Mark this key as scanned regardless of whether it exists in FDB. + requested_get_keys.push(key.clone()); + + let key_subspace = + subspace.subspace(&keys::actor_kv::KeyWrapper(key.clone())); + let mut stream = tx.get_ranges_keyvalues( + universaldb::RangeOption { + mode: universaldb::options::StreamingMode::WantAll, + ..key_subspace.range().into() + }, + Snapshot, + ); + + let mut builder: Option = None; + + while let Some(fdb_kv) = stream.try_next().await? { + if builder.is_none() { + let parsed_key = + tx.unpack::(&fdb_kv.key())? + .key; + builder = Some(EntryBuilder::new(parsed_key)); + } + + let b = builder.as_mut().unwrap(); + + if let Ok(chunk_key) = + tx.unpack::(&fdb_kv.key()) + { + b.append_chunk(chunk_key.chunk, fdb_kv.value()); + } else if let Ok(metadata_key) = + tx.unpack::(&fdb_kv.key()) + { + let metadata = metadata_key.deserialize(fdb_kv.value())?; + b.append_metadata(metadata); + } else { + bail!("unexpected sub key in preload get"); + } + } + + if let Some(b) = builder { + let (k, v, m) = b.build()?; + let size = entry_size(&k, &v, &m); + if total_bytes + size <= max_total_bytes { + total_bytes += size; + entries.push(ep::PreloadedKvEntry { + key: k, + value: v, + metadata: m, + }); + } + } + } + + // 2. Read prefix ranges in priority order. Each prefix is bounded by + // its per-prefix max_bytes and the remaining global budget. + for (i, prefix) in prefix_keys.iter().enumerate() { + let (max_bytes, partial) = prefix_params[i]; + + // Skip prefixes disabled by config (max_bytes == 0) or when + // global budget is exhausted. Do not include in requested_prefixes + // so the actor falls back to kvListPrefix. + let remaining_budget = max_total_bytes.saturating_sub(total_bytes); + let effective_limit = max_bytes.min(remaining_budget); + + if effective_limit == 0 { + tracing::debug!( + ?prefix, + max_bytes, + remaining_budget, + "preload prefix skipped, effective limit is 0" + ); + continue; + } + + let range = prefix_range(prefix, &subspace); + let mut stream = tx.get_ranges_keyvalues( + universaldb::RangeOption { + mode: universaldb::options::StreamingMode::Iterator, + ..range.into() + }, + Snapshot, + ); + + let mut prefix_entries: Vec = Vec::new(); + let mut prefix_bytes: u64 = 0; + let mut current_entry: Option = None; + let mut exceeded = false; + + while let Some(fdb_kv) = stream.try_next().await? { + let key = + tx.unpack::(&fdb_kv.key())?.key; + + let curr = if let Some(inner) = &mut current_entry { + if inner.key != key { + // Finalize the previous entry. + let prev = + std::mem::replace(inner, EntryBuilder::new(key)); + let (k, v, m) = prev.build()?; + let size = entry_size(&k, &v, &m); + + if prefix_bytes + size > effective_limit { + exceeded = true; + break; + } + + prefix_bytes += size; + prefix_entries.push(ep::PreloadedKvEntry { + key: k, + value: v, + metadata: m, + }); + } + + inner + } else { + current_entry = Some(EntryBuilder::new(key)); + current_entry.as_mut().expect("just set") + }; + + if let Ok(chunk_key) = + tx.unpack::(&fdb_kv.key()) + { + curr.append_chunk(chunk_key.chunk, fdb_kv.value()); + } else if let Ok(metadata_key) = + tx.unpack::(&fdb_kv.key()) + { + let metadata = metadata_key.deserialize(fdb_kv.value())?; + curr.append_metadata(metadata); + } else { + bail!("unexpected sub key in preload prefix scan"); + } + } + + // Finalize the last entry if the stream ended without exceeding. + if !exceeded { + if let Some(b) = current_entry { + let (k, v, m) = b.build()?; + let size = entry_size(&k, &v, &m); + if prefix_bytes + size > effective_limit { + exceeded = true; + } else { + prefix_bytes += size; + prefix_entries.push(ep::PreloadedKvEntry { + key: k, + value: v, + metadata: m, + }); + } + } + } + + // For non-partial prefixes, discard all entries if the data exceeded + // the limit. The subsystem will fall back to a full kvListPrefix. + // Do not include in requested_prefixes so the actor knows to fall back. + if exceeded && !partial { + tracing::debug!( + ?prefix, + prefix_bytes, + effective_limit, + "preload prefix truncated (partial: false), discarding entries" + ); + continue; + } + + if exceeded { + tracing::debug!( + ?prefix, + prefix_bytes, + effective_limit, + "preload prefix truncated (partial: true), keeping partial data" + ); + } + + requested_prefixes.push(prefix.clone()); + total_bytes += prefix_bytes; + entries.extend(prefix_entries); + } + + Ok(ep::PreloadedKv { + entries, + requested_get_keys, + requested_prefixes, + }) + } + }) + .custom_instrument(tracing::info_span!("kv_batch_preload_tx")) + .await + .map_err(Into::::into) +} + +/// Fetches preloaded KV data for an actor using engine config and actor name +/// metadata. Returns `None` if preloading is disabled or missing from actor +/// metadata. Fails if the FDB transaction fails (no silent fallback). +#[tracing::instrument(skip_all)] +pub async fn fetch_preloaded_kv( + db: &universaldb::Database, + config: &Pegboard, + actor_id: Id, + namespace_id: Id, + actor_name: &str, +) -> Result> { + // Read actor name metadata from FDB. + let metadata = db + .run(|tx| { + let tx = tx.with_subspace(keys::subspace()); + let name_key = + keys::ns::ActorNameKey::new(namespace_id, actor_name.to_string()); + async move { tx.read_opt(&name_key, Snapshot).await } + }) + .await?; + + let metadata_map = metadata + .map(|d: rivet_data::converted::ActorNameKeyData| d.metadata) + .unwrap_or_default(); + + let Some(preload_config) = metadata_map + .get("preload") + .and_then(|value| serde_json::from_value::(value.clone()).ok()) + else { + return Ok(None); + }; + + if config.preload_max_total_bytes() == 0 { + return Ok(None); + }; + + let prefix_requests = preload_config + .prefixes + .into_iter() + .map(|prefix| PreloadPrefixRequest { + prefix: prefix.prefix, + max_bytes: prefix.max_bytes, + partial: prefix.partial, + }) + .collect(); + + let preloaded = batch_preload( + db, + actor_id, + preload_config.keys, + prefix_requests, + config.preload_max_total_bytes(), + ) + .await?; + + Ok(Some(preloaded)) +} + +/// Computes the serialized size of a preloaded KV entry, including key, value, +/// and metadata (version bytes + i64 timestamp). +fn entry_size(key: &ep::KvKey, value: &ep::KvValue, metadata: &ep::KvMetadata) -> u64 { + (key.len() + value.len() + metadata.version.len() + std::mem::size_of::()) as u64 +} + +/// Computes the FDB key range for a prefix scan within the actor KV subspace. +fn prefix_range(prefix: &ep::KvKey, subspace: &Subspace) -> (Vec, Vec) { + let mut start = subspace.pack(&keys::actor_kv::ListKeyWrapper(prefix.clone())); + // Remove the trailing 0 byte that tuple encoding adds to bytes. + if let Some(&0) = start.last() { + start.pop(); + } + let mut end = start.clone(); + end.push(0xFF); + (start, end) +} diff --git a/engine/packages/pegboard/src/actor_kv/utils.rs b/engine/packages/pegboard/src/actor_kv/utils.rs index 93be4d6d8a..ed91ade151 100644 --- a/engine/packages/pegboard/src/actor_kv/utils.rs +++ b/engine/packages/pegboard/src/actor_kv/utils.rs @@ -5,6 +5,7 @@ use super::{ MAX_KEY_SIZE, MAX_KEYS, MAX_PUT_PAYLOAD_SIZE, MAX_STORAGE_SIZE, MAX_VALUE_SIZE, keys::actor_kv::KeyWrapper, }; +use crate::errors; pub fn validate_list_query(query: &ep::KvListQuery) -> Result<()> { match query { @@ -74,10 +75,13 @@ pub fn validate_entries( ); let storage_remaining = MAX_STORAGE_SIZE.saturating_sub(total_size); - ensure!( - payload_size <= storage_remaining, - "not enough space left in storage ({storage_remaining} bytes remaining, current payload is {payload_size} bytes)" - ); + if payload_size > storage_remaining { + return Err(errors::Actor::KvStorageQuotaExceeded { + remaining: storage_remaining, + payload_size, + } + .build()); + } for key in keys { ensure!( diff --git a/engine/packages/pegboard/src/errors.rs b/engine/packages/pegboard/src/errors.rs index 6744bce7c8..f59b03a2b6 100644 --- a/engine/packages/pegboard/src/errors.rs +++ b/engine/packages/pegboard/src/errors.rs @@ -68,6 +68,13 @@ pub enum Actor { #[error("kv_key_not_found", "The KV key does not exist for this actor.")] KvKeyNotFound, + + #[error( + "kv_storage_quota_exceeded", + "Not enough space left in storage.", + "Not enough space left in storage ({remaining} bytes remaining, current payload is {payload_size} bytes)." + )] + KvStorageQuotaExceeded { remaining: usize, payload_size: usize }, } #[derive(RivetError, Debug, Clone, Deserialize, Serialize)] diff --git a/engine/packages/pegboard/src/ops/actor/get_for_runner.rs b/engine/packages/pegboard/src/ops/actor/get_for_runner.rs index c3ead1f566..b34639ced2 100644 --- a/engine/packages/pegboard/src/ops/actor/get_for_runner.rs +++ b/engine/packages/pegboard/src/ops/actor/get_for_runner.rs @@ -11,6 +11,7 @@ pub struct Input { #[derive(Debug)] pub struct Output { pub name: String, + pub namespace_id: Id, pub runner_id: Id, pub is_connectable: bool, } @@ -28,26 +29,28 @@ pub async fn pegboard_actor_get_for_runner( let workflow_id_key = keys::actor::WorkflowIdKey::new(input.actor_id); let name_key = keys::actor::NameKey::new(input.actor_id); + let namespace_id_key = keys::actor::NamespaceIdKey::new(input.actor_id); let runner_id_key = keys::actor::RunnerIdKey::new(input.actor_id); let connectable_key = keys::actor::ConnectableKey::new(input.actor_id); - let (workflow_id, name_entry, runner_id_entry, is_connectable) = tokio::try_join!( + let (workflow_id, name_entry, namespace_id, runner_id_entry, is_connectable) = tokio::try_join!( tx.read_opt(&workflow_id_key, Serializable), tx.read_opt(&name_key, Serializable), + tx.read_opt(&namespace_id_key, Serializable), tx.read_opt(&runner_id_key, Serializable), tx.exists(&connectable_key, Serializable), )?; - let (Some(workflow_id), Some(runner_id)) = (workflow_id, runner_id_entry) else { + let (Some(workflow_id), Some(namespace_id), Some(runner_id)) = (workflow_id, namespace_id, runner_id_entry) else { return Ok(None); }; - Ok(Some((workflow_id, name_entry, runner_id, is_connectable))) + Ok(Some((workflow_id, name_entry, namespace_id, runner_id, is_connectable))) }) .custom_instrument(tracing::info_span!("actor_get_for_runner_tx")) .await?; - let Some((workflow_id, name, runner_id, is_connectable)) = res else { + let Some((workflow_id, name, namespace_id, runner_id, is_connectable)) = res else { return Ok(None); }; @@ -81,6 +84,7 @@ pub async fn pegboard_actor_get_for_runner( Ok(Some(Output { name, + namespace_id, runner_id, is_connectable, })) diff --git a/engine/packages/pegboard/src/workflows/actor2/runtime.rs b/engine/packages/pegboard/src/workflows/actor2/runtime.rs index 5e08ecd023..5986573e15 100644 --- a/engine/packages/pegboard/src/workflows/actor2/runtime.rs +++ b/engine/packages/pegboard/src/workflows/actor2/runtime.rs @@ -366,6 +366,7 @@ pub async fn send_outbound(ctx: &ActivityCtx, input: &SendOutboundInput) -> Resu // Empty because request ids are ephemeral. This is intercepted by guard and // populated before it reaches the runner hibernating_requests: Vec::new(), + preloaded_kv: None, }); // NOTE: Kinda jank but it works @@ -965,25 +966,66 @@ pub async fn insert_and_send_commands( state.envoy_last_command_idx += input.commands.len() as i64; + // Fetch preloaded KV at send time for any start commands. Preloaded KV is + // never persisted in the command queue or workflow history. + let preloaded_kv = { + let has_start_cmd = input + .commands + .iter() + .any(|command| matches!(command, protocol::Command::CommandStartActor(_))); + if has_start_cmd { + let db = ctx.udb()?; + crate::actor_kv::preload::fetch_preloaded_kv( + &db, + ctx.config().pegboard(), + actor_id, + namespace_id, + &input + .commands + .iter() + .find_map(|command| match command { + protocol::Command::CommandStartActor(start) => { + Some(start.config.name.clone()) + } + _ => None, + }) + .unwrap_or_default(), + ) + .await? + } else { + None + } + }; + let receiver_subject = crate::pubsub_subjects::EnvoyReceiverSubject::new( state.namespace_id, input.envoy_key.clone(), ) .to_string(); + let mut preloaded_kv = preloaded_kv; let message_serialized = versioned::ToEnvoyConn::wrap_latest(protocol::ToEnvoyConn::ToEnvoyCommands( input .commands .iter() .enumerate() - .map(|(i, command)| protocol::CommandWrapper { - checkpoint: protocol::ActorCheckpoint { - actor_id: state.actor_id.to_string(), - generation: input.generation, - index: old_last_command_idx + i as i64 + 1, - }, - inner: command.clone(), + .map(|(i, command)| { + let mut command = command.clone(); + if let protocol::Command::CommandStartActor(ref mut start) = + command + { + start.preloaded_kv = preloaded_kv.take(); + } + + protocol::CommandWrapper { + checkpoint: protocol::ActorCheckpoint { + actor_id: state.actor_id.to_string(), + generation: input.generation, + index: old_last_command_idx + i as i64 + 1, + }, + inner: command, + } }) .collect(), )) diff --git a/engine/sdks/rust/kv-channel-protocol/Cargo.toml b/engine/sdks/rust/kv-channel-protocol/Cargo.toml new file mode 100644 index 0000000000..7991d5772b --- /dev/null +++ b/engine/sdks/rust/kv-channel-protocol/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rivet-kv-channel-protocol" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +serde_bare.workspace = true +serde.workspace = true +vbare.workspace = true + +[build-dependencies] +vbare-compiler.workspace = true diff --git a/engine/sdks/rust/kv-channel-protocol/build.rs b/engine/sdks/rust/kv-channel-protocol/build.rs new file mode 100644 index 0000000000..7867d7183a --- /dev/null +++ b/engine/sdks/rust/kv-channel-protocol/build.rs @@ -0,0 +1,157 @@ +use std::path::Path; + +fn main() -> Result<(), Box> { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?; + let workspace_root = Path::new(&manifest_dir) + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .ok_or("Failed to find workspace root")?; + + let schema_dir = workspace_root + .join("sdks") + .join("schemas") + .join("kv-channel-protocol"); + + // Rust type generation from BARE schemas. + let cfg = vbare_compiler::Config::with_hashable_map(); + vbare_compiler::process_schemas_with_config(&schema_dir, &cfg)?; + + // TypeScript SDK generation. + let cli_js_path = workspace_root + .parent() + .unwrap() + .join("node_modules/@bare-ts/tools/dist/bin/cli.js"); + if cli_js_path.exists() { + typescript::generate_sdk(&schema_dir, workspace_root); + } else { + println!( + "cargo:warning=TypeScript SDK generation skipped: cli.js not found at {}. Run `pnpm install` to install.", + cli_js_path.display() + ); + } + + Ok(()) +} + +mod typescript { + use super::*; + use std::{fs, path::PathBuf, process::Command}; + + pub fn generate_sdk(schema_dir: &Path, workspace_root: &Path) { + let sdk_dir = workspace_root + .join("sdks") + .join("typescript") + .join("kv-channel-protocol"); + let src_dir = sdk_dir.join("src"); + + let highest_version_path = find_highest_version(schema_dir); + + let _ = fs::remove_dir_all(&src_dir); + if let Err(e) = fs::create_dir_all(&src_dir) { + panic!("Failed to create SDK directory: {}", e); + } + + let output_path = src_dir.join("index.ts"); + + let output = Command::new( + workspace_root + .parent() + .unwrap() + .join("node_modules/@bare-ts/tools/dist/bin/cli.js"), + ) + .arg("compile") + .arg("--generator") + .arg("ts") + .arg(&highest_version_path) + .arg("-o") + .arg(&output_path) + .output() + .expect("Failed to execute bare compiler for TypeScript"); + + if !output.status.success() { + panic!( + "BARE TypeScript generation failed: {}", + String::from_utf8_lossy(&output.stderr), + ); + } + + // Post-process the generated TypeScript file. + // IMPORTANT: Keep this in sync with rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts + post_process_generated_ts(&output_path); + } + + const POST_PROCESS_MARKER: &str = "// @generated - post-processed by build.rs\n"; + + /// Post-process the generated TypeScript file to: + /// 1. Replace @bare-ts/lib import with @rivetkit/bare-ts + /// 2. Replace Node.js assert import with a custom assert function + /// + /// IMPORTANT: Keep this in sync with rivetkit-typescript/packages/rivetkit/scripts/compile-bare.ts + fn post_process_generated_ts(path: &Path) { + let content = fs::read_to_string(path).expect("Failed to read generated TypeScript file"); + + // Skip if already post-processed. + if content.starts_with(POST_PROCESS_MARKER) { + return; + } + + // Add PROTOCOL_VERSION constant at the top (not in the BARE schema). + let content = format!("export const PROTOCOL_VERSION = 1;\n\n{content}"); + + // Replace @bare-ts/lib with @rivetkit/bare-ts. + let content = content.replace("@bare-ts/lib", "@rivetkit/bare-ts"); + + // Replace Node.js assert import with custom assert function. + let content = content.replace("import assert from \"assert\"", ""); + let content = content.replace("import assert from \"node:assert\"", ""); + + // Append custom assert function. + let assert_function = r#" +function assert(condition: boolean, message?: string): asserts condition { + if (!condition) throw new Error(message ?? "Assertion failed") +} +"#; + let content = format!("{}{}\n{}", POST_PROCESS_MARKER, content, assert_function); + + // Validate post-processing succeeded. + assert!( + !content.contains("@bare-ts/lib"), + "Failed to replace @bare-ts/lib import" + ); + assert!( + !content.contains("import assert from"), + "Failed to remove Node.js assert import" + ); + + fs::write(path, content).expect("Failed to write post-processed TypeScript file"); + } + + fn find_highest_version(schema_dir: &Path) -> PathBuf { + let mut highest_version = 0; + let mut highest_version_path = PathBuf::new(); + + for entry in fs::read_dir(schema_dir).unwrap().flatten() { + if !entry.path().is_dir() { + let path = entry.path(); + let bare_name = path + .file_name() + .unwrap() + .to_str() + .unwrap() + .split_once('.') + .unwrap() + .0; + + if let Ok(version) = bare_name[1..].parse::() { + if version > highest_version { + highest_version = version; + highest_version_path = path; + } + } + } + } + + highest_version_path + } +} diff --git a/engine/sdks/rust/kv-channel-protocol/src/generated.rs b/engine/sdks/rust/kv-channel-protocol/src/generated.rs new file mode 100644 index 0000000000..84801af8dc --- /dev/null +++ b/engine/sdks/rust/kv-channel-protocol/src/generated.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/combined_imports.rs")); diff --git a/engine/sdks/rust/kv-channel-protocol/src/lib.rs b/engine/sdks/rust/kv-channel-protocol/src/lib.rs new file mode 100644 index 0000000000..7502faf079 --- /dev/null +++ b/engine/sdks/rust/kv-channel-protocol/src/lib.rs @@ -0,0 +1,138 @@ +pub mod generated; + +// Re-export latest version. +pub use generated::v1::*; + +pub const PROTOCOL_VERSION: u32 = 1; + +/// Serialize a ToServer message to BARE bytes. +pub fn encode_to_server(msg: &ToServer) -> Result, serde_bare::error::Error> { + serde_bare::to_vec(msg) +} + +/// Deserialize a ToServer message from BARE bytes. +pub fn decode_to_server(bytes: &[u8]) -> Result { + serde_bare::from_slice(bytes) +} + +/// Serialize a ToClient message to BARE bytes. +pub fn encode_to_client(msg: &ToClient) -> Result, serde_bare::error::Error> { + serde_bare::to_vec(msg) +} + +/// Deserialize a ToClient message from BARE bytes. +pub fn decode_to_client(bytes: &[u8]) -> Result { + serde_bare::from_slice(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + // MARK: Round-trip tests + + #[test] + fn round_trip_to_server_request_actor_open() { + let msg = ToServer::ToServerRequest(ToServerRequest { + request_id: 1, + actor_id: "abc".into(), + data: RequestData::ActorOpenRequest, + }); + let bytes = encode_to_server(&msg).unwrap(); + let decoded = decode_to_server(&bytes).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn round_trip_to_server_request_kv_get() { + let msg = ToServer::ToServerRequest(ToServerRequest { + request_id: 3, + actor_id: "actor1".into(), + data: RequestData::KvGetRequest(KvGetRequest { + keys: vec![vec![1, 2, 3], vec![4, 5]], + }), + }); + let bytes = encode_to_server(&msg).unwrap(); + let decoded = decode_to_server(&bytes).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn round_trip_to_client_response_error() { + let msg = ToClient::ToClientResponse(ToClientResponse { + request_id: 10, + data: ResponseData::ErrorResponse(ErrorResponse { + code: "actor_locked".into(), + message: "actor is locked by another connection".into(), + }), + }); + let bytes = encode_to_client(&msg).unwrap(); + let decoded = decode_to_client(&bytes).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn round_trip_to_client_ping() { + let msg = ToClient::ToClientPing(ToClientPing { ts: 9876543210 }); + let bytes = encode_to_client(&msg).unwrap(); + let decoded = decode_to_client(&bytes).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] + fn round_trip_to_client_close() { + let msg = ToClient::ToClientClose; + let bytes = encode_to_client(&msg).unwrap(); + let decoded = decode_to_client(&bytes).unwrap(); + assert_eq!(msg, decoded); + } + + // MARK: Cross-language byte compatibility tests + + #[test] + fn bytes_to_server_request_actor_open() { + let msg = ToServer::ToServerRequest(ToServerRequest { + request_id: 1, + actor_id: "abc".into(), + data: RequestData::ActorOpenRequest, + }); + let bytes = encode_to_server(&msg).unwrap(); + assert_eq!( + bytes, + [0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x61, 0x62, 0x63, 0x00] + ); + } + + #[test] + fn bytes_to_server_pong() { + let msg = ToServer::ToServerPong(ToServerPong { ts: 1234567890 }); + let bytes = encode_to_server(&msg).unwrap(); + assert_eq!( + bytes, + [0x01, 0xD2, 0x02, 0x96, 0x49, 0x00, 0x00, 0x00, 0x00] + ); + } + + #[test] + fn bytes_to_client_close() { + let msg = ToClient::ToClientClose; + let bytes = encode_to_client(&msg).unwrap(); + assert_eq!(bytes, [0x02]); + } + + #[test] + fn bytes_to_client_response_kv_get() { + let msg = ToClient::ToClientResponse(ToClientResponse { + request_id: 42, + data: ResponseData::KvGetResponse(KvGetResponse { + keys: vec![vec![1, 2]], + values: vec![vec![3, 4, 5]], + }), + }); + let bytes = encode_to_client(&msg).unwrap(); + assert_eq!( + bytes, + [0x00, 0x2A, 0x00, 0x00, 0x00, 0x03, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x03, 0x04, 0x05] + ); + } +} diff --git a/engine/sdks/schemas/envoy-protocol/v1.bare b/engine/sdks/schemas/envoy-protocol/v1.bare index 977f35d268..33d8de9876 100644 --- a/engine/sdks/schemas/envoy-protocol/v1.bare +++ b/engine/sdks/schemas/envoy-protocol/v1.bare @@ -174,6 +174,20 @@ type EventWrapper struct { inner: Event } +# MARK: Preloaded KV + +type PreloadedKvEntry struct { + key: KvKey + value: KvValue + metadata: KvMetadata +} + +type PreloadedKv struct { + entries: list + requestedGetKeys: list + requestedPrefixes: list +} + # MARK: Commands type HibernatingRequest struct { @@ -184,6 +198,7 @@ type HibernatingRequest struct { type CommandStartActor struct { config: ActorConfig hibernatingRequests: list + preloadedKv: optional } type StopActorReason enum { diff --git a/engine/sdks/schemas/kv-channel-protocol/v1.bare b/engine/sdks/schemas/kv-channel-protocol/v1.bare new file mode 100644 index 0000000000..1a393301d7 --- /dev/null +++ b/engine/sdks/schemas/kv-channel-protocol/v1.bare @@ -0,0 +1,146 @@ +# KV Channel Protocol v1 + +# MARK: Core + +# Id is a 30-character base36 string encoding the V1 format from +# engine/packages/util-id/. Use the util-id library for parsing +# and validation. Do not hand-roll Id parsing. +type Id str + +# MARK: Actor Session +# +# ActorOpen acquires a single-writer lock on an actor's KV data. +# ActorClose releases the lock. These are optimistic: the client +# does not wait for a response before sending KV requests. The +# server processes messages in WebSocket order, so the open is +# always processed before any KV requests that follow it. +# +# If the lock cannot be acquired (another connection holds it), +# the server sends an error response for the open and rejects +# subsequent KV requests for that actor with "actor_locked". + +# actorId is on ToServerRequest, not on open/close. The outer +# actorId is the single source of truth for routing. +type ActorOpenRequest void + +type ActorCloseRequest void + +type ActorOpenResponse void + +type ActorCloseResponse void + +# MARK: KV +# +# These types mirror the runner protocol KV types +# (engine/sdks/schemas/runner-protocol/). Changes to KV types in +# either protocol must be mirrored in the other. +# +# Omitted from the runner protocol (not needed by the VFS): +# - KvListRequest/KvListResponse (prefix scan) +# - KvDropRequest/KvDropResponse (drop all KV data) +# - KvMetadata on responses (update timestamps) +# +# The same engine KV limits apply to both protocols. See the +# "KV Limits" section below. + +type KvKey data +type KvValue data + +type KvGetRequest struct { + keys: list +} + +type KvPutRequest struct { + # keys and values are parallel lists. keys.len() must equal values.len(). + keys: list + values: list +} + +type KvDeleteRequest struct { + keys: list +} + +type KvDeleteRangeRequest struct { + start: KvKey + end: KvKey +} + +# MARK: Request/Response + +type RequestData union { + ActorOpenRequest | + ActorCloseRequest | + KvGetRequest | + KvPutRequest | + KvDeleteRequest | + KvDeleteRangeRequest +} + +type ErrorResponse struct { + code: str + message: str +} + +type KvGetResponse struct { + # Only keys that exist are returned. Missing keys are omitted. + # The client infers missing keys by comparing request keys to + # response keys. This matches the runner protocol behavior + # (engine/packages/pegboard/src/actor_kv/mod.rs). + keys: list + values: list +} + +type KvPutResponse void + +# KvDeleteResponse is used for both KvDeleteRequest and +# KvDeleteRangeRequest, same as the runner protocol. +type KvDeleteResponse void + +type ResponseData union { + ErrorResponse | + ActorOpenResponse | + ActorCloseResponse | + KvGetResponse | + KvPutResponse | + KvDeleteResponse +} + +# MARK: To Server + +type ToServerRequest struct { + requestId: u32 + actorId: Id + data: RequestData +} + +type ToServerPong struct { + ts: i64 +} + +type ToServer union { + ToServerRequest | + ToServerPong +} + +# MARK: To Client + +type ToClientResponse struct { + requestId: u32 + data: ResponseData +} + +type ToClientPing struct { + ts: i64 +} + +# Server-initiated close. Sent when the server is shutting down +# or draining connections. The client should close all actors +# and reconnect with backoff. Same pattern as the runner +# protocol's ToRunnerClose. +type ToClientClose void + +type ToClient union { + ToClientResponse | + ToClientPing | + ToClientClose +} diff --git a/engine/sdks/typescript/envoy-client/src/config.ts b/engine/sdks/typescript/envoy-client/src/config.ts index b20ed35aa2..37720ed78f 100644 --- a/engine/sdks/typescript/envoy-client/src/config.ts +++ b/engine/sdks/typescript/envoy-client/src/config.ts @@ -152,6 +152,7 @@ export interface EnvoyConfig { actorId: string, generation: number, config: protocol.ActorConfig, + preloadedKv: protocol.PreloadedKv | null, ) => Promise; onActorStop: ( diff --git a/engine/sdks/typescript/envoy-client/src/tasks/actor.ts b/engine/sdks/typescript/envoy-client/src/tasks/actor.ts index 8a8aff33fd..7c4243f6f9 100644 --- a/engine/sdks/typescript/envoy-client/src/tasks/actor.ts +++ b/engine/sdks/typescript/envoy-client/src/tasks/actor.ts @@ -18,8 +18,8 @@ export interface CreateActorOpts { actorId: string; generation: number; config: protocol.ActorConfig; - hibernatingRequests: readonly protocol.HibernatingRequest[]; + preloadedKv: protocol.PreloadedKv | null; } export type ToActor = @@ -123,7 +123,6 @@ async function actorInner( config: opts.config, commandIdx: 0n, eventIndex: 0n, - pendingRequests: new BufferMap(), webSockets: new BufferMap(), hibernationRestored: false, @@ -136,6 +135,7 @@ async function actorInner( opts.actorId, opts.generation, opts.config, + opts.preloadedKv, ); } catch (error) { log(ctx)?.error({ @@ -408,7 +408,7 @@ async function handleWsOpen(ctx: ActorContext, messageId: protocol.MessageId, pa ); try { - // #createWebSocket will call `runner.config.websocket` under the + // #createWebSocket will call `envoy.config.websocket` under the // hood to add the event listeners for open, etc. If this handler // throws, then the WebSocket will be closed before sending the // open event. @@ -539,7 +539,7 @@ async function handleHwsRestore(ctx: ActorContext, metaEntries: HibernatingWebSo } else { ctx.pendingRequests.set([gatewayId, requestId], { envoyMessageIndex: 0 }); - // This will call `runner.config.websocket` under the hood to + // This will call `envoy.config.websocket` under the hood to // attach the event listeners to the WebSocket. // Track this operation to ensure it completes const restoreOperation = createWebSocket( diff --git a/engine/sdks/typescript/envoy-client/src/tasks/envoy/commands.ts b/engine/sdks/typescript/envoy-client/src/tasks/envoy/commands.ts index 05e2770afd..7686e9ee87 100644 --- a/engine/sdks/typescript/envoy-client/src/tasks/envoy/commands.ts +++ b/engine/sdks/typescript/envoy-client/src/tasks/envoy/commands.ts @@ -26,6 +26,7 @@ export function handleCommands( generation: checkpoint.generation, config: val.config, hibernatingRequests: val.hibernatingRequests, + preloadedKv: val.preloadedKv ?? null, }); let generations = ctx.actors.get(checkpoint.actorId); diff --git a/engine/sdks/typescript/envoy-client/src/tasks/envoy/index.ts b/engine/sdks/typescript/envoy-client/src/tasks/envoy/index.ts index 67248242c4..9daefd8137 100644 --- a/engine/sdks/typescript/envoy-client/src/tasks/envoy/index.ts +++ b/engine/sdks/typescript/envoy-client/src/tasks/envoy/index.ts @@ -235,7 +235,7 @@ function handleConnClose(ctx: EnvoyContext, lostTimeout: NodeJS.Timeout | undefi if (!lostTimeout) { let lostThreshold = ctx.shared.protocolMetadata ? Number(ctx.shared.protocolMetadata.envoyLostThreshold) : 10000; log(ctx.shared)?.debug({ - msg: "starting runner lost timeout", + msg: "starting envoy lost timeout", seconds: lostThreshold / 1000, }); @@ -251,7 +251,7 @@ function handleConnClose(ctx: EnvoyContext, lostTimeout: NodeJS.Timeout | undefi if (ctx.actors.size == 0) return; log(ctx.shared)?.warn({ - msg: "stopping all actors due to runner lost threshold", + msg: "stopping all actors due to envoy lost threshold", }); // Stop all actors diff --git a/engine/sdks/typescript/envoy-client/src/websocket.ts b/engine/sdks/typescript/envoy-client/src/websocket.ts index 9001ca2a6d..bfd3bb4fe1 100644 --- a/engine/sdks/typescript/envoy-client/src/websocket.ts +++ b/engine/sdks/typescript/envoy-client/src/websocket.ts @@ -1,4 +1,4 @@ -import type * as protocol from "@rivetkit/engine-runner-protocol"; +import type * as protocol from "@rivetkit/engine-envoy-protocol"; import type { UnboundedReceiver, UnboundedSender } from "antiox/sync/mpsc"; import { OnceCell } from "antiox/sync/once_cell"; import { spawn } from "antiox/task"; diff --git a/engine/sdks/typescript/envoy-protocol/src/index.ts b/engine/sdks/typescript/envoy-protocol/src/index.ts index ba20853621..388e77fe35 100644 --- a/engine/sdks/typescript/envoy-protocol/src/index.ts +++ b/engine/sdks/typescript/envoy-protocol/src/index.ts @@ -865,6 +865,65 @@ export function writeEventWrapper(bc: bare.ByteCursor, x: EventWrapper): void { writeEvent(bc, x.inner) } +export type PreloadedKvEntry = { + readonly key: KvKey + readonly value: KvValue + readonly metadata: KvMetadata +} + +export function readPreloadedKvEntry(bc: bare.ByteCursor): PreloadedKvEntry { + return { + key: readKvKey(bc), + value: readKvValue(bc), + metadata: readKvMetadata(bc), + } +} + +export function writePreloadedKvEntry(bc: bare.ByteCursor, x: PreloadedKvEntry): void { + writeKvKey(bc, x.key) + writeKvValue(bc, x.value) + writeKvMetadata(bc, x.metadata) +} + +function read8(bc: bare.ByteCursor): readonly PreloadedKvEntry[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readPreloadedKvEntry(bc)] + for (let i = 1; i < len; i++) { + result[i] = readPreloadedKvEntry(bc) + } + return result +} + +function write8(bc: bare.ByteCursor, x: readonly PreloadedKvEntry[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writePreloadedKvEntry(bc, x[i]) + } +} + +export type PreloadedKv = { + readonly entries: readonly PreloadedKvEntry[] + readonly requestedGetKeys: readonly KvKey[] + readonly requestedPrefixes: readonly KvKey[] +} + +export function readPreloadedKv(bc: bare.ByteCursor): PreloadedKv { + return { + entries: read8(bc), + requestedGetKeys: read0(bc), + requestedPrefixes: read0(bc), + } +} + +export function writePreloadedKv(bc: bare.ByteCursor, x: PreloadedKv): void { + write8(bc, x.entries) + write0(bc, x.requestedGetKeys) + write0(bc, x.requestedPrefixes) +} + export type HibernatingRequest = { readonly gatewayId: GatewayId readonly requestId: RequestId @@ -882,7 +941,7 @@ export function writeHibernatingRequest(bc: bare.ByteCursor, x: HibernatingReque writeRequestId(bc, x.requestId) } -function read8(bc: bare.ByteCursor): readonly HibernatingRequest[] { +function read9(bc: bare.ByteCursor): readonly HibernatingRequest[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -894,28 +953,42 @@ function read8(bc: bare.ByteCursor): readonly HibernatingRequest[] { return result } -function write8(bc: bare.ByteCursor, x: readonly HibernatingRequest[]): void { +function write9(bc: bare.ByteCursor, x: readonly HibernatingRequest[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeHibernatingRequest(bc, x[i]) } } +function read10(bc: bare.ByteCursor): PreloadedKv | null { + return bare.readBool(bc) ? readPreloadedKv(bc) : null +} + +function write10(bc: bare.ByteCursor, x: PreloadedKv | null): void { + bare.writeBool(bc, x != null) + if (x != null) { + writePreloadedKv(bc, x) + } +} + export type CommandStartActor = { readonly config: ActorConfig readonly hibernatingRequests: readonly HibernatingRequest[] + readonly preloadedKv: PreloadedKv | null } export function readCommandStartActor(bc: bare.ByteCursor): CommandStartActor { return { config: readActorConfig(bc), - hibernatingRequests: read8(bc), + hibernatingRequests: read9(bc), + preloadedKv: read10(bc), } } export function writeCommandStartActor(bc: bare.ByteCursor, x: CommandStartActor): void { writeActorConfig(bc, x.config) - write8(bc, x.hibernatingRequests) + write9(bc, x.hibernatingRequests) + write10(bc, x.preloadedKv) } export enum StopActorReason { @@ -1122,7 +1195,7 @@ export function writeMessageId(bc: bare.ByteCursor, x: MessageId): void { writeMessageIndex(bc, x.messageIndex) } -function read9(bc: bare.ByteCursor): ReadonlyMap { +function read11(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -1137,7 +1210,7 @@ function read9(bc: bare.ByteCursor): ReadonlyMap { return result } -function write9(bc: bare.ByteCursor, x: ReadonlyMap): void { +function write11(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeString(bc, kv[0]) @@ -1162,7 +1235,7 @@ export function readToEnvoyRequestStart(bc: bare.ByteCursor): ToEnvoyRequestStar actorId: readId(bc), method: bare.readString(bc), path: bare.readString(bc), - headers: read9(bc), + headers: read11(bc), body: read6(bc), stream: bare.readBool(bc), } @@ -1172,7 +1245,7 @@ export function writeToEnvoyRequestStart(bc: bare.ByteCursor, x: ToEnvoyRequestS writeId(bc, x.actorId) bare.writeString(bc, x.method) bare.writeString(bc, x.path) - write9(bc, x.headers) + write11(bc, x.headers) write6(bc, x.body) bare.writeBool(bc, x.stream) } @@ -1206,7 +1279,7 @@ export type ToRivetResponseStart = { export function readToRivetResponseStart(bc: bare.ByteCursor): ToRivetResponseStart { return { status: bare.readU16(bc), - headers: read9(bc), + headers: read11(bc), body: read6(bc), stream: bare.readBool(bc), } @@ -1214,7 +1287,7 @@ export function readToRivetResponseStart(bc: bare.ByteCursor): ToRivetResponseSt export function writeToRivetResponseStart(bc: bare.ByteCursor, x: ToRivetResponseStart): void { bare.writeU16(bc, x.status) - write9(bc, x.headers) + write11(bc, x.headers) write6(bc, x.body) bare.writeBool(bc, x.stream) } @@ -1251,14 +1324,14 @@ export function readToEnvoyWebSocketOpen(bc: bare.ByteCursor): ToEnvoyWebSocketO return { actorId: readId(bc), path: bare.readString(bc), - headers: read9(bc), + headers: read11(bc), } } export function writeToEnvoyWebSocketOpen(bc: bare.ByteCursor, x: ToEnvoyWebSocketOpen): void { writeId(bc, x.actorId) bare.writeString(bc, x.path) - write9(bc, x.headers) + write11(bc, x.headers) } export type ToEnvoyWebSocketMessage = { @@ -1278,11 +1351,11 @@ export function writeToEnvoyWebSocketMessage(bc: bare.ByteCursor, x: ToEnvoyWebS bare.writeBool(bc, x.binary) } -function read10(bc: bare.ByteCursor): u16 | null { +function read12(bc: bare.ByteCursor): u16 | null { return bare.readBool(bc) ? bare.readU16(bc) : null } -function write10(bc: bare.ByteCursor, x: u16 | null): void { +function write12(bc: bare.ByteCursor, x: u16 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeU16(bc, x) @@ -1296,13 +1369,13 @@ export type ToEnvoyWebSocketClose = { export function readToEnvoyWebSocketClose(bc: bare.ByteCursor): ToEnvoyWebSocketClose { return { - code: read10(bc), + code: read12(bc), reason: read5(bc), } } export function writeToEnvoyWebSocketClose(bc: bare.ByteCursor, x: ToEnvoyWebSocketClose): void { - write10(bc, x.code) + write12(bc, x.code) write5(bc, x.reason) } @@ -1359,14 +1432,14 @@ export type ToRivetWebSocketClose = { export function readToRivetWebSocketClose(bc: bare.ByteCursor): ToRivetWebSocketClose { return { - code: read10(bc), + code: read12(bc), reason: read5(bc), hibernate: bare.readBool(bc), } } export function writeToRivetWebSocketClose(bc: bare.ByteCursor, x: ToRivetWebSocketClose): void { - write10(bc, x.code) + write12(bc, x.code) write5(bc, x.reason) bare.writeBool(bc, x.hibernate) } @@ -1575,7 +1648,7 @@ export function writeToEnvoyPing(bc: bare.ByteCursor, x: ToEnvoyPing): void { bare.writeI64(bc, x.ts) } -function read11(bc: bare.ByteCursor): ReadonlyMap { +function read13(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -1590,7 +1663,7 @@ function read11(bc: bare.ByteCursor): ReadonlyMap { return result } -function write11(bc: bare.ByteCursor, x: ReadonlyMap): void { +function write13(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeString(bc, kv[0]) @@ -1598,22 +1671,22 @@ function write11(bc: bare.ByteCursor, x: ReadonlyMap): void { } } -function read12(bc: bare.ByteCursor): ReadonlyMap | null { - return bare.readBool(bc) ? read11(bc) : null +function read14(bc: bare.ByteCursor): ReadonlyMap | null { + return bare.readBool(bc) ? read13(bc) : null } -function write12(bc: bare.ByteCursor, x: ReadonlyMap | null): void { +function write14(bc: bare.ByteCursor, x: ReadonlyMap | null): void { bare.writeBool(bc, x != null) if (x != null) { - write11(bc, x) + write13(bc, x) } } -function read13(bc: bare.ByteCursor): Json | null { +function read15(bc: bare.ByteCursor): Json | null { return bare.readBool(bc) ? readJson(bc) : null } -function write13(bc: bare.ByteCursor, x: Json | null): void { +function write15(bc: bare.ByteCursor, x: Json | null): void { bare.writeBool(bc, x != null) if (x != null) { writeJson(bc, x) @@ -1634,16 +1707,16 @@ export function readToRivetInit(bc: bare.ByteCursor): ToRivetInit { return { envoyKey: bare.readString(bc), version: bare.readU32(bc), - prepopulateActorNames: read12(bc), - metadata: read13(bc), + prepopulateActorNames: read14(bc), + metadata: read15(bc), } } export function writeToRivetInit(bc: bare.ByteCursor, x: ToRivetInit): void { bare.writeString(bc, x.envoyKey) bare.writeU32(bc, x.version) - write12(bc, x.prepopulateActorNames) - write13(bc, x.metadata) + write14(bc, x.prepopulateActorNames) + write15(bc, x.metadata) } export type ToRivetEvents = readonly EventWrapper[] @@ -1667,7 +1740,7 @@ export function writeToRivetEvents(bc: bare.ByteCursor, x: ToRivetEvents): void } } -function read14(bc: bare.ByteCursor): readonly ActorCheckpoint[] { +function read16(bc: bare.ByteCursor): readonly ActorCheckpoint[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -1679,7 +1752,7 @@ function read14(bc: bare.ByteCursor): readonly ActorCheckpoint[] { return result } -function write14(bc: bare.ByteCursor, x: readonly ActorCheckpoint[]): void { +function write16(bc: bare.ByteCursor, x: readonly ActorCheckpoint[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeActorCheckpoint(bc, x[i]) @@ -1692,12 +1765,12 @@ export type ToRivetAckCommands = { export function readToRivetAckCommands(bc: bare.ByteCursor): ToRivetAckCommands { return { - lastCommandCheckpoints: read14(bc), + lastCommandCheckpoints: read16(bc), } } export function writeToRivetAckCommands(bc: bare.ByteCursor, x: ToRivetAckCommands): void { - write14(bc, x.lastCommandCheckpoints) + write16(bc, x.lastCommandCheckpoints) } export type ToRivetStopping = null @@ -1895,12 +1968,12 @@ export type ToEnvoyAckEvents = { export function readToEnvoyAckEvents(bc: bare.ByteCursor): ToEnvoyAckEvents { return { - lastEventCheckpoints: read14(bc), + lastEventCheckpoints: read16(bc), } } export function writeToEnvoyAckEvents(bc: bare.ByteCursor, x: ToEnvoyAckEvents): void { - write14(bc, x.lastEventCheckpoints) + write16(bc, x.lastEventCheckpoints) } export type ToEnvoyKvResponse = { diff --git a/engine/sdks/typescript/kv-channel-protocol/package.json b/engine/sdks/typescript/kv-channel-protocol/package.json new file mode 100644 index 0000000000..79cb0822b9 --- /dev/null +++ b/engine/sdks/typescript/kv-channel-protocol/package.json @@ -0,0 +1,36 @@ +{ + "name": "@rivetkit/engine-kv-channel-protocol", + "version": "2.1.6", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "scripts": { + "build": "tsup src/index.ts", + "clean": "rm -rf dist", + "check-types": "tsc --noEmit" + }, + "types": "dist/index.d.ts", + "dependencies": { + "@rivetkit/bare-ts": "^0.6.2" + }, + "devDependencies": { + "@types/node": "^20.19.13", + "tsup": "^8.5.0", + "typescript": "^5.9.2" + } +} diff --git a/engine/sdks/typescript/kv-channel-protocol/src/index.ts b/engine/sdks/typescript/kv-channel-protocol/src/index.ts new file mode 100644 index 0000000000..229cde4a2f --- /dev/null +++ b/engine/sdks/typescript/kv-channel-protocol/src/index.ts @@ -0,0 +1,524 @@ +// @generated - post-processed by build.rs +export const PROTOCOL_VERSION = 1; + +import * as bare from "@rivetkit/bare-ts" + +const DEFAULT_CONFIG = /* @__PURE__ */ bare.Config({}) + +export type i64 = bigint +export type u32 = number + +/** + * Id is a 30-character base36 string encoding the V1 format from + * engine/packages/util-id/. Use the util-id library for parsing + * and validation. Do not hand-roll Id parsing. + */ +export type Id = string + +export function readId(bc: bare.ByteCursor): Id { + return bare.readString(bc) +} + +export function writeId(bc: bare.ByteCursor, x: Id): void { + bare.writeString(bc, x) +} + +/** + * actorId is on ToServerRequest, not on open/close. The outer + * actorId is the single source of truth for routing. + */ +export type ActorOpenRequest = null + +export type ActorCloseRequest = null + +export type ActorOpenResponse = null + +export type ActorCloseResponse = null + +export type KvKey = ArrayBuffer + +export function readKvKey(bc: bare.ByteCursor): KvKey { + return bare.readData(bc) +} + +export function writeKvKey(bc: bare.ByteCursor, x: KvKey): void { + bare.writeData(bc, x) +} + +export type KvValue = ArrayBuffer + +export function readKvValue(bc: bare.ByteCursor): KvValue { + return bare.readData(bc) +} + +export function writeKvValue(bc: bare.ByteCursor, x: KvValue): void { + bare.writeData(bc, x) +} + +function read0(bc: bare.ByteCursor): readonly KvKey[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readKvKey(bc)] + for (let i = 1; i < len; i++) { + result[i] = readKvKey(bc) + } + return result +} + +function write0(bc: bare.ByteCursor, x: readonly KvKey[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writeKvKey(bc, x[i]) + } +} + +export type KvGetRequest = { + readonly keys: readonly KvKey[] +} + +export function readKvGetRequest(bc: bare.ByteCursor): KvGetRequest { + return { + keys: read0(bc), + } +} + +export function writeKvGetRequest(bc: bare.ByteCursor, x: KvGetRequest): void { + write0(bc, x.keys) +} + +function read1(bc: bare.ByteCursor): readonly KvValue[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readKvValue(bc)] + for (let i = 1; i < len; i++) { + result[i] = readKvValue(bc) + } + return result +} + +function write1(bc: bare.ByteCursor, x: readonly KvValue[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writeKvValue(bc, x[i]) + } +} + +export type KvPutRequest = { + /** + * keys and values are parallel lists. keys.len() must equal values.len(). + */ + readonly keys: readonly KvKey[] + readonly values: readonly KvValue[] +} + +export function readKvPutRequest(bc: bare.ByteCursor): KvPutRequest { + return { + keys: read0(bc), + values: read1(bc), + } +} + +export function writeKvPutRequest(bc: bare.ByteCursor, x: KvPutRequest): void { + write0(bc, x.keys) + write1(bc, x.values) +} + +export type KvDeleteRequest = { + readonly keys: readonly KvKey[] +} + +export function readKvDeleteRequest(bc: bare.ByteCursor): KvDeleteRequest { + return { + keys: read0(bc), + } +} + +export function writeKvDeleteRequest(bc: bare.ByteCursor, x: KvDeleteRequest): void { + write0(bc, x.keys) +} + +export type KvDeleteRangeRequest = { + readonly start: KvKey + readonly end: KvKey +} + +export function readKvDeleteRangeRequest(bc: bare.ByteCursor): KvDeleteRangeRequest { + return { + start: readKvKey(bc), + end: readKvKey(bc), + } +} + +export function writeKvDeleteRangeRequest(bc: bare.ByteCursor, x: KvDeleteRangeRequest): void { + writeKvKey(bc, x.start) + writeKvKey(bc, x.end) +} + +export type RequestData = + | { readonly tag: "ActorOpenRequest"; readonly val: ActorOpenRequest } + | { readonly tag: "ActorCloseRequest"; readonly val: ActorCloseRequest } + | { readonly tag: "KvGetRequest"; readonly val: KvGetRequest } + | { readonly tag: "KvPutRequest"; readonly val: KvPutRequest } + | { readonly tag: "KvDeleteRequest"; readonly val: KvDeleteRequest } + | { readonly tag: "KvDeleteRangeRequest"; readonly val: KvDeleteRangeRequest } + +export function readRequestData(bc: bare.ByteCursor): RequestData { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return { tag: "ActorOpenRequest", val: null } + case 1: + return { tag: "ActorCloseRequest", val: null } + case 2: + return { tag: "KvGetRequest", val: readKvGetRequest(bc) } + case 3: + return { tag: "KvPutRequest", val: readKvPutRequest(bc) } + case 4: + return { tag: "KvDeleteRequest", val: readKvDeleteRequest(bc) } + case 5: + return { tag: "KvDeleteRangeRequest", val: readKvDeleteRangeRequest(bc) } + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeRequestData(bc: bare.ByteCursor, x: RequestData): void { + switch (x.tag) { + case "ActorOpenRequest": { + bare.writeU8(bc, 0) + break + } + case "ActorCloseRequest": { + bare.writeU8(bc, 1) + break + } + case "KvGetRequest": { + bare.writeU8(bc, 2) + writeKvGetRequest(bc, x.val) + break + } + case "KvPutRequest": { + bare.writeU8(bc, 3) + writeKvPutRequest(bc, x.val) + break + } + case "KvDeleteRequest": { + bare.writeU8(bc, 4) + writeKvDeleteRequest(bc, x.val) + break + } + case "KvDeleteRangeRequest": { + bare.writeU8(bc, 5) + writeKvDeleteRangeRequest(bc, x.val) + break + } + } +} + +export type ErrorResponse = { + readonly code: string + readonly message: string +} + +export function readErrorResponse(bc: bare.ByteCursor): ErrorResponse { + return { + code: bare.readString(bc), + message: bare.readString(bc), + } +} + +export function writeErrorResponse(bc: bare.ByteCursor, x: ErrorResponse): void { + bare.writeString(bc, x.code) + bare.writeString(bc, x.message) +} + +export type KvGetResponse = { + /** + * Only keys that exist are returned. Missing keys are omitted. + * The client infers missing keys by comparing request keys to + * response keys. This matches the runner protocol behavior + * (engine/packages/pegboard/src/actor_kv/mod.rs). + */ + readonly keys: readonly KvKey[] + readonly values: readonly KvValue[] +} + +export function readKvGetResponse(bc: bare.ByteCursor): KvGetResponse { + return { + keys: read0(bc), + values: read1(bc), + } +} + +export function writeKvGetResponse(bc: bare.ByteCursor, x: KvGetResponse): void { + write0(bc, x.keys) + write1(bc, x.values) +} + +export type KvPutResponse = null + +/** + * KvDeleteResponse is used for both KvDeleteRequest and + * KvDeleteRangeRequest, same as the runner protocol. + */ +export type KvDeleteResponse = null + +export type ResponseData = + | { readonly tag: "ErrorResponse"; readonly val: ErrorResponse } + | { readonly tag: "ActorOpenResponse"; readonly val: ActorOpenResponse } + | { readonly tag: "ActorCloseResponse"; readonly val: ActorCloseResponse } + | { readonly tag: "KvGetResponse"; readonly val: KvGetResponse } + | { readonly tag: "KvPutResponse"; readonly val: KvPutResponse } + | { readonly tag: "KvDeleteResponse"; readonly val: KvDeleteResponse } + +export function readResponseData(bc: bare.ByteCursor): ResponseData { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return { tag: "ErrorResponse", val: readErrorResponse(bc) } + case 1: + return { tag: "ActorOpenResponse", val: null } + case 2: + return { tag: "ActorCloseResponse", val: null } + case 3: + return { tag: "KvGetResponse", val: readKvGetResponse(bc) } + case 4: + return { tag: "KvPutResponse", val: null } + case 5: + return { tag: "KvDeleteResponse", val: null } + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeResponseData(bc: bare.ByteCursor, x: ResponseData): void { + switch (x.tag) { + case "ErrorResponse": { + bare.writeU8(bc, 0) + writeErrorResponse(bc, x.val) + break + } + case "ActorOpenResponse": { + bare.writeU8(bc, 1) + break + } + case "ActorCloseResponse": { + bare.writeU8(bc, 2) + break + } + case "KvGetResponse": { + bare.writeU8(bc, 3) + writeKvGetResponse(bc, x.val) + break + } + case "KvPutResponse": { + bare.writeU8(bc, 4) + break + } + case "KvDeleteResponse": { + bare.writeU8(bc, 5) + break + } + } +} + +export type ToServerRequest = { + readonly requestId: u32 + readonly actorId: Id + readonly data: RequestData +} + +export function readToServerRequest(bc: bare.ByteCursor): ToServerRequest { + return { + requestId: bare.readU32(bc), + actorId: readId(bc), + data: readRequestData(bc), + } +} + +export function writeToServerRequest(bc: bare.ByteCursor, x: ToServerRequest): void { + bare.writeU32(bc, x.requestId) + writeId(bc, x.actorId) + writeRequestData(bc, x.data) +} + +export type ToServerPong = { + readonly ts: i64 +} + +export function readToServerPong(bc: bare.ByteCursor): ToServerPong { + return { + ts: bare.readI64(bc), + } +} + +export function writeToServerPong(bc: bare.ByteCursor, x: ToServerPong): void { + bare.writeI64(bc, x.ts) +} + +export type ToServer = + | { readonly tag: "ToServerRequest"; readonly val: ToServerRequest } + | { readonly tag: "ToServerPong"; readonly val: ToServerPong } + +export function readToServer(bc: bare.ByteCursor): ToServer { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return { tag: "ToServerRequest", val: readToServerRequest(bc) } + case 1: + return { tag: "ToServerPong", val: readToServerPong(bc) } + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeToServer(bc: bare.ByteCursor, x: ToServer): void { + switch (x.tag) { + case "ToServerRequest": { + bare.writeU8(bc, 0) + writeToServerRequest(bc, x.val) + break + } + case "ToServerPong": { + bare.writeU8(bc, 1) + writeToServerPong(bc, x.val) + break + } + } +} + +export function encodeToServer(x: ToServer, config?: Partial): Uint8Array { + const fullConfig = config != null ? bare.Config(config) : DEFAULT_CONFIG + const bc = new bare.ByteCursor( + new Uint8Array(fullConfig.initialBufferLength), + fullConfig, + ) + writeToServer(bc, x) + return new Uint8Array(bc.view.buffer, bc.view.byteOffset, bc.offset) +} + +export function decodeToServer(bytes: Uint8Array): ToServer { + const bc = new bare.ByteCursor(bytes, DEFAULT_CONFIG) + const result = readToServer(bc) + if (bc.offset < bc.view.byteLength) { + throw new bare.BareError(bc.offset, "remaining bytes") + } + return result +} + +export type ToClientResponse = { + readonly requestId: u32 + readonly data: ResponseData +} + +export function readToClientResponse(bc: bare.ByteCursor): ToClientResponse { + return { + requestId: bare.readU32(bc), + data: readResponseData(bc), + } +} + +export function writeToClientResponse(bc: bare.ByteCursor, x: ToClientResponse): void { + bare.writeU32(bc, x.requestId) + writeResponseData(bc, x.data) +} + +export type ToClientPing = { + readonly ts: i64 +} + +export function readToClientPing(bc: bare.ByteCursor): ToClientPing { + return { + ts: bare.readI64(bc), + } +} + +export function writeToClientPing(bc: bare.ByteCursor, x: ToClientPing): void { + bare.writeI64(bc, x.ts) +} + +/** + * Server-initiated close. Sent when the server is shutting down + * or draining connections. The client should close all actors + * and reconnect with backoff. Same pattern as the runner + * protocol's ToRunnerClose. + */ +export type ToClientClose = null + +export type ToClient = + | { readonly tag: "ToClientResponse"; readonly val: ToClientResponse } + | { readonly tag: "ToClientPing"; readonly val: ToClientPing } + | { readonly tag: "ToClientClose"; readonly val: ToClientClose } + +export function readToClient(bc: bare.ByteCursor): ToClient { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return { tag: "ToClientResponse", val: readToClientResponse(bc) } + case 1: + return { tag: "ToClientPing", val: readToClientPing(bc) } + case 2: + return { tag: "ToClientClose", val: null } + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeToClient(bc: bare.ByteCursor, x: ToClient): void { + switch (x.tag) { + case "ToClientResponse": { + bare.writeU8(bc, 0) + writeToClientResponse(bc, x.val) + break + } + case "ToClientPing": { + bare.writeU8(bc, 1) + writeToClientPing(bc, x.val) + break + } + case "ToClientClose": { + bare.writeU8(bc, 2) + break + } + } +} + +export function encodeToClient(x: ToClient, config?: Partial): Uint8Array { + const fullConfig = config != null ? bare.Config(config) : DEFAULT_CONFIG + const bc = new bare.ByteCursor( + new Uint8Array(fullConfig.initialBufferLength), + fullConfig, + ) + writeToClient(bc, x) + return new Uint8Array(bc.view.buffer, bc.view.byteOffset, bc.offset) +} + +export function decodeToClient(bytes: Uint8Array): ToClient { + const bc = new bare.ByteCursor(bytes, DEFAULT_CONFIG) + const result = readToClient(bc) + if (bc.offset < bc.view.byteLength) { + throw new bare.BareError(bc.offset, "remaining bytes") + } + return result +} + + +function assert(condition: boolean, message?: string): asserts condition { + if (!condition) throw new Error(message ?? "Assertion failed") +} diff --git a/engine/sdks/typescript/kv-channel-protocol/tsconfig.json b/engine/sdks/typescript/kv-channel-protocol/tsconfig.json new file mode 100644 index 0000000000..d8dc820c55 --- /dev/null +++ b/engine/sdks/typescript/kv-channel-protocol/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist" + }, + "exclude": ["dist", "node_modules"], + "include": ["**/*.ts"] +} diff --git a/engine/sdks/typescript/kv-channel-protocol/tsup.config.ts b/engine/sdks/typescript/kv-channel-protocol/tsup.config.ts new file mode 100644 index 0000000000..2d399b5efe --- /dev/null +++ b/engine/sdks/typescript/kv-channel-protocol/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsup"; +import defaultConfig from "../../../../tsup.base"; + +export default defineConfig(defaultConfig); diff --git a/examples/kitchen-sink/scripts/bench-report.ts b/examples/kitchen-sink/scripts/bench-report.ts new file mode 100644 index 0000000000..49cb5d8829 --- /dev/null +++ b/examples/kitchen-sink/scripts/bench-report.ts @@ -0,0 +1,183 @@ +#!/usr/bin/env -S npx tsx + +/** + * Benchmark Report Generator + * + * Runs the SQLite benchmark twice (native and WASM) and generates a + * markdown comparison report. + * + * Usage: + * RIVET_ENDPOINT=http://127.0.0.1:6420 npx tsx scripts/bench-report.ts + * RIVET_ENDPOINT=http://127.0.0.1:6420 npx tsx scripts/bench-report.ts --quick + */ + +import { execSync } from "node:child_process"; +import { readFileSync, writeFileSync, renameSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const QUICK = process.argv.includes("--quick") ? "--quick" : ""; +const endpoint = process.env.RIVET_ENDPOINT; +if (!endpoint) { + console.error("RIVET_ENDPOINT is required"); + process.exit(1); +} + +const NATIVE_NODE = join( + __dirname, + "../../../rivetkit-typescript/packages/sqlite-native/sqlite-native.linux-x64-gnu.node", +); +const NATIVE_BAK = NATIVE_NODE + ".bak"; + +interface BenchEntry { + name: string; + elapsedMs: number; + detail?: string; +} + +function runBench(label: string, outputFile: string): BenchEntry[] { + console.log(`\n${"=".repeat(60)}`); + console.log(`Running ${label} benchmark...`); + console.log("=".repeat(60)); + + try { + execSync( + `BENCH_REPORT=${outputFile} RIVET_ENDPOINT=${endpoint} npx tsx scripts/bench-sqlite.ts ${QUICK}`, + { stdio: "inherit", timeout: 600_000 }, + ); + } catch { + console.error(`${label} benchmark failed`); + } + + const jsonFile = outputFile.replace(/\.md$/, ".json"); + if (!existsSync(jsonFile)) return []; + const data = readFileSync(jsonFile, "utf-8"); + return JSON.parse(data) as BenchEntry[]; +} + +// Run native (default). +const nativeResults = runBench("Native KV Channel", "/tmp/bench-native.md"); + +// Hide native addon to force WASM fallback. +if (existsSync(NATIVE_NODE)) { + renameSync(NATIVE_NODE, NATIVE_BAK); +} + +let wasmResults: BenchEntry[]; +try { + wasmResults = runBench("WASM VFS", "/tmp/bench-wasm.md"); +} finally { + // Restore native addon. + if (existsSync(NATIVE_BAK)) { + renameSync(NATIVE_BAK, NATIVE_NODE); + } +} + +// Build lookup maps. +const nativeMap = new Map(nativeResults.map((e) => [e.name, e])); +const wasmMap = new Map(wasmResults.map((e) => [e.name, e])); + +// Collect all unique names in order (native first, then any WASM-only). +const allNames: string[] = []; +const seen = new Set(); +for (const e of nativeResults) { + if (!seen.has(e.name)) { allNames.push(e.name); seen.add(e.name); } +} +for (const e of wasmResults) { + if (!seen.has(e.name)) { allNames.push(e.name); seen.add(e.name); } +} + +// Generate markdown. +const lines: string[] = []; +const now = new Date().toISOString().slice(0, 19).replace("T", " "); +lines.push(`# SQLite Benchmark Report`); +lines.push(``); +lines.push(`Generated: ${now} UTC`); +lines.push(`Engine: ${endpoint}`); +lines.push(`Mode: ${QUICK ? "quick" : "full"}`); +lines.push(``); + +// Summary table. +lines.push(`## Results`); +lines.push(``); +lines.push(`| Benchmark | Native (ms) | WASM (ms) | Speedup | Native/op | WASM/op |`); +lines.push(`|-----------|------------:|----------:|--------:|----------:|--------:|`); + +for (const name of allNames) { + const n = nativeMap.get(name); + const w = wasmMap.get(name); + + const nMs = n && n.elapsedMs > 0 ? n.elapsedMs : null; + const wMs = w && w.elapsedMs > 0 ? w.elapsedMs : null; + + const nStr = nMs !== null ? nMs.toFixed(1) : n?.detail === "TIMEOUT" ? "TIMEOUT" : "-"; + const wStr = wMs !== null ? wMs.toFixed(1) : w?.detail === "TIMEOUT" ? "TIMEOUT" : "-"; + + let speedup = "-"; + if (nMs !== null && wMs !== null && nMs > 0) { + const ratio = wMs / nMs; + speedup = ratio >= 1.1 ? `**${ratio.toFixed(1)}x**` : ratio <= 0.9 ? `${ratio.toFixed(1)}x` : `~1.0x`; + } + + const nOp = n?.detail && n.detail !== "TIMEOUT" ? n.detail : "-"; + const wOp = w?.detail && w.detail !== "TIMEOUT" ? w.detail : "-"; + + lines.push(`| ${name} | ${nStr} | ${wStr} | ${speedup} | ${nOp} | ${wOp} |`); +} + +// Scale sweep tables (group by prefix). +const scalePrefixes = ["Insert single", "Insert batch", "Insert TX", "Point read", "Mixed OLTP", "Hot row updates"]; +for (const prefix of scalePrefixes) { + const scaleEntries = allNames.filter((name) => name.startsWith(prefix + " x")); + if (scaleEntries.length < 2) continue; + + lines.push(``); + lines.push(`### ${prefix} (scale sweep)`); + lines.push(``); + lines.push(`| N | Native (ms) | Native/op | WASM (ms) | WASM/op | Speedup |`); + lines.push(`|--:|------------:|----------:|----------:|--------:|--------:|`); + + for (const name of scaleEntries) { + const n = nativeMap.get(name); + const w = wasmMap.get(name); + const nMs = n && n.elapsedMs > 0 ? n.elapsedMs : null; + const wMs = w && w.elapsedMs > 0 ? w.elapsedMs : null; + + // Extract N from name like "Insert single x1000" + const nMatch = name.match(/x(\d+)/); + const count = nMatch ? nMatch[1] : "?"; + + const nStr = nMs !== null ? nMs.toFixed(1) : n?.detail === "TIMEOUT" ? "TIMEOUT" : "-"; + const wStr = wMs !== null ? wMs.toFixed(1) : w?.detail === "TIMEOUT" ? "TIMEOUT" : "-"; + const nOp = n?.detail && n.detail !== "TIMEOUT" ? n.detail : "-"; + const wOp = w?.detail && w.detail !== "TIMEOUT" ? w.detail : "-"; + + let speedup = "-"; + if (nMs !== null && wMs !== null && nMs > 0) { + const ratio = wMs / nMs; + speedup = ratio >= 1.1 ? `**${ratio.toFixed(1)}x**` : ratio <= 0.9 ? `${ratio.toFixed(1)}x` : `~1.0x`; + } + + lines.push(`| ${count} | ${nStr} | ${nOp} | ${wStr} | ${wOp} | ${speedup} |`); + } +} + +// Totals. +const nTotal = nativeResults.reduce((s, e) => s + (e.elapsedMs > 0 ? e.elapsedMs : 0), 0); +const wTotal = wasmResults.reduce((s, e) => s + (e.elapsedMs > 0 ? e.elapsedMs : 0), 0); +lines.push(``); +lines.push(`## Totals`); +lines.push(``); +lines.push(`- **Native total**: ${(nTotal / 1000).toFixed(1)}s`); +lines.push(`- **WASM total**: ${(wTotal / 1000).toFixed(1)}s`); +lines.push(`- **Overall speedup**: ${(wTotal / nTotal).toFixed(1)}x`); +lines.push(``); + +const reportPath = process.env.BENCH_REPORT || "/home/nathan/rivet-5/.agent/notes/bench-report.md"; +writeFileSync(reportPath, lines.join("\n")); +console.log(`\nReport written to ${reportPath}`); + +process.exit(0); diff --git a/examples/kitchen-sink/scripts/bench-sqlite.ts b/examples/kitchen-sink/scripts/bench-sqlite.ts new file mode 100644 index 0000000000..0305fd5ad5 --- /dev/null +++ b/examples/kitchen-sink/scripts/bench-sqlite.ts @@ -0,0 +1,719 @@ +#!/usr/bin/env -S npx tsx + +/** + * SQLite Benchmark Runner + * + * Spins up actors against the kitchen-sink registry and runs each benchmark + * scenario, printing a summary table at the end. + * + * Usage: + * # Against local engine (spawn_engine=true default): + * npx tsx scripts/bench-sqlite.ts + * + * # Against a remote endpoint: + * RIVET_ENDPOINT=http://localhost:6420 npx tsx scripts/bench-sqlite.ts + * + * # Quick mode (smaller datasets): + * npx tsx scripts/bench-sqlite.ts --quick + */ + +import { setup } from "rivetkit"; +import { createClient } from "rivetkit/client"; +import { sqliteBench } from "../src/actors/sqlite-bench.ts"; + +const registry = setup({ use: { sqliteBench } }); +type Registry = typeof registry; + +// ── Config ────────────────────────────────────────────────────────── + +const QUICK = process.argv.includes("--quick"); + +const SIZES = QUICK + ? { small: 100, medium: 500, large: 1000, growth: 2000, growthInterval: 500 } + : { small: 500, medium: 2000, large: 10000, growth: 10000, growthInterval: 2000 }; + +// Orders of magnitude for scale sweep benchmarks. +const SCALES = QUICK + ? [1, 10, 100, 1000, 10000] + : [1, 10, 100, 1000, 10000, 100000]; + +// Per-operation timeout for large scale tests (ms). +const SCALE_TIMEOUT_MS = 120_000; + +// Output file for markdown report. +const REPORT_FILE = process.env.BENCH_REPORT; + +// ── Types ─────────────────────────────────────────────────────────── + +interface BenchmarkEntry { + name: string; + elapsedMs: number; + detail?: string; +} + +// ── Helpers ───────────────────────────────────────────────────────── + +function ms(n: number): string { + return `${n.toFixed(1)}ms`; +} + +function perOp(total: number, count: number): string { + return `${(total / count).toFixed(3)}ms/op`; +} + +/** Run a benchmark with a timeout. Returns null if it times out. */ +async function withTimeout(fn: () => Promise, timeoutMs: number): Promise { + return Promise.race([ + fn(), + new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)), + ]); +} + +function printTable(entries: BenchmarkEntry[]): void { + const nameWidth = Math.max(40, ...entries.map((e) => e.name.length)); + const timeWidth = 14; + const detailWidth = 40; + + const sep = "-".repeat(nameWidth + timeWidth + detailWidth + 8); + console.log(sep); + console.log( + `${"Benchmark".padEnd(nameWidth)} ${"Time".padStart(timeWidth)} ${"Detail".padEnd(detailWidth)}`, + ); + console.log(sep); + for (const e of entries) { + console.log( + `${e.name.padEnd(nameWidth)} ${ms(e.elapsedMs).padStart(timeWidth)} ${(e.detail ?? "").padEnd(detailWidth)}`, + ); + } + console.log(sep); +} + +// ── Runner ────────────────────────────────────────────────────────── + +type Client = Awaited>["client"]; + +async function freshActor(client: Client) { + return client.sqliteBench.getOrCreate([`bench-${crypto.randomUUID()}`]); +} + +async function runAll(client: Client): Promise { + const entries: BenchmarkEntry[] = []; + + // 1. Large migrations + console.log(" [1/14] Large migrations..."); + { + const a = await freshActor(client); + const r = await a.benchMigration(QUICK ? 50 : 100); + entries.push({ + name: `Migration (${r.tableCount} tables + indexes)`, + elapsedMs: r.elapsedMs, + detail: perOp(r.elapsedMs, r.tableCount), + }); + } + + // 1b. Large migrations in transaction + console.log(" [1b/14] Large migrations (transaction)..."); + { + const a = await freshActor(client); + const r = await a.benchMigrationTransaction(QUICK ? 50 : 100); + entries.push({ + name: `Migration TX (${r.tableCount} tables + indexes)`, + elapsedMs: r.elapsedMs, + detail: perOp(r.elapsedMs, r.tableCount), + }); + } + + // 2. Single-row inserts (scale sweep) + console.log(" [2] Single-row inserts (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchInsertSingle(n), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Insert single x${n}`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Insert single x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 3. Batch inserts (scale sweep) + console.log(" [3] Batch inserts (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchInsertBatch(n, 50), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Insert batch x${n}`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Insert batch x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 4. Transactional inserts (scale sweep) + console.log(" [4] TX inserts (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchInsertTransaction(n), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Insert TX x${n}`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Insert TX x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 5. Point reads (scale sweep) + console.log(" [5] Point reads (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchPointRead(n), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Point read x${n}`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Point read x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 6. Full table scan + console.log(" [6/14] Full table scan..."); + { + const a = await freshActor(client); + const r = await a.benchFullScan(SIZES.medium); + entries.push({ + name: `Full scan (${r.rowsReturned} rows)`, + elapsedMs: r.elapsedMs, + }); + } + + // 7. Range scan + console.log(" [7/14] Range scan..."); + { + const a = await freshActor(client); + const r = await a.benchRangeScan(SIZES.medium); + entries.push({ + name: `Range scan indexed (${r.indexed.rowsReturned} rows)`, + elapsedMs: r.indexed.elapsedMs, + }); + entries.push({ + name: `Range scan unindexed (${r.unindexed.rowsReturned} rows)`, + elapsedMs: r.unindexed.elapsedMs, + }); + } + + // 8. Large payloads + console.log(" [8/14] Large payloads..."); + { + const a = await freshActor(client); + const r = await a.benchLargePayload(100, 4096); + entries.push({ + name: `Large payload insert (4KB x ${r.rowCount})`, + elapsedMs: r.insertElapsedMs, + detail: perOp(r.insertElapsedMs, r.rowCount), + }); + entries.push({ + name: `Large payload read (4KB x ${r.rowsRead})`, + elapsedMs: r.readElapsedMs, + }); + } + { + const a = await freshActor(client); + const r = await a.benchLargePayload(20, 32768); + entries.push({ + name: `Large payload insert (32KB x ${r.rowCount})`, + elapsedMs: r.insertElapsedMs, + detail: perOp(r.insertElapsedMs, r.rowCount), + }); + entries.push({ + name: `Large payload read (32KB x ${r.rowsRead})`, + elapsedMs: r.readElapsedMs, + }); + } + + // 9. Complex queries + console.log(" [9/14] Complex queries..."); + { + const a = await freshActor(client); + const r = await a.benchComplexQueries(SIZES.medium); + for (const [queryType, result] of Object.entries(r.results)) { + entries.push({ + name: `Complex: ${queryType} (${result.rowCount} rows)`, + elapsedMs: result.elapsedMs, + }); + } + } + + // 10. Bulk update + console.log(" [10/14] Bulk update..."); + { + const a = await freshActor(client); + const r = await a.benchBulkUpdate(SIZES.medium); + entries.push({ + name: `Bulk update (~${Math.floor(r.seedRows / 2)} rows)`, + elapsedMs: r.elapsedMs, + }); + } + + // 11. Bulk delete + VACUUM + console.log(" [11/14] Bulk delete + VACUUM..."); + { + const a = await freshActor(client); + const r = await a.benchDeleteVacuum(SIZES.medium); + entries.push({ + name: `Bulk delete (~${Math.floor(r.seedRows / 2)} rows)`, + elapsedMs: r.deleteElapsedMs, + }); + entries.push({ + name: `VACUUM after delete`, + elapsedMs: r.vacuumElapsedMs, + }); + } + + // 12. Mixed OLTP (scale sweep) + console.log(" [12] Mixed OLTP (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchMixedOltp(n, 0.7), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Mixed OLTP x${n} (${r.reads}R/${r.writes}W)`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Mixed OLTP x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 13. Hot row (scale sweep) + console.log(" [13] Hot row (scale sweep)..."); + for (const n of SCALES) { + process.stdout.write(` x${n}...`); + const a = await freshActor(client); + const r = await withTimeout(() => a.benchHotRow(n), SCALE_TIMEOUT_MS); + if (r) { + console.log(` ${ms(r.elapsedMs)}`); + entries.push({ name: `Hot row updates x${n}`, elapsedMs: r.elapsedMs, detail: perOp(r.elapsedMs, n) }); + } else { + console.log(" TIMEOUT"); + entries.push({ name: `Hot row updates x${n}`, elapsedMs: -1, detail: "TIMEOUT" }); + break; + } + } + + // 14. JSON operations + console.log(" [14/14] JSON operations..."); + { + const a = await freshActor(client); + const r = await a.benchJson(SIZES.small); + entries.push({ + name: `JSON insert x${r.rowCount}`, + elapsedMs: r.insertElapsedMs, + detail: perOp(r.insertElapsedMs, r.rowCount), + }); + entries.push({ + name: `JSON extract query (${r.jsonExtract.rowCount} rows)`, + elapsedMs: r.jsonExtract.elapsedMs, + }); + entries.push({ + name: `JSON each aggregation (${r.jsonEach.rowCount} groups)`, + elapsedMs: r.jsonEach.elapsedMs, + }); + } + + // FTS and growth are optional since FTS5 may not be available in all builds. + console.log(" [bonus] FTS5..."); + try { + const a = await freshActor(client); + const r = await a.benchFts(SIZES.small); + entries.push({ + name: `FTS5 insert x${r.docCount}`, + elapsedMs: r.insertElapsedMs, + detail: perOp(r.insertElapsedMs, r.docCount), + }); + entries.push({ + name: `FTS5 search (${r.search.rowCount} hits)`, + elapsedMs: r.search.elapsedMs, + }); + entries.push({ + name: `FTS5 prefix search (${r.prefixSearch.rowCount} hits)`, + elapsedMs: r.prefixSearch.elapsedMs, + }); + } catch (err) { + console.log(` Skipped FTS5: ${err}`); + } + + console.log(" [bonus] Growth test..."); + { + const a = await freshActor(client); + const r = await a.benchGrowth(SIZES.growth, SIZES.growthInterval); + for (const m of r.measurements) { + entries.push({ + name: `Growth @${m.rowCount} rows: insert batch`, + elapsedMs: m.insertBatchMs, + detail: perOp(m.insertBatchMs, SIZES.growthInterval), + }); + entries.push({ + name: `Growth @${m.rowCount} rows: 100 point reads`, + elapsedMs: m.pointReadMs, + detail: perOp(m.pointReadMs, 100), + }); + } + } + + return entries; +} + +// ── Concurrent actor benchmark ────────────────────────────────────── + +const CONCURRENCY_SCALES = [1, 5, 10, 50, 100]; +const ROWS_PER_CONCURRENT_ACTOR = 100; + +async function runConcurrent( + client: Client, +): Promise { + const entries: BenchmarkEntry[] = []; + + for (const actorCount of CONCURRENCY_SCALES) { + process.stdout.write(` Concurrent: ${actorCount} actors x ${ROWS_PER_CONCURRENT_ACTOR} rows...`); + const start = performance.now(); + const promises = Array.from({ length: actorCount }, async () => { + const a = await freshActor(client); + return a.benchInsertTransaction(ROWS_PER_CONCURRENT_ACTOR); + }); + const results = await withTimeout( + () => Promise.all(promises), + SCALE_TIMEOUT_MS, + ); + if (!results) { + console.log(" TIMEOUT"); + entries.push({ + name: `Concurrent x${actorCount} wall`, + elapsedMs: -1, + detail: "TIMEOUT", + }); + break; + } + const totalMs = performance.now() - start; + const avgMs = + results.reduce((sum, r) => sum + r.elapsedMs, 0) / results.length; + const totalRows = actorCount * ROWS_PER_CONCURRENT_ACTOR; + console.log(` ${totalMs.toFixed(0)}ms wall, ${avgMs.toFixed(1)}ms avg/actor`); + entries.push({ + name: `Concurrent x${actorCount} wall`, + elapsedMs: totalMs, + detail: `${totalRows} total rows`, + }); + entries.push({ + name: `Concurrent x${actorCount} avg/actor`, + elapsedMs: avgMs, + detail: perOp(avgMs, ROWS_PER_CONCURRENT_ACTOR), + }); + entries.push({ + name: `Concurrent x${actorCount} throughput`, + elapsedMs: totalRows / (totalMs / 1000), + detail: `rows/sec`, + }); + } + + return entries; +} + +// ── Native SQLite baseline (no VFS, no KV channel, raw disk) ─────── + +async function runNativeBaseline(): Promise { + const { DatabaseSync } = await import("node:sqlite"); + const { mkdtempSync, rmSync } = await import("node:fs"); + const { join } = await import("node:path"); + const { tmpdir } = await import("node:os"); + + const dir = mkdtempSync(join(tmpdir(), "sqlite-bench-")); + const dbPath = join(dir, "bench.db"); + const db = new DatabaseSync(dbPath); + db.exec("PRAGMA journal_mode=WAL"); + db.exec("PRAGMA synchronous=NORMAL"); + + // Create the base table (matches actor onMigrate) + db.exec(` + CREATE TABLE IF NOT EXISTS bench ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT, value TEXT, num REAL, created_at INTEGER NOT NULL + ) + `); + db.exec("CREATE INDEX IF NOT EXISTS idx_bench_key ON bench(key)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_bench_num ON bench(num)"); + + const entries: BenchmarkEntry[] = []; + const tableCount = QUICK ? 50 : 100; + + // Migration (no transaction) + { + const start = performance.now(); + for (let i = 0; i < tableCount; i++) { + db.exec(`CREATE TABLE IF NOT EXISTS baseline_t${i} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + a TEXT, b TEXT, c REAL, d INTEGER, created_at INTEGER NOT NULL + )`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_baseline_t${i}_a ON baseline_t${i}(a)`); + } + entries.push({ + name: `[baseline] Migration (${tableCount} tables)`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, tableCount), + }); + } + + // Migration in transaction + { + const start = performance.now(); + db.exec("BEGIN"); + for (let i = 0; i < tableCount; i++) { + db.exec(`CREATE TABLE IF NOT EXISTS baseline_tx_t${i} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + a TEXT, b TEXT, c REAL, d INTEGER, created_at INTEGER NOT NULL + )`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_baseline_tx_t${i}_a ON baseline_tx_t${i}(a)`); + } + db.exec("COMMIT"); + entries.push({ + name: `[baseline] Migration TX (${tableCount} tables)`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, tableCount), + }); + } + + // Single-row inserts + { + const count = SIZES.small; + const stmt = db.prepare("INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)"); + const start = performance.now(); + for (let i = 0; i < count; i++) { + stmt.run(`key-${i}`, `value-${i}`, Math.random(), Date.now()); + } + entries.push({ + name: `[baseline] Insert single-row x${count}`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, count), + }); + } + + // Insert in transaction + { + const count = SIZES.small; + const stmt = db.prepare("INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)"); + const start = performance.now(); + db.exec("BEGIN"); + for (let i = 0; i < count; i++) { + stmt.run(`tx-key-${i}`, `tx-value-${i}`, Math.random(), Date.now()); + } + db.exec("COMMIT"); + entries.push({ + name: `[baseline] Insert TX x${count}`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, count), + }); + } + + // Point reads + { + const count = SIZES.small; + const stmt = db.prepare("SELECT * FROM bench WHERE key = ?"); + const start = performance.now(); + for (let i = 0; i < count; i++) { + stmt.get(`key-${i}`); + } + entries.push({ + name: `[baseline] Point read x${count}`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, count), + }); + } + + // Full scan + { + const start = performance.now(); + const rows = db.prepare("SELECT * FROM bench").all(); + entries.push({ + name: `[baseline] Full scan (${rows.length} rows)`, + elapsedMs: performance.now() - start, + }); + } + + // Hot row updates + { + db.exec("INSERT INTO bench (key, value, num, created_at) VALUES ('hot', 'row', 0, 0)"); + const count = SIZES.small; + const stmt = db.prepare("UPDATE bench SET num = ? WHERE key = 'hot'"); + const start = performance.now(); + for (let i = 0; i < count; i++) { + stmt.run(i); + } + entries.push({ + name: `[baseline] Hot row updates x${count}`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, count), + }); + } + + // Mixed OLTP + { + const count = SIZES.small; + const readStmt = db.prepare("SELECT * FROM bench WHERE key = ?"); + const writeStmt = db.prepare("INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)"); + let reads = 0, writes = 0; + const start = performance.now(); + for (let i = 0; i < count; i++) { + if (Math.random() < 0.7) { + readStmt.get(`key-${Math.floor(Math.random() * count)}`); + reads++; + } else { + writeStmt.run(`oltp-${i}`, `val-${i}`, Math.random(), Date.now()); + writes++; + } + } + entries.push({ + name: `[baseline] Mixed OLTP x${count} (${reads}R/${writes}W)`, + elapsedMs: performance.now() - start, + detail: perOp(performance.now() - start, count), + }); + } + + db.close(); + rmSync(dir, { recursive: true }); + return entries; +} + +// ── Main ──────────────────────────────────────────────────────────── + +async function main(): Promise { + console.log(`SQLite Benchmark (${QUICK ? "quick" : "full"} mode)\n`); + + const endpoint = process.env.RIVET_ENDPOINT; + + let client: ReturnType>; + if (endpoint) { + console.log(`Connecting to endpoint: ${endpoint}\n`); + registry.start(); + client = createClient({ endpoint }); + } else { + console.log("Starting with local file-system driver\n"); + registry.start(); + client = createClient({ endpoint: "http://localhost:6420" }); + } + + // Give runner time to connect to the engine + if (endpoint) { + console.log("Waiting for runner to connect..."); + // Poll until the engine has a runner available + for (let i = 0; i < 30; i++) { + try { + const res = await fetch(`${endpoint}/actors?namespace=default`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "sqliteBench", key: ["__health_check__"] }), + }); + if (res.ok) { + console.log("Runner connected!\n"); + break; + } + const body = await res.json().catch(() => ({})); + if ((body as any)?.error?.code !== "no_runners_available") { + console.log("Runner connected!\n"); + break; + } + } catch {} + await new Promise(r => setTimeout(r, 1000)); + process.stdout.write("."); + } + console.log(""); + } + + console.log("Running benchmarks...\n"); + const entries = await runAll(client); + + // Concurrent actor test. + console.log("\nRunning concurrent actor scale sweep..."); + const concurrentEntries = await runConcurrent(client); + entries.push(...concurrentEntries); + + // Native SQLite baseline (raw disk, no VFS/KV). + console.log("\nRunning native SQLite baseline..."); + const baselineEntries = await runNativeBaseline(); + entries.push(...baselineEntries); + + console.log("\n"); + printTable(entries); + + // Summary stats. + const totalMs = entries.reduce((sum, e) => sum + (e.elapsedMs > 0 ? e.elapsedMs : 0), 0); + console.log(`\nTotal benchmark time: ${ms(totalMs)}`); + console.log(`Scenarios run: ${entries.length}`); + + // Write JSON results for report generation. + const jsonFile = REPORT_FILE ? REPORT_FILE.replace(/\.md$/, ".json") : `/tmp/bench-results-${Date.now()}.json`; + const { writeFileSync } = await import("node:fs"); + writeFileSync(jsonFile, JSON.stringify(entries, null, 2)); + console.log(`\nResults written to ${jsonFile}`); + + // Print KV channel metrics if native SQLite is available. + try { + // Access the internal native-sqlite module to get KV channel metrics. + // Uses createRequire for CJS compat with the napi addon. + const { createRequire } = await import("node:module"); + const require = createRequire(import.meta.url); + const native = require("@rivetkit/sqlite-native"); + // The kvChannel handle is stored as a module-level singleton in native-sqlite.ts. + // We can't access it directly, but we exported getKvChannelMetrics. + // For the bench, we'll try the direct path. + const nativeSqlite = await import( + // @ts-ignore + "../../../rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts" + ); + const m = nativeSqlite.getKvChannelMetrics?.(); + if (m) { + console.log("\n--- KV Channel Metrics ---"); + const ops: [string, any][] = [ + ["get", m.get], + ["put", m.put], + ["delete", m.delete], + ["deleteRange", m.deleteRange], + ["actorOpen", m.actorOpen], + ["actorClose", m.actorClose], + ]; + const nameWidth = 14; + const colWidth = 12; + console.log( + `${"Op".padEnd(nameWidth)} ${"Count".padStart(colWidth)} ${"Avg (us)".padStart(colWidth)} ${"Min (us)".padStart(colWidth)} ${"Max (us)".padStart(colWidth)} ${"Total (ms)".padStart(colWidth)}`, + ); + console.log("-".repeat(nameWidth + colWidth * 5 + 10)); + for (const [name, s] of ops) { + if (s && s.count > 0) { + console.log( + `${name.padEnd(nameWidth)} ${String(s.count).padStart(colWidth)} ${s.avgDurationUs.toFixed(0).padStart(colWidth)} ${String(s.minDurationUs).padStart(colWidth)} ${String(s.maxDurationUs).padStart(colWidth)} ${(s.totalDurationUs / 1000).toFixed(1).padStart(colWidth)}`, + ); + } + } + } + } catch { + // Native module or metrics not available, skip. + } + + process.exit(0); +} + +main().catch((err) => { + console.error("Benchmark failed:", err); + process.exit(1); +}); diff --git a/examples/kitchen-sink/scripts/diag-cold-reads.ts b/examples/kitchen-sink/scripts/diag-cold-reads.ts new file mode 100644 index 0000000000..bfdfe493e6 --- /dev/null +++ b/examples/kitchen-sink/scripts/diag-cold-reads.ts @@ -0,0 +1,65 @@ +#!/usr/bin/env -S npx tsx + +import { setup } from "rivetkit"; +import { createClient } from "rivetkit/client"; +import { sqliteBench } from "../src/actors/sqlite-bench.ts"; + +const registry = setup({ use: { sqliteBench } }); +type R = typeof registry; + +async function main() { + const endpoint = process.env.RIVET_ENDPOINT || "http://127.0.0.1:6420"; + registry.start(); + const client = createClient({ endpoint }); + await new Promise(r => setTimeout(r, 5000)); + + function fresh() { + return client.sqliteBench.getOrCreate(["diag-" + Math.random().toString(36).slice(2)]); + } + + // Cold reads: fresh actor every time (like the bench does) + console.log("=== Cold actor point reads ==="); + for (const n of [1, 10, 100]) { + const a = fresh(); + const r = await a.benchPointRead(n); + console.log(`Fresh actor point read x${n}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / n).toFixed(3)}ms/op)`); + } + + // Warm reads: reuse same actor + console.log("\n=== Warm actor point reads ==="); + const a = fresh(); + await a.benchInsertTransaction(1000); + for (const n of [1, 10, 100, 1000]) { + const r = await a.benchPointRead(n); + console.log(`Warm actor point read x${n}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / n).toFixed(3)}ms/op)`); + } + + // Cold TX: fresh actor + console.log("\n=== Cold actor TX inserts ==="); + for (const n of [1, 10, 100]) { + const a = fresh(); + const r = await a.benchInsertTransaction(n); + console.log(`Fresh actor TX x${n}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / n).toFixed(3)}ms/op)`); + } + + // Warm TX: reuse same actor + console.log("\n=== Warm actor TX inserts ==="); + const a2 = fresh(); + await a2.benchInsertTransaction(10); // warmup + for (const n of [1, 10, 100]) { + const r = await a2.benchInsertTransaction(n); + console.log(`Warm actor TX x${n}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / n).toFixed(3)}ms/op)`); + } + + // Cold batch x1: the 38ms anomaly + console.log("\n=== Cold actor batch x1 ==="); + for (let i = 0; i < 5; i++) { + const a = fresh(); + const r = await a.benchInsertBatch(1, 50); + console.log(`Fresh actor batch x1 attempt ${i}: ${r.elapsedMs.toFixed(1)}ms`); + } + + process.exit(0); +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/examples/kitchen-sink/scripts/diag-perf.ts b/examples/kitchen-sink/scripts/diag-perf.ts new file mode 100644 index 0000000000..b183accf2b --- /dev/null +++ b/examples/kitchen-sink/scripts/diag-perf.ts @@ -0,0 +1,83 @@ +#!/usr/bin/env -S npx tsx + +import { setup } from "rivetkit"; +import { createClient } from "rivetkit/client"; +import { sqliteBench } from "../src/actors/sqlite-bench.ts"; + +const registry = setup({ use: { sqliteBench } }); +type R = typeof registry; + +async function main() { + const endpoint = process.env.RIVET_ENDPOINT || "http://127.0.0.1:6420"; + registry.start(); + const client = createClient({ endpoint }); + await new Promise(r => setTimeout(r, 5000)); + + function fresh() { + return client.sqliteBench.getOrCreate(["diag-" + Math.random().toString(36).slice(2)]); + } + + // TX x1 cold vs warm + console.log("=== TX x1 cold vs warm ==="); + const a1 = fresh(); + const r1 = await a1.benchInsertTransaction(1); + console.log(`TX x1 cold: ${r1.elapsedMs.toFixed(1)}ms`); + const r2 = await a1.benchInsertTransaction(1); + console.log(`TX x1 warm: ${r2.elapsedMs.toFixed(1)}ms`); + const r3 = await a1.benchInsertTransaction(1); + console.log(`TX x1 warm2: ${r3.elapsedMs.toFixed(1)}ms`); + + // TX x10 cold vs warm + console.log("\n=== TX x10 cold vs warm ==="); + const a1b = fresh(); + const r1b = await a1b.benchInsertTransaction(10); + console.log(`TX x10 cold: ${r1b.elapsedMs.toFixed(1)}ms`); + const r2b = await a1b.benchInsertTransaction(10); + console.log(`TX x10 warm: ${r2b.elapsedMs.toFixed(1)}ms`); + + // Point reads: seed, then multiple rounds + console.log("\n=== Point reads ==="); + const a2 = fresh(); + await a2.benchInsertTransaction(1000); + console.log("Seeded 1000 rows"); + for (let i = 0; i < 5; i++) { + const r = await a2.benchPointRead(100); + console.log(`Point read x100 round ${i}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / 100).toFixed(3)}ms/op)`); + } + for (let i = 0; i < 3; i++) { + const r = await a2.benchPointRead(1000); + console.log(`Point read x1000 round ${i}: ${r.elapsedMs.toFixed(1)}ms (${(r.elapsedMs / 1000).toFixed(3)}ms/op)`); + } + + // Large payload: insert then read multiple times + console.log("\n=== Large payload 4KB ==="); + const a3 = fresh(); + const rL1 = await a3.benchLargePayload(100, 4096); + console.log(`Insert 4KB x100: ${rL1.insertElapsedMs.toFixed(1)}ms`); + console.log(`Read 4KB x100: ${rL1.readElapsedMs.toFixed(1)}ms`); + // Read again by querying directly + const rL2 = await a3.benchPointRead(100); + console.log(`Point read after large payload: ${rL2.elapsedMs.toFixed(1)}ms`); + + // Batch x1 cold vs warm + console.log("\n=== Batch x1 cold vs warm ==="); + const a4 = fresh(); + const rB1 = await a4.benchInsertBatch(1, 50); + console.log(`Batch x1 cold: ${rB1.elapsedMs.toFixed(1)}ms`); + const rB2 = await a4.benchInsertBatch(1, 50); + console.log(`Batch x1 warm: ${rB2.elapsedMs.toFixed(1)}ms`); + const rB3 = await a4.benchInsertBatch(1, 50); + console.log(`Batch x1 warm2: ${rB3.elapsedMs.toFixed(1)}ms`); + + // JSON insert cold vs warm + console.log("\n=== JSON insert ==="); + const a5 = fresh(); + const rJ1 = await a5.benchJson(10); + console.log(`JSON x10 cold: insert=${rJ1.insertElapsedMs.toFixed(1)}ms extract=${rJ1.jsonExtract.elapsedMs.toFixed(1)}ms each=${rJ1.jsonEach.elapsedMs.toFixed(1)}ms`); + const rJ2 = await a5.benchJson(10); + console.log(`JSON x10 warm: insert=${rJ2.insertElapsedMs.toFixed(1)}ms extract=${rJ2.jsonExtract.elapsedMs.toFixed(1)}ms each=${rJ2.jsonEach.elapsedMs.toFixed(1)}ms`); + + process.exit(0); +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/examples/kitchen-sink/src/actors/sqlite-bench.ts b/examples/kitchen-sink/src/actors/sqlite-bench.ts new file mode 100644 index 0000000000..9787bf8c12 --- /dev/null +++ b/examples/kitchen-sink/src/actors/sqlite-bench.ts @@ -0,0 +1,742 @@ +import { actor } from "rivetkit"; +import { db } from "rivetkit/db"; + +// Generates a string of the given byte size. +function payload(bytes: number): string { + return "x".repeat(bytes); +} + +export const sqliteBench = actor({ + options: { + actionTimeout: 300_000, // 5 minutes for large-scale benchmarks + }, + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS bench ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT, + value TEXT, + num REAL, + created_at INTEGER NOT NULL + ) + `); + await db.execute( + `CREATE INDEX IF NOT EXISTS idx_bench_key ON bench(key)`, + ); + await db.execute( + `CREATE INDEX IF NOT EXISTS idx_bench_num ON bench(num)`, + ); + }, + }), + actions: { + // ── Migrations ────────────────────────────────────────────── + + // Create N tables each with an index to stress migration overhead. + benchMigration: async (c, tableCount: number) => { + const start = performance.now(); + for (let i = 0; i < tableCount; i++) { + await c.db.execute(` + CREATE TABLE IF NOT EXISTS migration_t${i} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + a TEXT, b TEXT, c REAL, d INTEGER, + created_at INTEGER NOT NULL + ) + `); + await c.db.execute( + `CREATE INDEX IF NOT EXISTS idx_migration_t${i}_a ON migration_t${i}(a)`, + ); + } + return { tableCount, elapsedMs: performance.now() - start }; + }, + + // Same as benchMigration but wrapped in a single transaction. + benchMigrationTransaction: async (c, tableCount: number) => { + const start = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < tableCount; i++) { + await c.db.execute(` + CREATE TABLE IF NOT EXISTS migration_tx_t${i} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + a TEXT, b TEXT, c REAL, d INTEGER, + created_at INTEGER NOT NULL + ) + `); + await c.db.execute( + `CREATE INDEX IF NOT EXISTS idx_migration_tx_t${i}_a ON migration_tx_t${i}(a)`, + ); + } + await c.db.execute("COMMIT"); + return { tableCount, elapsedMs: performance.now() - start }; + }, + + // ── Single-row inserts ────────────────────────────────────── + + benchInsertSingle: async (c, rowCount: number) => { + const start = performance.now(); + for (let i = 0; i < rowCount; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + Math.random() * 1000, + Date.now(), + ); + } + return { + rowCount, + elapsedMs: performance.now() - start, + }; + }, + + // ── Batch inserts (multi-row VALUES) ──────────────────────── + + benchInsertBatch: async (c, rowCount: number, batchSize: number = 50) => { + const start = performance.now(); + for (let offset = 0; offset < rowCount; offset += batchSize) { + const count = Math.min(batchSize, rowCount - offset); + const placeholders = Array.from( + { length: count }, + () => "(?, ?, ?, ?)", + ).join(", "); + const params: (string | number)[] = []; + for (let i = 0; i < count; i++) { + const idx = offset + i; + params.push( + `key-${idx}`, + `value-${idx}`, + Math.random() * 1000, + Date.now(), + ); + } + await c.db.execute( + `INSERT INTO bench (key, value, num, created_at) VALUES ${placeholders}`, + ...params, + ); + } + return { + rowCount, + batchSize, + elapsedMs: performance.now() - start, + }; + }, + + // ── Transactional inserts ─────────────────────────────────── + + benchInsertTransaction: async (c, rowCount: number) => { + const start = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < rowCount; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + Math.random() * 1000, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + return { + rowCount, + elapsedMs: performance.now() - start, + }; + }, + + // ── Point reads ───────────────────────────────────────────── + + benchPointRead: async (c, queryCount: number) => { + // Seed data if table is empty. + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt === 0) { + await c.db.execute("BEGIN"); + for (let i = 0; i < 1000; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + } + + const start = performance.now(); + for (let i = 0; i < queryCount; i++) { + const id = (i % 1000) + 1; + await c.db.execute("SELECT * FROM bench WHERE id = ?", id); + } + return { + queryCount, + elapsedMs: performance.now() - start, + }; + }, + + // ── Full table scan ───────────────────────────────────────── + + benchFullScan: async (c, seedRows: number) => { + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt < seedRows) { + await c.db.execute("BEGIN"); + for (let i = cnt; i < seedRows; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + } + + const start = performance.now(); + const rows = await c.db.execute("SELECT * FROM bench"); + const elapsed = performance.now() - start; + return { + rowsReturned: (rows as unknown[]).length, + elapsedMs: elapsed, + }; + }, + + // ── Range scan (indexed vs non-indexed) ───────────────────── + + benchRangeScan: async (c, seedRows: number) => { + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt < seedRows) { + await c.db.execute("BEGIN"); + for (let i = cnt; i < seedRows; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + } + + // Indexed range scan on num. + const startIndexed = performance.now(); + const indexedRows = await c.db.execute( + "SELECT * FROM bench WHERE num BETWEEN ? AND ?", + 0, + seedRows / 10, + ); + const indexedMs = performance.now() - startIndexed; + + // Non-indexed scan on value (LIKE prefix). + const startUnindexed = performance.now(); + const unindexedRows = await c.db.execute( + "SELECT * FROM bench WHERE value LIKE ?", + "value-1%", + ); + const unindexedMs = performance.now() - startUnindexed; + + return { + seedRows, + indexed: { + rowsReturned: (indexedRows as unknown[]).length, + elapsedMs: indexedMs, + }, + unindexed: { + rowsReturned: (unindexedRows as unknown[]).length, + elapsedMs: unindexedMs, + }, + }; + }, + + // ── Large payloads (chunk boundary stress) ────────────────── + + benchLargePayload: async ( + c, + rowCount: number, + payloadBytes: number, + ) => { + const data = payload(payloadBytes); + const start = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < rowCount; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `large-${i}`, + data, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + const insertMs = performance.now() - start; + + // Read them back. + const readStart = performance.now(); + const rows = await c.db.execute( + "SELECT * FROM bench WHERE key LIKE 'large-%'", + ); + const readMs = performance.now() - readStart; + + return { + rowCount, + payloadBytes, + insertElapsedMs: insertMs, + readElapsedMs: readMs, + rowsRead: (rows as unknown[]).length, + }; + }, + + // ── Complex queries (JOINs, aggregations, CTEs, window fns) ─ + + benchComplexQueries: async (c, seedRows: number) => { + // Ensure we have two tables to join. + await c.db.execute(` + CREATE TABLE IF NOT EXISTS bench_tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bench_id INTEGER NOT NULL, + tag TEXT NOT NULL + ) + `); + await c.db.execute( + `CREATE INDEX IF NOT EXISTS idx_bench_tags_bid ON bench_tags(bench_id)`, + ); + + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt < seedRows) { + await c.db.execute("BEGIN"); + for (let i = cnt; i < seedRows; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + // Add 2 tags per row. + await c.db.execute( + "INSERT INTO bench_tags (bench_id, tag) VALUES (?, ?), (?, ?)", + i + 1, + `tag-${i % 10}`, + i + 1, + `tag-${(i + 5) % 10}`, + ); + } + await c.db.execute("COMMIT"); + } + + const results: Record = + {}; + + // JOIN + { + const s = performance.now(); + const rows = await c.db.execute(` + SELECT b.id, b.key, t.tag + FROM bench b + JOIN bench_tags t ON t.bench_id = b.id + WHERE b.num < 100 + `); + results.join = { + elapsedMs: performance.now() - s, + rowCount: (rows as unknown[]).length, + }; + } + + // Aggregation + { + const s = performance.now(); + const rows = await c.db.execute(` + SELECT t.tag, COUNT(*) as cnt, AVG(b.num) as avg_num + FROM bench b + JOIN bench_tags t ON t.bench_id = b.id + GROUP BY t.tag + HAVING cnt > 1 + ORDER BY cnt DESC + `); + results.aggregation = { + elapsedMs: performance.now() - s, + rowCount: (rows as unknown[]).length, + }; + } + + // CTE + { + const s = performance.now(); + const rows = await c.db.execute(` + WITH ranked AS ( + SELECT id, key, num, + ROW_NUMBER() OVER (ORDER BY num DESC) as rank + FROM bench + ) + SELECT * FROM ranked WHERE rank <= 50 + `); + results.cte_window = { + elapsedMs: performance.now() - s, + rowCount: (rows as unknown[]).length, + }; + } + + // Subquery + { + const s = performance.now(); + const rows = await c.db.execute(` + SELECT * FROM bench + WHERE id IN ( + SELECT bench_id FROM bench_tags WHERE tag = 'tag-0' + ) + `); + results.subquery = { + elapsedMs: performance.now() - s, + rowCount: (rows as unknown[]).length, + }; + } + + return { seedRows, results }; + }, + + // ── Bulk update ───────────────────────────────────────────── + + benchBulkUpdate: async (c, seedRows: number) => { + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt < seedRows) { + await c.db.execute("BEGIN"); + for (let i = cnt; i < seedRows; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + } + + const start = performance.now(); + await c.db.execute( + "UPDATE bench SET value = 'updated', num = num + 1 WHERE num < ?", + seedRows / 2, + ); + return { + seedRows, + elapsedMs: performance.now() - start, + }; + }, + + // ── Bulk delete + VACUUM ──────────────────────────────────── + + benchDeleteVacuum: async (c, seedRows: number) => { + const [{ cnt }] = (await c.db.execute( + "SELECT COUNT(*) as cnt FROM bench", + )) as { cnt: number }[]; + if (cnt < seedRows) { + await c.db.execute("BEGIN"); + for (let i = cnt; i < seedRows; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `key-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + } + + // Delete half the rows. + const deleteStart = performance.now(); + await c.db.execute("DELETE FROM bench WHERE num < ?", seedRows / 2); + const deleteMs = performance.now() - deleteStart; + + // VACUUM. + const vacuumStart = performance.now(); + await c.db.execute("VACUUM"); + const vacuumMs = performance.now() - vacuumStart; + + const [{ remaining }] = (await c.db.execute( + "SELECT COUNT(*) as remaining FROM bench", + )) as { remaining: number }[]; + + return { + seedRows, + deleteElapsedMs: deleteMs, + vacuumElapsedMs: vacuumMs, + remainingRows: remaining, + }; + }, + + // ── Mixed OLTP (interleaved reads + writes) ───────────────── + + benchMixedOltp: async ( + c, + operationCount: number, + readRatio: number = 0.7, + ) => { + // Seed some data. + await c.db.execute("BEGIN"); + for (let i = 0; i < 500; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `oltp-${i}`, + `value-${i}`, + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + + let reads = 0; + let writes = 0; + const start = performance.now(); + + for (let i = 0; i < operationCount; i++) { + if (Math.random() < readRatio) { + const id = Math.floor(Math.random() * 500) + 1; + await c.db.execute("SELECT * FROM bench WHERE id = ?", id); + reads++; + } else { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `oltp-new-${i}`, + `value-${i}`, + i, + Date.now(), + ); + writes++; + } + } + return { + operationCount, + reads, + writes, + elapsedMs: performance.now() - start, + }; + }, + + // ── Hot row (write amplification) ─────────────────────────── + + benchHotRow: async (c, updateCount: number) => { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + "hot-row", + "initial", + 0, + Date.now(), + ); + const [{ id: hotId }] = (await c.db.execute( + "SELECT id FROM bench WHERE key = 'hot-row' LIMIT 1", + )) as { id: number }[]; + + const start = performance.now(); + for (let i = 0; i < updateCount; i++) { + await c.db.execute( + "UPDATE bench SET value = ?, num = ? WHERE id = ?", + `updated-${i}`, + i, + hotId, + ); + } + return { + updateCount, + elapsedMs: performance.now() - start, + }; + }, + + // ── JSON operations ───────────────────────────────────────── + + benchJson: async (c, rowCount: number) => { + await c.db.execute(` + CREATE TABLE IF NOT EXISTS bench_json ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + data TEXT NOT NULL + ) + `); + + // Insert JSON rows. + const insertStart = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < rowCount; i++) { + const json = JSON.stringify({ + name: `user-${i}`, + age: 20 + (i % 50), + tags: [`tag-${i % 5}`, `tag-${(i + 1) % 5}`], + address: { + city: `city-${i % 20}`, + zip: `${10000 + i}`, + }, + }); + await c.db.execute( + "INSERT INTO bench_json (data) VALUES (?)", + json, + ); + } + await c.db.execute("COMMIT"); + const insertMs = performance.now() - insertStart; + + // json_extract query. + const extractStart = performance.now(); + const extractRows = await c.db.execute(` + SELECT id, json_extract(data, '$.name') as name, + json_extract(data, '$.age') as age, + json_extract(data, '$.address.city') as city + FROM bench_json + WHERE json_extract(data, '$.age') > 40 + `); + const extractMs = performance.now() - extractStart; + + // json_each aggregation. + const eachStart = performance.now(); + const eachRows = await c.db.execute(` + SELECT value as tag, COUNT(*) as cnt + FROM bench_json, json_each(json_extract(data, '$.tags')) + GROUP BY value + ORDER BY cnt DESC + `); + const eachMs = performance.now() - eachStart; + + return { + rowCount, + insertElapsedMs: insertMs, + jsonExtract: { + elapsedMs: extractMs, + rowCount: (extractRows as unknown[]).length, + }, + jsonEach: { + elapsedMs: eachMs, + rowCount: (eachRows as unknown[]).length, + }, + }; + }, + + // ── FTS5 full-text search ─────────────────────────────────── + + benchFts: async (c, docCount: number) => { + await c.db.execute(` + CREATE VIRTUAL TABLE IF NOT EXISTS bench_fts + USING fts5(title, body) + `); + + const words = [ + "alpha", "bravo", "charlie", "delta", "echo", + "foxtrot", "golf", "hotel", "india", "juliet", + "kilo", "lima", "mike", "november", "oscar", + ]; + function randomSentence(len: number): string { + return Array.from( + { length: len }, + () => words[Math.floor(Math.random() * words.length)], + ).join(" "); + } + + // Insert documents. + const insertStart = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < docCount; i++) { + await c.db.execute( + "INSERT INTO bench_fts (title, body) VALUES (?, ?)", + randomSentence(5), + randomSentence(50), + ); + } + await c.db.execute("COMMIT"); + const insertMs = performance.now() - insertStart; + + // Search. + const searchStart = performance.now(); + const searchRows = await c.db.execute(` + SELECT * FROM bench_fts WHERE bench_fts MATCH 'alpha AND bravo' + ORDER BY rank + LIMIT 50 + `); + const searchMs = performance.now() - searchStart; + + // Prefix search. + const prefixStart = performance.now(); + const prefixRows = await c.db.execute(` + SELECT * FROM bench_fts WHERE bench_fts MATCH 'cha*' + ORDER BY rank + LIMIT 50 + `); + const prefixMs = performance.now() - prefixStart; + + return { + docCount, + insertElapsedMs: insertMs, + search: { + elapsedMs: searchMs, + rowCount: (searchRows as unknown[]).length, + }, + prefixSearch: { + elapsedMs: prefixMs, + rowCount: (prefixRows as unknown[]).length, + }, + }; + }, + + // ── Database growth (throughput at different sizes) ────────── + + benchGrowth: async (c, targetRows: number, measureInterval: number) => { + const measurements: { + rowCount: number; + insertBatchMs: number; + pointReadMs: number; + }[] = []; + + let totalInserted = 0; + while (totalInserted < targetRows) { + const batchCount = Math.min(measureInterval, targetRows - totalInserted); + + // Measure insert batch. + const insertStart = performance.now(); + await c.db.execute("BEGIN"); + for (let i = 0; i < batchCount; i++) { + await c.db.execute( + "INSERT INTO bench (key, value, num, created_at) VALUES (?, ?, ?, ?)", + `grow-${totalInserted + i}`, + `value-${totalInserted + i}`, + totalInserted + i, + Date.now(), + ); + } + await c.db.execute("COMMIT"); + const insertMs = performance.now() - insertStart; + totalInserted += batchCount; + + // Measure point read at current size. + const readStart = performance.now(); + for (let i = 0; i < 100; i++) { + const id = Math.floor(Math.random() * totalInserted) + 1; + await c.db.execute("SELECT * FROM bench WHERE id = ?", id); + } + const readMs = performance.now() - readStart; + + measurements.push({ + rowCount: totalInserted, + insertBatchMs: insertMs, + pointReadMs: readMs, + }); + } + + return { targetRows, measureInterval, measurements }; + }, + + // ── Utility: reset tables ─────────────────────────────────── + + reset: async (c) => { + await c.db.execute("DELETE FROM bench"); + await c.db.execute( + "DELETE FROM sqlite_sequence WHERE name='bench'", + ); + return { ok: true }; + }, + }, +}); diff --git a/examples/kitchen-sink/src/index.ts b/examples/kitchen-sink/src/index.ts index e2e9de5ac5..fb22f56b62 100644 --- a/examples/kitchen-sink/src/index.ts +++ b/examples/kitchen-sink/src/index.ts @@ -1,9 +1,11 @@ import { setup } from "rivetkit"; import { demo } from "./actors/demo.ts"; +import { sqliteBench } from "./actors/sqlite-bench.ts"; export const registry = setup({ use: { demo, + sqliteBench, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad86e48ee6..f1500907db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,22 @@ importers: specifier: ^5.9.2 version: 5.9.3 + engine/sdks/typescript/kv-channel-protocol: + dependencies: + '@rivetkit/bare-ts': + specifier: ^0.6.2 + version: 0.6.2 + devDependencies: + '@types/node': + specifier: ^20.19.13 + version: 20.19.13 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@20.19.13))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + engine/sdks/typescript/runner: dependencies: '@rivetkit/engine-runner-protocol': @@ -255,7 +271,7 @@ importers: dependencies: '@hono/node-server': specifier: ^1.19.1 - version: 1.19.9(hono@4.11.9) + version: 1.19.12(hono@4.11.9) '@rivetkit/engine-envoy-client': specifier: workspace:* version: link:../envoy-client @@ -4147,6 +4163,9 @@ importers: '@rivetkit/engine-envoy-client': specifier: workspace:* version: link:../../../engine/sdks/typescript/envoy-client + '@rivetkit/engine-kv-channel-protocol': + specifier: workspace:* + version: link:../../../engine/sdks/typescript/kv-channel-protocol '@rivetkit/engine-runner': specifier: workspace:* version: link:../../../engine/sdks/typescript/runner @@ -4173,13 +4192,13 @@ importers: version: link:../workflow-engine '@vercel/sandbox': specifier: '>=0.1.0' - version: 1.9.0 + version: 1.9.2 cbor-x: specifier: ^1.6.0 version: 1.6.0 computesdk: specifier: '>=0.1.0' - version: 2.5.3 + version: 2.5.4 drizzle-kit: specifier: ^0.31.2 version: 0.31.5 @@ -4194,7 +4213,7 @@ importers: version: 2.2.4 modal: specifier: '>=0.1.0' - version: 0.7.3 + version: 0.7.4 nanoevents: specifier: ^9.1.0 version: 9.1.0 @@ -4206,7 +4225,7 @@ importers: version: 9.9.5 sandbox-agent: specifier: ^0.4.2 - version: 0.4.2(@daytonaio/sdk@0.150.0(ws@8.19.0))(@e2b/code-interpreter@2.3.3)(@fly/sprites@0.0.1)(@vercel/sandbox@1.9.0)(computesdk@2.5.3)(dockerode@4.0.9)(get-port@7.1.0)(modal@0.7.3)(zod@4.1.13) + version: 0.4.2(@daytonaio/sdk@0.150.0(ws@8.19.0))(@e2b/code-interpreter@2.3.3)(@fly/sprites@0.0.1)(@vercel/sandbox@1.9.2)(computesdk@2.5.4)(dockerode@4.0.9)(get-port@7.1.0)(modal@0.7.4)(zod@4.1.13) tar: specifier: ^7.5.0 version: 7.5.7 @@ -4228,7 +4247,7 @@ importers: version: 2.3.11 '@copilotkit/llmock': specifier: ^1.6.0 - version: 1.6.0 + version: 1.7.1 '@daytonaio/sdk': specifier: ^0.150.0 version: 0.150.0(ws@8.19.0) @@ -4308,6 +4327,12 @@ importers: specifier: ^8.5.0 version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@22.19.10))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + rivetkit-typescript/packages/sqlite-native: + devDependencies: + '@napi-rs/cli': + specifier: ^2.18.0 + version: 2.18.4 + rivetkit-typescript/packages/sqlite-vfs: dependencies: '@rivetkit/bare-ts': @@ -4971,52 +4996,88 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-bedrock-runtime@3.1019.0': - resolution: {integrity: sha512-Wq1uMAZfySYofuwkFMaMM+k7epsGBRcJGE+ZosGB+8jC8Xs1lycbjSEFMt0Mo3z1qhkgEKGCQyjCbPTICMkkVw==} + '@aws-sdk/client-bedrock-runtime@3.1024.0': + resolution: {integrity: sha512-nIhsn0/eYrL2fTh4kMO7Hpfmhv+AkkXl0KGNpD6+fdmotGvRBWcDv9/PmP/+sT6gvrKTYyzH3vu4efpTPzzP0Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1007.0': + resolution: {integrity: sha512-QdFNDy+eKpcbv3ieGNl7XsDhpOj5mfb2xwnNM/YC108JpNJ5Ox79mbwtsKKqmQfen0JeaJml58vFnRHjfkjw9w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.19': + resolution: {integrity: sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.26': + resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.4': + resolution: {integrity: sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.17': + resolution: {integrity: sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.24': + resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.19': + resolution: {integrity: sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.26': + resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.18': + resolution: {integrity: sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1019.0': - resolution: {integrity: sha512-0pb9x7PPhS4oEi4c0rL3vzQQoXA4cWKtPuGga/UfVYLZ68yrqdq0NDKg0fr55qzdhNvWFCpmGx73g9Iyy03kkA==} + '@aws-sdk/credential-provider-ini@3.972.28': + resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.25': - resolution: {integrity: sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==} + '@aws-sdk/credential-provider-login@3.972.18': + resolution: {integrity: sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/crc64-nvme@3.972.5': - resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} + '@aws-sdk/credential-provider-login@3.972.28': + resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.23': - resolution: {integrity: sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==} + '@aws-sdk/credential-provider-node@3.972.19': + resolution: {integrity: sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.25': - resolution: {integrity: sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==} + '@aws-sdk/credential-provider-node@3.972.29': + resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.26': - resolution: {integrity: sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg==} + '@aws-sdk/credential-provider-process@3.972.17': + resolution: {integrity: sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.26': - resolution: {integrity: sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ==} + '@aws-sdk/credential-provider-process@3.972.24': + resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.27': - resolution: {integrity: sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg==} + '@aws-sdk/credential-provider-sso@3.972.18': + resolution: {integrity: sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.23': - resolution: {integrity: sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==} + '@aws-sdk/credential-provider-sso@3.972.28': + resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.26': - resolution: {integrity: sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg==} + '@aws-sdk/credential-provider-web-identity@3.972.18': + resolution: {integrity: sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.26': - resolution: {integrity: sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg==} + '@aws-sdk/credential-provider-web-identity@3.972.28': + resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} engines: {node: '>=20.0.0'} '@aws-sdk/eventstream-handler-node@3.972.12': @@ -5029,68 +5090,104 @@ packages: peerDependencies: '@aws-sdk/client-s3': ^3.1007.0 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': - resolution: {integrity: sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==} + '@aws-sdk/middleware-bucket-endpoint@3.972.7': + resolution: {integrity: sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-eventstream@3.972.8': resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-expect-continue@3.972.8': - resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} + '@aws-sdk/middleware-expect-continue@3.972.7': + resolution: {integrity: sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.973.5': + resolution: {integrity: sha512-Dp3hqE5W6hG8HQ3Uh+AINx9wjjqYmFHbxede54sGj3akx/haIQrkp85lNdTdC+ouNUcSYNiuGkzmyDREfHX1Gg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.5': - resolution: {integrity: sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==} + '@aws-sdk/middleware-host-header@3.972.7': + resolution: {integrity: sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-host-header@3.972.8': resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-location-constraint@3.972.8': - resolution: {integrity: sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==} + '@aws-sdk/middleware-location-constraint@3.972.7': + resolution: {integrity: sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.7': + resolution: {integrity: sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-logger@3.972.8': resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.7': + resolution: {integrity: sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.9': resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.26': - resolution: {integrity: sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==} + '@aws-sdk/middleware-sdk-s3@3.972.19': + resolution: {integrity: sha512-/CtOHHVFg4ZuN6CnLnYkrqWgVEnbOBC4kNiKa+4fldJ9cioDt3dD/f5vpq0cWLOXwmGL2zgVrVxNhjxWpxNMkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.7': + resolution: {integrity: sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-ssec@3.972.8': - resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} + '@aws-sdk/middleware-user-agent@3.972.20': + resolution: {integrity: sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.26': - resolution: {integrity: sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==} + '@aws-sdk/middleware-user-agent@3.972.28': + resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} engines: {node: '>=20.0.0'} '@aws-sdk/middleware-websocket@3.972.14': resolution: {integrity: sha512-qnfDlIHjm6DrTYNvWOUbnZdVKgtoKbO/Qzj+C0Wp5Y7VUrsvBRQtGKxD+hc+mRTS4N0kBJ6iZ3+zxm4N1OSyjg==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.996.16': - resolution: {integrity: sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g==} + '@aws-sdk/nested-clients@3.996.18': + resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.8': + resolution: {integrity: sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==} engines: {node: '>=20.0.0'} '@aws-sdk/region-config-resolver@3.972.10': resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.14': - resolution: {integrity: sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==} + '@aws-sdk/region-config-resolver@3.972.7': + resolution: {integrity: sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.7': + resolution: {integrity: sha512-mYhh7FY+7OOqjkYkd6+6GgJOsXK1xBWmuR+c5mxJPj2kr5TBNeZq+nUvE9kANWAux5UxDVrNOSiEM/wlHzC3Lg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1005.0': + resolution: {integrity: sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1021.0': + resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1024.0': + resolution: {integrity: sha512-eoyTMgd6OzoE1dq50um5Y53NrosEkWsjH0W6pswi7vrv1W9hY/7hR43jDcPevqqj+OQksf/5lc++FTqRlb8Y1Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1019.0': - resolution: {integrity: sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ==} + '@aws-sdk/types@3.973.5': + resolution: {integrity: sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==} engines: {node: '>=20.0.0'} '@aws-sdk/types@3.973.6': @@ -5101,6 +5198,10 @@ packages: resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-endpoints@3.996.4': + resolution: {integrity: sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-endpoints@3.996.5': resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} engines: {node: '>=20.0.0'} @@ -5113,11 +5214,14 @@ packages: resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-user-agent-browser@3.972.7': + resolution: {integrity: sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==} + '@aws-sdk/util-user-agent-browser@3.972.8': resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} - '@aws-sdk/util-user-agent-node@3.973.12': - resolution: {integrity: sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==} + '@aws-sdk/util-user-agent-node@3.973.14': + resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -5125,6 +5229,19 @@ packages: aws-crt: optional: true + '@aws-sdk/util-user-agent-node@3.973.5': + resolution: {integrity: sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.10': + resolution: {integrity: sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.16': resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} engines: {node: '>=20.0.0'} @@ -5614,10 +5731,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -5865,7 +5978,6 @@ packages: '@clerk/types@4.101.12': resolution: {integrity: sha512-ePXOla3B1qgPtV0AzrLx2PVC3s/lsjOSYnuIFAxaIlRNT2+eb/BjeoqtTOcezwbdQ00jQ2RvXahdfZRSEuvZ7A==} engines: {node: '>=18.17.0'} - deprecated: 'This package is no longer supported. Please import types from @clerk/shared/types instead. See the upgrade guide for more info: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3' '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} @@ -5967,12 +6079,16 @@ packages: peerDependencies: '@bufbuild/protobuf': ^2.2.0 - '@copilotkit/llmock@1.6.0': - resolution: {integrity: sha512-wq4J7ampjoEiOi6v2d7GMK5lTZcTnuhMduSPCIwmyxBTCPA3lekXyNKGJ4t3xM5OgoJReMQ5KmlfrMBVTRNGsA==} + '@copilotkit/aimock@1.7.0': + resolution: {integrity: sha512-X6B2z0MgGTg8N/geRg6zRVVgEp3krP+gYapwXCt2w3JU7BSf2q0laa4iHC+BZqPXf29iVDVwDM7BxB5LqhjcAg==} engines: {node: '>=20.15.0'} deprecated: This package has moved to @copilotkit/aimock hasBin: true + '@copilotkit/llmock@1.7.1': + resolution: {integrity: sha512-IHBhkowTi8baM67Z5fpFcmeEPwNmzEfSWejZ1hmur/nlNKPdc28n0aD9DUdSfvaRN7wjtHgcOF6RizCgfoqaaQ==} + deprecated: This package has moved to @copilotkit/aimock + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -7223,8 +7339,8 @@ packages: '@fortawesome/fontawesome-svg-core': ~6 || ~7 react: 19.1.0 - '@google/genai@1.47.0': - resolution: {integrity: sha512-0VV7AaXm5rQu3oRHNZNEubRAOL2lv5u+YA72eWnDwcOx3B1jFRbvtgL4drRHlocRHOnludvr3xmbQGbR+/RQAQ==} + '@google/genai@1.48.0': + resolution: {integrity: sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==} engines: {node: '>=20.0.0'} peerDependencies: '@modelcontextprotocol/sdk': ^1.25.2 @@ -7969,6 +8085,11 @@ packages: resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} engines: {node: '>=18'} + '@napi-rs/cli@2.18.4': + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + '@neophi/sieve-cache@1.5.0': resolution: {integrity: sha512-9T3nD5q51X1d4QYW6vouKW9hBSb2Tb/wB/2XoTr4oP5SCGtp3a7aTHHewQFylred1B21/Bhev6gy4x01FPBcbQ==} engines: {node: '>=18'} @@ -9998,10 +10119,6 @@ packages: resolution: {integrity: sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==} engines: {node: '>=18.0.0'} - '@smithy/abort-controller@4.2.12': - resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} - engines: {node: '>=18.0.0'} - '@smithy/chunked-blob-reader-native@4.2.3': resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} engines: {node: '>=18.0.0'} @@ -10010,38 +10127,66 @@ packages: resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.4.10': + resolution: {integrity: sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==} + engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.4.13': resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.12': - resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + '@smithy/core@3.23.13': + resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} engines: {node: '>=18.0.0'} '@smithy/core@3.23.9': resolution: {integrity: sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==} engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.11': + resolution: {integrity: sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.12': resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-codec@4.2.11': + resolution: {integrity: sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-codec@4.2.12': resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-browser@4.2.11': + resolution: {integrity: sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-browser@4.2.12': resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-config-resolver@4.3.11': + resolution: {integrity: sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-config-resolver@4.3.12': resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-node@4.2.11': + resolution: {integrity: sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-node@4.2.12': resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-universal@4.2.11': + resolution: {integrity: sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==} + engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-universal@4.2.12': resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} engines: {node: '>=18.0.0'} @@ -10054,16 +10199,24 @@ packages: resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} engines: {node: '>=18.0.0'} - '@smithy/hash-blob-browser@4.2.13': - resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} + '@smithy/hash-blob-browser@4.2.12': + resolution: {integrity: sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.11': + resolution: {integrity: sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==} engines: {node: '>=18.0.0'} '@smithy/hash-node@4.2.12': resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} engines: {node: '>=18.0.0'} - '@smithy/hash-stream-node@4.2.12': - resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} + '@smithy/hash-stream-node@4.2.11': + resolution: {integrity: sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.11': + resolution: {integrity: sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==} engines: {node: '>=18.0.0'} '@smithy/invalid-dependency@4.2.12': @@ -10078,8 +10231,12 @@ packages: resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} engines: {node: '>=18.0.0'} - '@smithy/md5-js@4.2.12': - resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + '@smithy/md5-js@4.2.11': + resolution: {integrity: sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.11': + resolution: {integrity: sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==} engines: {node: '>=18.0.0'} '@smithy/middleware-content-length@4.2.12': @@ -10090,20 +10247,24 @@ packages: resolution: {integrity: sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.27': - resolution: {integrity: sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==} + '@smithy/middleware-endpoint@4.4.28': + resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.40': + resolution: {integrity: sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.44': - resolution: {integrity: sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==} + '@smithy/middleware-retry@4.4.46': + resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} engines: {node: '>=18.0.0'} '@smithy/middleware-serde@4.2.12': resolution: {integrity: sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.15': - resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + '@smithy/middleware-serde@4.2.16': + resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} engines: {node: '>=18.0.0'} '@smithy/middleware-stack@4.2.11': @@ -10126,8 +10287,8 @@ packages: resolution: {integrity: sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.0': - resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + '@smithy/node-http-handler@4.5.1': + resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} engines: {node: '>=18.0.0'} '@smithy/property-provider@4.2.11': @@ -10162,6 +10323,10 @@ packages: resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.2.11': + resolution: {integrity: sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==} + engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.2.12': resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} engines: {node: '>=18.0.0'} @@ -10174,6 +10339,10 @@ packages: resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.11': + resolution: {integrity: sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.12': resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} engines: {node: '>=18.0.0'} @@ -10182,8 +10351,8 @@ packages: resolution: {integrity: sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.7': - resolution: {integrity: sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==} + '@smithy/smithy-client@4.12.8': + resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} engines: {node: '>=18.0.0'} '@smithy/types@4.13.0': @@ -10226,12 +10395,24 @@ packages: resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.43': - resolution: {integrity: sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==} + '@smithy/util-defaults-mode-browser@4.3.39': + resolution: {integrity: sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.47': - resolution: {integrity: sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==} + '@smithy/util-defaults-mode-browser@4.3.44': + resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.42': + resolution: {integrity: sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.48': + resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.2': + resolution: {integrity: sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.3.3': @@ -10250,16 +10431,20 @@ packages: resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.12': - resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + '@smithy/util-retry@4.2.11': + resolution: {integrity: sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.13': + resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} engines: {node: '>=18.0.0'} '@smithy/util-stream@4.5.17': resolution: {integrity: sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.20': - resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + '@smithy/util-stream@4.5.21': + resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.2': @@ -10274,8 +10459,8 @@ packages: resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.2.13': - resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + '@smithy/util-waiter@4.2.12': + resolution: {integrity: sha512-ek5hyDrzS6mBFsNCEX8LpM+EWSLq6b9FdmPRlkpXXhiJE6aIZehKT9clC6+nFpZAA+i/Yg0xlaPeWGNbf5rzQA==} engines: {node: '>=18.0.0'} '@smithy/uuid@1.1.2': @@ -11014,8 +11199,8 @@ packages: resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} engines: {node: '>= 20'} - '@vercel/sandbox@1.9.0': - resolution: {integrity: sha512-zgr1ad0tkT1xZn/8Vxo60wOUOLqMAVGo4WqJQ8/UDcUtWynNJsBjI2tiMdWZrAo9EKH1MIqEzJNkcclF0UT1EQ==} + '@vercel/sandbox@1.9.2': + resolution: {integrity: sha512-tKPKisnf9YSmqCr1X4mThLjNacTnWMmAfzYfoEul1aILdMHKpsECUBae9FASWL+PsZpT4hi1QrcSHkPXX212rw==} '@visx/axis@3.12.0': resolution: {integrity: sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==} @@ -11287,6 +11472,9 @@ packages: '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@workflow/serde@4.1.0-beta.2': + resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} + '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} @@ -11848,6 +12036,10 @@ packages: resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -12297,8 +12489,8 @@ packages: computeds@0.0.1: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - computesdk@2.5.3: - resolution: {integrity: sha512-YR3xLnBYokxNC/IdDXPiwIWd2dA/gud3wqXpOQbUhYhk7Kuk5Gz2sAko+naQTfZVB5zXVwOqjQUytJF3TL5uxg==} + computesdk@2.5.4: + resolution: {integrity: sha512-5y705cJcGo8TwD9oPxRsfQ+G2oqslv/bCfGC1vAUA7p5xdL7ScIEI2bVYJJy10gnFyeHHgNHwMZ++tesB0PLjg==} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -13598,9 +13790,16 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.1.2: + resolution: {integrity: sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==} + fast-xml-builder@1.1.4: resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-parser@5.4.1: + resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + hasBin: true + fast-xml-parser@5.5.8: resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} hasBin: true @@ -14746,8 +14945,8 @@ packages: resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==} engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} - koffi@2.15.2: - resolution: {integrity: sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==} + koffi@2.15.4: + resolution: {integrity: sha512-6l7xxt8heHWQ63WyGd8ofne4TrzhqeKHhvSlI3GnxMIHp3PlDrOPyZbW5YNINXNma1qrKkpM/PGLY8U0V8Hxbw==} kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -15513,8 +15712,8 @@ packages: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@3.0.8: @@ -15568,8 +15767,8 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - modal@0.7.3: - resolution: {integrity: sha512-4CliqNF15sZPBGpSoCj5Y9fd8fTp1ONrBLIJiC4amm/Qzc1rn8CH45SVzSu+1DokHCIRiZqQ1xMhRKpDvDCkBw==} + modal@0.7.4: + resolution: {integrity: sha512-md/+L67tM1RazAt2xvLO+gUqRz6zllyYoNNiM8h+Eb1wLy7JzliH7vnx9f9Sq4zE3qQHENpX0Tjy/LSkIyrANA==} module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} @@ -16154,8 +16353,12 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + path-expression-matcher@1.1.3: + resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==} + engines: {node: '>=14.0.0'} + + path-expression-matcher@1.2.1: + resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==} engines: {node: '>=14.0.0'} path-is-absolute@1.0.1: @@ -18186,8 +18389,8 @@ packages: resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} engines: {node: '>=20.18.1'} - undici@7.24.6: - resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} + undici@7.24.7: + resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.21: @@ -19509,20 +19712,20 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -19532,7 +19735,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -19540,7 +19743,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -19549,31 +19752,31 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.1019.0': + '@aws-sdk/client-bedrock-runtime@3.1024.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/credential-provider-node': 3.972.27 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-node': 3.972.29 '@aws-sdk/eventstream-handler-node': 3.972.12 '@aws-sdk/middleware-eventstream': 3.972.8 '@aws-sdk/middleware-host-header': 3.972.8 '@aws-sdk/middleware-logger': 3.972.8 '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.26 + '@aws-sdk/middleware-user-agent': 3.972.28 '@aws-sdk/middleware-websocket': 3.972.14 '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/token-providers': 3.1019.0 + '@aws-sdk/token-providers': 3.1024.0 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.12 + '@aws-sdk/util-user-agent-node': 3.973.14 '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/eventstream-serde-browser': 4.2.12 '@smithy/eventstream-serde-config-resolver': 4.3.12 '@smithy/eventstream-serde-node': 4.2.12 @@ -19581,142 +19784,198 @@ snapshots: '@smithy/hash-node': 4.2.12 '@smithy/invalid-dependency': 4.2.12 '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 '@smithy/middleware-stack': 4.2.12 '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/url-parser': 4.2.12 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 '@smithy/util-endpoints': 3.3.3 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-retry': 4.2.13 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-s3@3.1019.0': + '@aws-sdk/client-s3@3.1007.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/credential-provider-node': 3.972.27 - '@aws-sdk/middleware-bucket-endpoint': 3.972.8 - '@aws-sdk/middleware-expect-continue': 3.972.8 - '@aws-sdk/middleware-flexible-checksums': 3.974.5 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-location-constraint': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-sdk-s3': 3.972.26 - '@aws-sdk/middleware-ssec': 3.972.8 - '@aws-sdk/middleware-user-agent': 3.972.26 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/signature-v4-multi-region': 3.996.14 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.12 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-blob-browser': 4.2.13 - '@smithy/hash-node': 4.2.12 - '@smithy/hash-stream-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/md5-js': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-node': 3.972.19 + '@aws-sdk/middleware-bucket-endpoint': 3.972.7 + '@aws-sdk/middleware-expect-continue': 3.972.7 + '@aws-sdk/middleware-flexible-checksums': 3.973.5 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-location-constraint': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-sdk-s3': 3.972.19 + '@aws-sdk/middleware-ssec': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/signature-v4-multi-region': 3.996.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/eventstream-serde-browser': 4.2.11 + '@smithy/eventstream-serde-config-resolver': 4.3.11 + '@smithy/eventstream-serde-node': 4.2.11 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-blob-browser': 4.2.12 + '@smithy/hash-node': 4.2.11 + '@smithy/hash-stream-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/md5-js': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/util-stream': 4.5.17 '@smithy/util-utf8': 4.2.2 - '@smithy/util-waiter': 4.2.13 + '@smithy/util-waiter': 4.2.12 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.973.25': + '@aws-sdk/core@3.973.19': + dependencies: + '@aws-sdk/types': 3.973.5 + '@aws-sdk/xml-builder': 3.972.10 + '@smithy/core': 3.23.9 + '@smithy/node-config-provider': 4.3.11 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.973.26': dependencies: '@aws-sdk/types': 3.973.6 '@aws-sdk/xml-builder': 3.972.16 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/util-base64': 4.3.2 '@smithy/util-middleware': 4.2.12 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.972.5': + '@aws-sdk/crc64-nvme@3.972.4': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.23': + '@aws-sdk/credential-provider-env@3.972.17': dependencies: - '@aws-sdk/core': 3.973.25 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.24': + dependencies: + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.25': + '@aws-sdk/credential-provider-http@3.972.19': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/node-http-handler': 4.4.14 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/util-stream': 4.5.17 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.26': dependencies: - '@aws-sdk/core': 3.973.25 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.26': + '@aws-sdk/credential-provider-ini@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/credential-provider-env': 3.972.17 + '@aws-sdk/credential-provider-http': 3.972.19 + '@aws-sdk/credential-provider-login': 3.972.18 + '@aws-sdk/credential-provider-process': 3.972.17 + '@aws-sdk/credential-provider-sso': 3.972.18 + '@aws-sdk/credential-provider-web-identity': 3.972.18 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/credential-provider-imds': 4.2.11 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-ini@3.972.28': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/credential-provider-env': 3.972.23 - '@aws-sdk/credential-provider-http': 3.972.25 - '@aws-sdk/credential-provider-login': 3.972.26 - '@aws-sdk/credential-provider-process': 3.972.23 - '@aws-sdk/credential-provider-sso': 3.972.26 - '@aws-sdk/credential-provider-web-identity': 3.972.26 - '@aws-sdk/nested-clients': 3.996.16 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-login': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/credential-provider-imds': 4.2.12 '@smithy/property-provider': 4.2.12 @@ -19726,10 +19985,23 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.26': + '@aws-sdk/credential-provider-login@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.28': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/protocol-http': 5.3.12 @@ -19739,14 +20011,31 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.27': + '@aws-sdk/credential-provider-node@3.972.19': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.17 + '@aws-sdk/credential-provider-http': 3.972.19 + '@aws-sdk/credential-provider-ini': 3.972.18 + '@aws-sdk/credential-provider-process': 3.972.17 + '@aws-sdk/credential-provider-sso': 3.972.18 + '@aws-sdk/credential-provider-web-identity': 3.972.18 + '@aws-sdk/types': 3.973.5 + '@smithy/credential-provider-imds': 4.2.11 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.29': dependencies: - '@aws-sdk/credential-provider-env': 3.972.23 - '@aws-sdk/credential-provider-http': 3.972.25 - '@aws-sdk/credential-provider-ini': 3.972.26 - '@aws-sdk/credential-provider-process': 3.972.23 - '@aws-sdk/credential-provider-sso': 3.972.26 - '@aws-sdk/credential-provider-web-identity': 3.972.26 + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-ini': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 '@aws-sdk/types': 3.973.6 '@smithy/credential-provider-imds': 4.2.12 '@smithy/property-provider': 4.2.12 @@ -19756,20 +20045,42 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.23': + '@aws-sdk/credential-provider-process@3.972.17': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.24': dependencies: - '@aws-sdk/core': 3.973.25 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.26': + '@aws-sdk/credential-provider-sso@3.972.18': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/token-providers': 3.1019.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/token-providers': 3.1005.0 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-sso@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/token-providers': 3.1021.0 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -19778,10 +20089,22 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.26': + '@aws-sdk/credential-provider-web-identity@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.28': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -19797,9 +20120,9 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/lib-storage@3.1007.0(@aws-sdk/client-s3@3.1019.0)': + '@aws-sdk/lib-storage@3.1007.0(@aws-sdk/client-s3@3.1007.0)': dependencies: - '@aws-sdk/client-s3': 3.1019.0 + '@aws-sdk/client-s3': 3.1007.0 '@smithy/abort-controller': 4.2.11 '@smithy/middleware-endpoint': 4.4.23 '@smithy/smithy-client': 4.12.3 @@ -19808,13 +20131,13 @@ snapshots: stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': + '@aws-sdk/middleware-bucket-endpoint@3.972.7': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 @@ -19825,30 +20148,37 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.972.8': + '@aws-sdk/middleware-expect-continue@3.972.7': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.5 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.5': + '@aws-sdk/middleware-flexible-checksums@3.973.5': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/crc64-nvme': 3.972.5 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/crc64-nvme': 3.972.4 + '@aws-sdk/types': 3.973.5 '@smithy/is-array-buffer': 4.2.2 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-stream': 4.5.17 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.7': + dependencies: + '@aws-sdk/types': 3.973.5 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -19856,10 +20186,16 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.972.8': + '@aws-sdk/middleware-location-constraint@3.972.7': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.7': + dependencies: + '@aws-sdk/types': 3.973.5 + '@smithy/types': 4.13.0 tslib: 2.8.1 '@aws-sdk/middleware-logger@3.972.8': @@ -19868,6 +20204,14 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.7': + dependencies: + '@aws-sdk/types': 3.973.5 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.9': dependencies: '@aws-sdk/types': 3.973.6 @@ -19876,38 +20220,49 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.26': + '@aws-sdk/middleware-sdk-s3@3.972.19': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/core': 3.23.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@smithy/core': 3.23.9 + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 '@smithy/util-config-provider': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-stream': 4.5.17 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-ssec@3.972.8': + '@aws-sdk/middleware-ssec@3.972.7': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.5 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@smithy/core': 3.23.9 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + '@smithy/util-retry': 4.2.11 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.26': + '@aws-sdk/middleware-user-agent@3.972.28': dependencies: - '@aws-sdk/core': 3.973.25 + '@aws-sdk/core': 3.973.26 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 tslib: 2.8.1 '@aws-sdk/middleware-websocket@3.972.14': @@ -19925,44 +20280,87 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.996.16': + '@aws-sdk/nested-clients@3.996.18': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.25 + '@aws-sdk/core': 3.973.26 '@aws-sdk/middleware-host-header': 3.972.8 '@aws-sdk/middleware-logger': 3.972.8 '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.26 + '@aws-sdk/middleware-user-agent': 3.972.28 '@aws-sdk/region-config-resolver': 3.972.10 '@aws-sdk/types': 3.973.6 '@aws-sdk/util-endpoints': 3.996.5 '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.12 + '@aws-sdk/util-user-agent-node': 3.973.14 '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/fetch-http-handler': 5.3.15 '@smithy/hash-node': 4.2.12 '@smithy/invalid-dependency': 4.2.12 '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 '@smithy/middleware-stack': 4.2.12 '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/url-parser': 4.2.12 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 '@smithy/util-endpoints': 3.3.3 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/nested-clients@3.996.8': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.19 + '@aws-sdk/middleware-host-header': 3.972.7 + '@aws-sdk/middleware-logger': 3.972.7 + '@aws-sdk/middleware-recursion-detection': 3.972.7 + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.7 + '@aws-sdk/types': 3.973.5 + '@aws-sdk/util-endpoints': 3.996.4 + '@aws-sdk/util-user-agent-browser': 3.972.7 + '@aws-sdk/util-user-agent-node': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/core': 3.23.9 + '@smithy/fetch-http-handler': 5.3.13 + '@smithy/hash-node': 4.2.11 + '@smithy/invalid-dependency': 4.2.11 + '@smithy/middleware-content-length': 4.2.11 + '@smithy/middleware-endpoint': 4.4.23 + '@smithy/middleware-retry': 4.4.40 + '@smithy/middleware-serde': 4.2.12 + '@smithy/middleware-stack': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/node-http-handler': 4.4.14 + '@smithy/protocol-http': 5.3.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.39 + '@smithy/util-defaults-mode-node': 4.2.42 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: @@ -19976,19 +20374,51 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.14': + '@aws-sdk/region-config-resolver@3.972.7': + dependencies: + '@aws-sdk/types': 3.973.5 + '@smithy/config-resolver': 4.4.10 + '@smithy/node-config-provider': 4.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.7': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.19 + '@aws-sdk/types': 3.973.5 + '@smithy/protocol-http': 5.3.11 + '@smithy/signature-v4': 5.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1005.0': + dependencies: + '@aws-sdk/core': 3.973.19 + '@aws-sdk/nested-clients': 3.996.8 + '@aws-sdk/types': 3.973.5 + '@smithy/property-provider': 4.2.11 + '@smithy/shared-ini-file-loader': 4.4.6 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.1021.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.972.26 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 '@smithy/types': 4.13.1 tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt - '@aws-sdk/token-providers@3.1019.0': + '@aws-sdk/token-providers@3.1024.0': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 '@aws-sdk/types': 3.973.6 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 @@ -19997,6 +20427,11 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/types@3.973.5': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@aws-sdk/types@3.973.6': dependencies: '@smithy/types': 4.13.1 @@ -20006,6 +20441,14 @@ snapshots: dependencies: tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.4': + dependencies: + '@aws-sdk/types': 3.973.5 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + '@smithy/util-endpoints': 3.3.2 + tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.5': dependencies: '@aws-sdk/types': 3.973.6 @@ -20025,6 +20468,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.7': + dependencies: + '@aws-sdk/types': 3.973.5 + '@smithy/types': 4.13.0 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.8': dependencies: '@aws-sdk/types': 3.973.6 @@ -20032,15 +20482,29 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.12': + '@aws-sdk/util-user-agent-node@3.973.14': dependencies: - '@aws-sdk/middleware-user-agent': 3.972.26 + '@aws-sdk/middleware-user-agent': 3.972.28 '@aws-sdk/types': 3.973.6 '@smithy/node-config-provider': 4.3.12 '@smithy/types': 4.13.1 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.973.5': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.20 + '@aws-sdk/types': 3.973.5 + '@smithy/node-config-provider': 4.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.10': + dependencies: + '@smithy/types': 4.13.0 + fast-xml-parser: 5.4.1 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.16': dependencies: '@smithy/types': 4.13.1 @@ -20629,8 +21093,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.28.6': {} - '@babel/runtime@7.29.2': {} '@babel/template@7.28.6': @@ -21035,7 +21497,11 @@ snapshots: dependencies: '@bufbuild/protobuf': 2.11.0 - '@copilotkit/llmock@1.6.0': {} + '@copilotkit/aimock@1.7.0': {} + + '@copilotkit/llmock@1.7.1': + dependencies: + '@copilotkit/aimock': 1.7.0 '@cspotcode/source-map-support@0.8.1': dependencies: @@ -21051,8 +21517,8 @@ snapshots: '@daytonaio/sdk@0.150.0(ws@8.19.0)': dependencies: - '@aws-sdk/client-s3': 3.1019.0 - '@aws-sdk/lib-storage': 3.1007.0(@aws-sdk/client-s3@3.1019.0) + '@aws-sdk/client-s3': 3.1007.0 + '@aws-sdk/lib-storage': 3.1007.0(@aws-sdk/client-s3@3.1007.0) '@daytonaio/api-client': 0.150.0 '@daytonaio/toolbox-api-client': 0.150.0 '@iarna/toml': 2.2.5 @@ -21128,7 +21594,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.28.6 - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -21157,7 +21623,7 @@ snapshots: '@emotion/react@11.11.1(@types/react@19.2.13)(react@19.1.0)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.3.3 @@ -22152,7 +22618,7 @@ snapshots: '@fortawesome/fontawesome-svg-core': 7.1.0 react: 19.1.0 - '@google/genai@1.47.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))': + '@google/genai@1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 @@ -22165,7 +22631,7 @@ snapshots: - supports-color - utf-8-validate - '@google/genai@1.47.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))': + '@google/genai@1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 @@ -22855,8 +23321,8 @@ snapshots: '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) - '@aws-sdk/client-bedrock-runtime': 3.1019.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76)) + '@aws-sdk/client-bedrock-runtime': 3.1024.0 + '@google/genai': 1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76)) '@mistralai/mistralai': 1.14.1 '@sinclair/typebox': 0.34.41 ajv: 8.17.1 @@ -22865,7 +23331,7 @@ snapshots: openai: 6.26.0(ws@8.19.0)(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 - undici: 7.24.6 + undici: 7.24.7 zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22879,8 +23345,8 @@ snapshots: '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@4.1.13) - '@aws-sdk/client-bedrock-runtime': 3.1019.0 - '@google/genai': 1.47.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13)) + '@aws-sdk/client-bedrock-runtime': 3.1024.0 + '@google/genai': 1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13)) '@mistralai/mistralai': 1.14.1 '@sinclair/typebox': 0.34.41 ajv: 8.17.1 @@ -22889,7 +23355,7 @@ snapshots: openai: 6.26.0(ws@8.19.0)(zod@4.1.13) partial-json: 0.1.7 proxy-agent: 6.5.0 - undici: 7.24.6 + undici: 7.24.7 zod-to-json-schema: 3.25.1(zod@4.1.13) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22916,10 +23382,10 @@ snapshots: hosted-git-info: 9.0.2 ignore: 7.0.5 marked: 15.0.12 - minimatch: 10.2.4 + minimatch: 10.2.5 proper-lockfile: 4.1.2 strip-ansi: 7.1.2 - undici: 7.24.6 + undici: 7.24.7 yaml: 2.8.2 optionalDependencies: '@mariozechner/clipboard': 0.3.2 @@ -22948,10 +23414,10 @@ snapshots: hosted-git-info: 9.0.2 ignore: 7.0.5 marked: 15.0.12 - minimatch: 10.2.4 + minimatch: 10.2.5 proper-lockfile: 4.1.2 strip-ansi: 7.1.2 - undici: 7.24.6 + undici: 7.24.7 yaml: 2.8.2 optionalDependencies: '@mariozechner/clipboard': 0.3.2 @@ -22972,7 +23438,7 @@ snapshots: marked: 15.0.12 mime-types: 3.0.2 optionalDependencies: - koffi: 2.15.2 + koffi: 2.15.4 '@mdx-js/mdx@3.1.1': dependencies: @@ -23299,7 +23765,7 @@ snapshots: '@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.9) + '@hono/node-server': 1.19.12(hono@4.11.9) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -23321,7 +23787,7 @@ snapshots: '@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13)': dependencies: - '@hono/node-server': 1.19.9(hono@4.11.9) + '@hono/node-server': 1.19.12(hono@4.11.9) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -23380,6 +23846,8 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 + '@napi-rs/cli@2.18.4': {} + '@neophi/sieve-cache@1.5.0': {} '@next/env@15.5.9': @@ -24165,7 +24633,7 @@ snapshots: '@radix-ui/react-compose-refs@1.0.0(react@19.1.0)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 react: 19.1.0 '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.13)(react@19.1.0)': @@ -24518,7 +24986,7 @@ snapshots: '@radix-ui/react-slot@1.0.1(react@19.1.0)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 '@radix-ui/react-compose-refs': 1.0.0(react@19.1.0) react: 19.1.0 @@ -25828,11 +26296,6 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/abort-controller@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - '@smithy/chunked-blob-reader-native@4.2.3': dependencies: '@smithy/util-base64': 4.3.2 @@ -25842,6 +26305,15 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/config-resolver@4.4.10': + dependencies: + '@smithy/node-config-provider': 4.3.11 + '@smithy/types': 4.13.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.2 + '@smithy/util-middleware': 4.2.11 + tslib: 2.8.1 + '@smithy/config-resolver@4.4.13': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -25851,7 +26323,7 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 - '@smithy/core@3.23.12': + '@smithy/core@3.23.13': dependencies: '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 @@ -25859,7 +26331,7 @@ snapshots: '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 '@smithy/util-utf8': 4.2.2 '@smithy/uuid': 1.1.2 tslib: 2.8.1 @@ -25877,6 +26349,14 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.11': + dependencies: + '@smithy/node-config-provider': 4.3.11 + '@smithy/property-provider': 4.2.11 + '@smithy/types': 4.13.0 + '@smithy/url-parser': 4.2.11 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.12': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -25885,6 +26365,13 @@ snapshots: '@smithy/url-parser': 4.2.12 tslib: 2.8.1 + '@smithy/eventstream-codec@4.2.11': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.13.0 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + '@smithy/eventstream-codec@4.2.12': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -25892,23 +26379,46 @@ snapshots: '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 + '@smithy/eventstream-serde-browser@4.2.11': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/eventstream-serde-browser@4.2.12': dependencies: '@smithy/eventstream-serde-universal': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-config-resolver@4.3.11': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/eventstream-serde-config-resolver@4.3.12': dependencies: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-node@4.2.11': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/eventstream-serde-node@4.2.12': dependencies: '@smithy/eventstream-serde-universal': 4.2.12 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/eventstream-serde-universal@4.2.11': + dependencies: + '@smithy/eventstream-codec': 4.2.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/eventstream-serde-universal@4.2.12': dependencies: '@smithy/eventstream-codec': 4.2.12 @@ -25931,11 +26441,18 @@ snapshots: '@smithy/util-base64': 4.3.2 tslib: 2.8.1 - '@smithy/hash-blob-browser@4.2.13': + '@smithy/hash-blob-browser@4.2.12': dependencies: '@smithy/chunked-blob-reader': 5.2.2 '@smithy/chunked-blob-reader-native': 4.2.3 - '@smithy/types': 4.13.1 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.11': + dependencies: + '@smithy/types': 4.13.0 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 '@smithy/hash-node@4.2.12': @@ -25945,12 +26462,17 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/hash-stream-node@4.2.12': + '@smithy/hash-stream-node@4.2.11': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.13.0 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.11': + dependencies: + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.12': dependencies: '@smithy/types': 4.13.1 @@ -25964,12 +26486,18 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/md5-js@4.2.12': + '@smithy/md5-js@4.2.11': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.13.0 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/middleware-content-length@4.2.11': + dependencies: + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/middleware-content-length@4.2.12': dependencies: '@smithy/protocol-http': 5.3.12 @@ -25987,10 +26515,10 @@ snapshots: '@smithy/util-middleware': 4.2.11 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.27': + '@smithy/middleware-endpoint@4.4.28': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-serde': 4.2.15 + '@smithy/core': 3.23.13 + '@smithy/middleware-serde': 4.2.16 '@smithy/node-config-provider': 4.3.12 '@smithy/shared-ini-file-loader': 4.4.7 '@smithy/types': 4.13.1 @@ -25998,15 +26526,27 @@ snapshots: '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.44': + '@smithy/middleware-retry@4.4.40': + dependencies: + '@smithy/node-config-provider': 4.3.11 + '@smithy/protocol-http': 5.3.11 + '@smithy/service-error-classification': 4.2.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-retry': 4.2.11 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.46': dependencies: '@smithy/node-config-provider': 4.3.12 '@smithy/protocol-http': 5.3.12 '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/util-retry': 4.2.13 '@smithy/uuid': 1.1.2 tslib: 2.8.1 @@ -26016,9 +26556,9 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.15': + '@smithy/middleware-serde@4.2.16': dependencies: - '@smithy/core': 3.23.12 + '@smithy/core': 3.23.13 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 tslib: 2.8.1 @@ -26055,9 +26595,8 @@ snapshots: '@smithy/types': 4.13.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.5.0': + '@smithy/node-http-handler@4.5.1': dependencies: - '@smithy/abort-controller': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/querystring-builder': 4.2.12 '@smithy/types': 4.13.1 @@ -26105,6 +26644,10 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/service-error-classification@4.2.11': + dependencies: + '@smithy/types': 4.13.0 + '@smithy/service-error-classification@4.2.12': dependencies: '@smithy/types': 4.13.1 @@ -26119,6 +26662,17 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/signature-v4@5.3.11': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.11 + '@smithy/types': 4.13.0 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.11 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/signature-v4@5.3.12': dependencies: '@smithy/is-array-buffer': 4.2.2 @@ -26140,14 +26694,14 @@ snapshots: '@smithy/util-stream': 4.5.17 tslib: 2.8.1 - '@smithy/smithy-client@4.12.7': + '@smithy/smithy-client@4.12.8': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-endpoint': 4.4.27 + '@smithy/core': 3.23.13 + '@smithy/middleware-endpoint': 4.4.28 '@smithy/middleware-stack': 4.2.12 '@smithy/protocol-http': 5.3.12 '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@smithy/util-stream': 4.5.21 tslib: 2.8.1 '@smithy/types@4.13.0': @@ -26198,23 +26752,46 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.43': + '@smithy/util-defaults-mode-browser@4.3.39': + dependencies: + '@smithy/property-provider': 4.2.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.44': dependencies: '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.47': + '@smithy/util-defaults-mode-node@4.2.42': + dependencies: + '@smithy/config-resolver': 4.4.10 + '@smithy/credential-provider-imds': 4.2.11 + '@smithy/node-config-provider': 4.3.11 + '@smithy/property-provider': 4.2.11 + '@smithy/smithy-client': 4.12.3 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.48': dependencies: '@smithy/config-resolver': 4.4.13 '@smithy/credential-provider-imds': 4.2.12 '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 + '@smithy/smithy-client': 4.12.8 '@smithy/types': 4.13.1 tslib: 2.8.1 + '@smithy/util-endpoints@3.3.2': + dependencies: + '@smithy/node-config-provider': 4.3.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + '@smithy/util-endpoints@3.3.3': dependencies: '@smithy/node-config-provider': 4.3.12 @@ -26235,7 +26812,13 @@ snapshots: '@smithy/types': 4.13.1 tslib: 2.8.1 - '@smithy/util-retry@4.2.12': + '@smithy/util-retry@4.2.11': + dependencies: + '@smithy/service-error-classification': 4.2.11 + '@smithy/types': 4.13.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.13': dependencies: '@smithy/service-error-classification': 4.2.12 '@smithy/types': 4.13.1 @@ -26252,10 +26835,10 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/util-stream@4.5.20': + '@smithy/util-stream@4.5.21': dependencies: '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 + '@smithy/node-http-handler': 4.5.1 '@smithy/types': 4.13.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 @@ -26277,10 +26860,10 @@ snapshots: '@smithy/util-buffer-from': 4.2.2 tslib: 2.8.1 - '@smithy/util-waiter@4.2.13': + '@smithy/util-waiter@4.2.12': dependencies: - '@smithy/abort-controller': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/abort-controller': 4.2.11 + '@smithy/types': 4.13.0 tslib: 2.8.1 '@smithy/uuid@1.1.2': @@ -27069,15 +27652,16 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vercel/sandbox@1.9.0': + '@vercel/sandbox@1.9.2': dependencies: '@vercel/oidc': 3.2.0 + '@workflow/serde': 4.1.0-beta.2 async-retry: 1.3.3 jsonlines: 0.1.1 ms: 2.1.3 picocolors: 1.1.1 tar-stream: 3.1.7 - undici: 7.24.6 + undici: 7.24.7 xdg-app-paths: 5.1.0 zod: 3.24.4 transitivePeerDependencies: @@ -27626,6 +28210,8 @@ snapshots: '@webgpu/types@0.1.69': {} + '@workflow/serde@4.1.0-beta.2': {} + '@xmldom/xmldom@0.7.13': {} '@xmldom/xmldom@0.8.11': {} @@ -28130,7 +28716,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 cosmiconfig: 7.1.0 resolve: 1.22.11 @@ -28357,6 +28943,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -28870,7 +29460,7 @@ snapshots: computeds@0.0.1: {} - computesdk@2.5.3: + computesdk@2.5.4: dependencies: '@computesdk/cmd': 0.4.1 @@ -29456,7 +30046,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 csstype: 3.2.3 dom-serializer@2.0.0: @@ -30276,14 +30866,23 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.1.2: + dependencies: + path-expression-matcher: 1.1.3 + fast-xml-builder@1.1.4: dependencies: - path-expression-matcher: 1.2.0 + path-expression-matcher: 1.2.1 + + fast-xml-parser@5.4.1: + dependencies: + fast-xml-builder: 1.1.2 + strnum: 2.2.0 fast-xml-parser@5.5.8: dependencies: fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 + path-expression-matcher: 1.2.1 strnum: 2.2.0 fastest-levenshtein@1.0.16: {} @@ -31056,7 +31655,7 @@ snapshots: history@5.3.0: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 hmac-drbg@1.0.1: dependencies: @@ -31507,7 +32106,7 @@ snapshots: json-schema-to-ts@3.1.1: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 ts-algebra: 2.0.0 json-schema-traverse@0.4.1: {} @@ -31612,7 +32211,7 @@ snapshots: transitivePeerDependencies: - supports-color - koffi@2.15.2: + koffi@2.15.4: optional: true kolorist@1.8.0: {} @@ -31918,7 +32517,7 @@ snapshots: md5.js@1.3.5: dependencies: - hash-base: 3.1.2 + hash-base: 3.0.5 inherits: 2.0.4 safe-buffer: 5.2.1 @@ -32839,9 +33438,9 @@ snapshots: dependencies: brace-expansion: 5.0.3 - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.3 + brace-expansion: 5.0.5 minimatch@3.0.8: dependencies: @@ -32890,7 +33489,7 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - modal@0.7.3: + modal@0.7.4: dependencies: cbor-x: 1.6.0 long: 5.3.2 @@ -33607,7 +34206,9 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.2.0: {} + path-expression-matcher@1.1.3: {} + + path-expression-matcher@1.2.1: {} path-is-absolute@1.0.1: {} @@ -34159,7 +34760,7 @@ snapshots: react-helmet-async@1.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 invariant: 2.2.4 prop-types: 15.8.1 react: 19.1.0 @@ -34335,7 +34936,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.29.2 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -34789,7 +35390,7 @@ snapshots: safer-buffer@2.1.2: {} - sandbox-agent@0.4.2(@daytonaio/sdk@0.150.0(ws@8.19.0))(@e2b/code-interpreter@2.3.3)(@fly/sprites@0.0.1)(@vercel/sandbox@1.9.0)(computesdk@2.5.3)(dockerode@4.0.9)(get-port@7.1.0)(modal@0.7.3)(zod@4.1.13): + sandbox-agent@0.4.2(@daytonaio/sdk@0.150.0(ws@8.19.0))(@e2b/code-interpreter@2.3.3)(@fly/sprites@0.0.1)(@vercel/sandbox@1.9.2)(computesdk@2.5.4)(dockerode@4.0.9)(get-port@7.1.0)(modal@0.7.4)(zod@4.1.13): dependencies: '@sandbox-agent/cli-shared': 0.4.2 acp-http-client: 0.4.2(zod@4.1.13) @@ -34798,11 +35399,11 @@ snapshots: '@e2b/code-interpreter': 2.3.3 '@fly/sprites': 0.0.1 '@sandbox-agent/cli': 0.4.2 - '@vercel/sandbox': 1.9.0 - computesdk: 2.5.3 + '@vercel/sandbox': 1.9.2 + computesdk: 2.5.4 dockerode: 4.0.9 get-port: 7.1.0 - modal: 0.7.3 + modal: 0.7.4 transitivePeerDependencies: - zod @@ -36021,7 +36622,7 @@ snapshots: undici@7.14.0: {} - undici@7.24.6: {} + undici@7.24.7: {} unenv@2.0.0-rc.21: dependencies: @@ -36816,7 +37417,7 @@ snapshots: expect-type: 1.2.2 magic-string: 0.30.21 pathe: 1.1.2 - std-env: 3.10.0 + std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinypool: 1.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bb3ce9b987..34265d5adf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - engine/sdks/typescript/envoy-client - engine/sdks/typescript/envoy-protocol - engine/sdks/typescript/runner + - engine/sdks/typescript/kv-channel-protocol - engine/sdks/typescript/runner-protocol - engine/sdks/typescript/test-envoy - engine/sdks/typescript/test-runner diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts b/rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts index 8b26c8bbb0..aac687f2ec 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/actor-handler-do.ts @@ -13,7 +13,7 @@ import { createCloudflareActorsActorDriverBuilder, } from "./actor-driver"; import { buildActorId, parseActorId } from "./actor-id"; -import { kvGet, kvPut } from "./actor-kv"; +import { kvDelete, kvDeleteRange, kvGet, kvPut } from "./actor-kv"; import { GLOBAL_KV_KEYS } from "./global-kv"; import type { Bindings } from "./handler"; import { getCloudflareAmbientEnv } from "./handler"; @@ -32,6 +32,10 @@ export interface ActorHandlerInterface extends DurableObject { | undefined >; managerKvGet(key: Uint8Array): Promise; + managerKvBatchGet(keys: Uint8Array[]): Promise<(Uint8Array | null)[]>; + managerKvBatchPut(entries: [Uint8Array, Uint8Array][]): Promise; + managerKvBatchDelete(keys: Uint8Array[]): Promise; + managerKvDeleteRange(start: Uint8Array, end: Uint8Array): Promise; } export interface ActorInitRequest { @@ -281,6 +285,33 @@ export function createActorDurableObject( return kvGet(this.ctx.storage.sql, key); } + /** RPC called by ManagerDriver.kvBatchGet to read multiple keys from KV. */ + async managerKvBatchGet(keys: Uint8Array[]): Promise<(Uint8Array | null)[]> { + const sql = this.ctx.storage.sql; + return keys.map((key) => kvGet(sql, key)); + } + + /** RPC called by ManagerDriver.kvBatchPut to write multiple entries to KV. */ + async managerKvBatchPut(entries: [Uint8Array, Uint8Array][]): Promise { + const sql = this.ctx.storage.sql; + for (const [key, value] of entries) { + kvPut(sql, key, value); + } + } + + /** RPC called by ManagerDriver.kvBatchDelete to delete multiple keys from KV. */ + async managerKvBatchDelete(keys: Uint8Array[]): Promise { + const sql = this.ctx.storage.sql; + for (const key of keys) { + kvDelete(sql, key); + } + } + + /** RPC called by ManagerDriver.kvDeleteRange to delete a key range from KV. */ + async managerKvDeleteRange(start: Uint8Array, end: Uint8Array): Promise { + kvDeleteRange(this.ctx.storage.sql, start, end); + } + /** RPC called by the manager to create a DO. Can optionally allow existing actors. */ async create(req: ActorInitRequest): Promise { // Check if actor exists diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/config.ts b/rivetkit-typescript/packages/cloudflare-workers/src/config.ts index ac5cb59bf5..d00a359f9e 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/config.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/config.ts @@ -5,8 +5,8 @@ const ConfigSchemaBase = z.object({ /** Path that the Rivet manager API will be mounted. */ managerPath: z.string().optional().default("/api/rivet"), - /** Deprecated. Runner key for authentication. */ - runnerKey: z.string().optional(), + /** Deprecated. Envoy key for authentication. */ + envoyKey: z.string().optional(), /** Disable the welcome message. */ noWelcome: z.boolean().optional().default(false), diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts b/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts index fbe0139c5c..f776db15ef 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/handler.ts @@ -57,8 +57,8 @@ export function createInlineClient>( ): InlineOutput { // HACK: Cloudflare does not support using `crypto.randomUUID()` before start, so we pass a default value // - // Runner key is not used on Cloudflare - inputConfig = { ...inputConfig, runnerKey: "" }; + // Envoy key is not used on Cloudflare + inputConfig = { ...inputConfig, envoyKey: "" }; // Parse config const config = ConfigSchema.parse(inputConfig); diff --git a/rivetkit-typescript/packages/cloudflare-workers/src/manager-driver.ts b/rivetkit-typescript/packages/cloudflare-workers/src/manager-driver.ts index 30ccdb34d8..ecde6a8627 100644 --- a/rivetkit-typescript/packages/cloudflare-workers/src/manager-driver.ts +++ b/rivetkit-typescript/packages/cloudflare-workers/src/manager-driver.ts @@ -446,4 +446,61 @@ export class CloudflareActorsManagerDriver implements ManagerDriver { const value = await stub.managerKvGet(key); return value !== null ? new TextDecoder().decode(value) : null; } + + async kvBatchGet( + actorId: string, + keys: Uint8Array[], + ): Promise<(Uint8Array | null)[]> { + const env = getCloudflareAmbientEnv(); + + const [doId] = parseActorId(actorId); + + const id = env.ACTOR_DO.idFromString(doId); + const stub = env.ACTOR_DO.get(id); + + return await stub.managerKvBatchGet(keys); + } + + async kvBatchPut( + actorId: string, + entries: [Uint8Array, Uint8Array][], + ): Promise { + const env = getCloudflareAmbientEnv(); + + const [doId] = parseActorId(actorId); + + const id = env.ACTOR_DO.idFromString(doId); + const stub = env.ACTOR_DO.get(id); + + await stub.managerKvBatchPut(entries); + } + + async kvBatchDelete( + actorId: string, + keys: Uint8Array[], + ): Promise { + const env = getCloudflareAmbientEnv(); + + const [doId] = parseActorId(actorId); + + const id = env.ACTOR_DO.idFromString(doId); + const stub = env.ACTOR_DO.get(id); + + await stub.managerKvBatchDelete(keys); + } + + async kvDeleteRange( + actorId: string, + start: Uint8Array, + end: Uint8Array, + ): Promise { + const env = getCloudflareAmbientEnv(); + + const [doId] = parseActorId(actorId); + + const id = env.ACTOR_DO.idFromString(doId); + const stub = env.ACTOR_DO.get(id); + + await stub.managerKvDeleteRange(start, end); + } } diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db-stress.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db-stress.ts new file mode 100644 index 0000000000..9239c5b5ac --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/db-stress.ts @@ -0,0 +1,104 @@ +import { actor } from "rivetkit"; +import { db } from "rivetkit/db"; + +export const dbStressActor = actor({ + state: {}, + db: db({ + onMigrate: async (db) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS stress_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + value TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `); + }, + }), + actions: { + // Insert many rows in a single action. Used to create a long-running + // DB operation that can race with destroy/disconnect. + insertBatch: async (c, count: number) => { + const now = Date.now(); + const values: string[] = []; + for (let i = 0; i < count; i++) { + values.push(`('row-${i}', ${now})`); + } + await c.db.execute( + `INSERT INTO stress_data (value, created_at) VALUES ${values.join(", ")}`, + ); + return { count }; + }, + + getCount: async (c) => { + const results = await c.db.execute<{ count: number }>( + `SELECT COUNT(*) as count FROM stress_data`, + ); + return results[0].count; + }, + + // Measure event loop health during a DB operation. + // Runs a Promise.resolve() microtask check interleaved with DB + // inserts to detect if the event loop is being blocked between + // awaits. Reports the wall-clock duration so the test can verify + // the inserts complete in a reasonable time (not blocked by + // synchronous lifecycle operations). + measureEventLoopHealth: async (c, insertCount: number) => { + const startMs = Date.now(); + + // Do DB work that should NOT block the event loop. + // Insert rows one at a time to create many async round-trips. + for (let i = 0; i < insertCount; i++) { + await c.db.execute( + `INSERT INTO stress_data (value, created_at) VALUES ('drift-${i}', ${Date.now()})`, + ); + } + + const elapsedMs = Date.now() - startMs; + + return { + elapsedMs, + insertCount, + }; + }, + + // Write data to multiple rows that can be verified after a + // forced disconnect and reconnect. + writeAndVerify: async (c, count: number) => { + const now = Date.now(); + for (let i = 0; i < count; i++) { + await c.db.execute( + `INSERT INTO stress_data (value, created_at) VALUES ('verify-${i}', ${now})`, + ); + } + + const results = await c.db.execute<{ count: number }>( + `SELECT COUNT(*) as count FROM stress_data WHERE value LIKE 'verify-%'`, + ); + return results[0].count; + }, + + integrityCheck: async (c) => { + const rows = await c.db.execute>( + "PRAGMA integrity_check", + ); + const value = Object.values(rows[0] ?? {})[0]; + return String(value ?? ""); + }, + + triggerSleep: (c) => { + c.sleep(); + }, + + reset: async (c) => { + await c.db.execute(`DELETE FROM stress_data`); + }, + + destroy: (c) => { + c.destroy(); + }, + }, + options: { + actionTimeout: 120_000, + sleepTimeout: 100, + }, +}); 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 12a5e74d49..0c8c00007a 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts @@ -3,7 +3,6 @@ import { accessControlActor, accessControlNoQueuesActor, } from "./access-control"; -import { agentOsTestActor } from "./agent-os"; import { inputActor } from "./action-inputs"; import { @@ -71,6 +70,7 @@ import { } from "./run"; import { dockerSandboxActor } from "./sandbox"; import { scheduled } from "./scheduled"; +import { dbStressActor } from "./db-stress"; import { scheduledDb } from "./scheduled-db"; import { sleep, @@ -140,6 +140,18 @@ import { workflowTryActor, } from "./workflow"; +let agentOsTestActor: + | (Awaited["agentOsTestActor"]) + | undefined; + +try { + ({ agentOsTestActor } = await import("./agent-os")); +} catch (error) { + if (!(error instanceof Error) || !error.message.includes("agent-os")) { + throw error; + } +} + // Consolidated setup with all actors export const registry = setup({ use: { @@ -151,6 +163,8 @@ export const registry = setup({ counterWithLifecycle, // From scheduled.ts scheduled, + // From db-stress.ts + dbStressActor, // From scheduled-db.ts scheduledDb, // From sandbox.ts @@ -302,7 +316,11 @@ export const registry = setup({ dbPragmaMigrationActor, // From state-zod-coercion.ts stateZodCoercionActor, - // From agent-os.ts - agentOsTestActor, + ...(agentOsTestActor + ? { + // From agent-os.ts + agentOsTestActor, + } + : {}), }, }); diff --git a/rivetkit-typescript/packages/rivetkit/package.json b/rivetkit-typescript/packages/rivetkit/package.json index 8a89adc2be..a05f021214 100644 --- a/rivetkit-typescript/packages/rivetkit/package.json +++ b/rivetkit-typescript/packages/rivetkit/package.json @@ -336,6 +336,7 @@ "@hono/zod-openapi": "^1.1.5", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-envoy-client": "workspace:*", + "@rivetkit/engine-kv-channel-protocol": "workspace:*", "@rivetkit/engine-runner": "workspace:*", "@rivetkit/fast-json-patch": "^3.1.2", "@rivetkit/on-change": "^6.0.2-rc.1", diff --git a/rivetkit-typescript/packages/rivetkit/scripts/manager-openapi-gen.ts b/rivetkit-typescript/packages/rivetkit/scripts/manager-openapi-gen.ts index bc8f69f6ed..3724f94025 100644 --- a/rivetkit-typescript/packages/rivetkit/scripts/manager-openapi-gen.ts +++ b/rivetkit-typescript/packages/rivetkit/scripts/manager-openapi-gen.ts @@ -33,6 +33,10 @@ async function main() { setGetUpgradeWebSocket: unimplemented, buildGatewayUrl: unimplemented, kvGet: unimplemented, + kvBatchGet: unimplemented, + kvBatchPut: unimplemented, + kvBatchDelete: unimplemented, + kvDeleteRange: unimplemented, }; // const client = createClientWithDriver( diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts index 6092b11347..e75fe0f1b1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts @@ -261,6 +261,12 @@ export const ActorConfigSchema = z zFunction<(request: Request) => boolean>(), ]) .default(false), + /** Override RivetKit's SQLite preload budget for this actor. Set to 0 to disable SQLite preloading. */ + preloadMaxSqliteBytes: z.number().nonnegative().optional(), + /** Override RivetKit's workflow preload budget for this actor. Set to 0 to disable workflow preloading. */ + preloadMaxWorkflowBytes: z.number().nonnegative().optional(), + /** Override RivetKit's connections preload budget for this actor. Set to 0 to disable connections preloading. */ + preloadMaxConnectionsBytes: z.number().nonnegative().optional(), }) .strict() .prefault(() => ({})), @@ -1128,6 +1134,24 @@ export const DocActorOptionsSchema = z .describe( "Whether WebSockets using onWebSocket can be hibernated. WebSockets using actions/events are hibernatable by default. Default: false", ), + preloadMaxSqliteBytes: z + .number() + .optional() + .describe( + "Override RivetKit's SQLite preload budget for this actor. Set to 0 to disable SQLite preloading.", + ), + preloadMaxWorkflowBytes: z + .number() + .optional() + .describe( + "Override RivetKit's workflow preload budget for this actor. Set to 0 to disable workflow preloading.", + ), + preloadMaxConnectionsBytes: z + .number() + .optional() + .describe( + "Override RivetKit's connections preload budget for this actor. Set to 0 to disable connections preloading.", + ), }) .describe("Actor options for timeouts and behavior configuration."); diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts b/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts index 0307aa783d..2951108c32 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts @@ -4,7 +4,11 @@ import type { ManagerDriver } from "@/manager/driver"; import { type AnyConn } from "./conn/mod"; import type { AnyActorInstance } from "./instance/mod"; import type { RegistryConfig } from "@/registry/config"; -import type { RawDatabaseClient, DrizzleDatabaseClient } from "@/db/config"; +import type { + RawDatabaseClient, + DrizzleDatabaseClient, + NativeSqliteConfig, +} from "@/db/config"; import type { ISqliteVfs } from "@rivetkit/sqlite-vfs"; export type ActorDriverBuilder = ( @@ -101,6 +105,13 @@ export interface ActorDriver { */ createSqliteVfs?(actorId: string): ISqliteVfs | Promise; + /** + * Returns native SQLite channel configuration for this actor. + */ + getNativeSqliteConfig?( + actorId: string, + ): NativeSqliteConfig | undefined; + /** * Requests the actor to go to sleep. * @@ -115,6 +126,15 @@ export interface ActorDriver { */ startDestroy(actorId: string): void; + /** + * Test-only helper that simulates an abrupt actor crash. + * + * Unlike startSleep/startDestroy, this skips actor lifecycle hooks and the + * final persist flush. Drivers may still release local resources so the + * current test process can continue running. + */ + hardCrashActor?(actorId: string): Promise; + /** * Shuts down the actor runner. */ diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index 2b025e6158..9194699a39 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -773,6 +773,34 @@ export class ActorInstance< } } + async debugForceCrash() { + if (this.#shutdownComplete) { + return; + } + if (this.#stopCalled) { + this.#rLog.warn({ msg: "already stopping actor during hard crash" }); + return; + } + this.#stopCalled = true; + + try { + if (this.#sleepTimeout) { + clearTimeout(this.#sleepTimeout); + this.#sleepTimeout = undefined; + } + + this.driver.cancelAlarm?.(this.#actorId); + this.stateManager.clearPendingSaveTimeout(); + + try { + this.#abortController.abort(); + } catch {} + } finally { + this.#shutdownComplete = true; + await this.#cleanupDatabase(); + } + } + // MARK: - Sleep startSleep() { if (this.#stopCalled || this.#destroyCalled) { @@ -1901,11 +1929,16 @@ export class ActorInstance< this.driver.kvBatchGet(this.#actorId, keys), batchDelete: (keys: Uint8Array[]) => this.driver.kvBatchDelete(this.#actorId, keys), + deleteRange: (start: Uint8Array, end: Uint8Array) => + this.driver.kvDeleteRange(this.#actorId, start, end), }, sqliteVfs: this.#sqliteVfs, metrics: this.#metrics, preloadedEntries: sqlitePreloadEntries, log: this.#rLog, + nativeSqliteConfig: this.driver.getNativeSqliteConfig?.( + this.#actorId, + ), }), ); this.#rLog.info({ msg: "database migration starting" }); diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/preload-map.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/preload-map.ts index 4162beea79..2e259c4418 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/preload-map.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/preload-map.ts @@ -1,6 +1,6 @@ /** - * Input types matching the runner protocol PreloadedKv structure. - * Defined locally to avoid a dependency on the runner-protocol package. + * Input types matching the actor start-command PreloadedKv structure. + * Defined locally to avoid a direct dependency on a protocol package here. */ export interface PreloadedKvInput { readonly entries: readonly { diff --git a/rivetkit-typescript/packages/rivetkit/src/db/config.ts b/rivetkit-typescript/packages/rivetkit/src/db/config.ts index f6a88af0d1..3ff3b32d0c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/config.ts @@ -3,6 +3,12 @@ import type { ActorMetrics } from "@/actor/metrics"; export type AnyDatabaseProvider = DatabaseProvider | undefined; +export interface NativeSqliteConfig { + endpoint: string; + token?: string; + namespace: string; +} + /** * Context provided to database providers for creating database clients */ @@ -33,6 +39,7 @@ export interface DatabaseProviderContext { batchPut: (entries: [Uint8Array, Uint8Array][]) => Promise; batchGet: (keys: Uint8Array[]) => Promise<(Uint8Array | null)[]>; batchDelete: (keys: Uint8Array[]) => Promise; + deleteRange: (start: Uint8Array, end: Uint8Array) => Promise; }; /** @@ -58,6 +65,12 @@ export interface DatabaseProviderContext { * duration and KV call count. */ log?: { debug(obj: Record): void }; + + /** + * Native SQLite channel configuration. When provided, the native addon + * connects to this explicit endpoint instead of reading process env. + */ + nativeSqliteConfig?: NativeSqliteConfig; } export type DatabaseProvider = { diff --git a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts index def08f4474..9fbe132299 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts @@ -1,4 +1,8 @@ import type { DatabaseProvider, RawAccess } from "./config"; +import { + nativeSqliteAvailable, + createNativeRawAccess, +} from "./native-sqlite"; import { AsyncMutex, createActorKvStore, @@ -8,6 +12,9 @@ import { export type { RawAccess } from "./config"; +// Log the native SQLite fallback warning at most once per process. +let nativeFallbackWarned = false; + interface DatabaseFactoryConfig { onMigrate?: (db: RawAccess) => Promise | void; } @@ -47,6 +54,24 @@ export function db({ } satisfies RawAccess; } + // Use native SQLite when the addon is available. The native path + // routes KV operations over a WebSocket KV channel, bypassing + // the WASM VFS entirely. + if (nativeSqliteAvailable()) { + return await createNativeRawAccess( + ctx.actorId, + ctx.nativeSqliteConfig, + ); + } + + // Native addon not available. Fall back to WASM SQLite. + if (!nativeFallbackWarned) { + nativeFallbackWarned = true; + console.warn( + "native SQLite not available, falling back to WebAssembly. run npm rebuild to install native bindings.", + ); + } + // Construct KV-backed client using actor driver's KV operations if (!ctx.sqliteVfs) { throw new Error( diff --git a/rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts b/rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts new file mode 100644 index 0000000000..b0da15ed54 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/db/native-sqlite.ts @@ -0,0 +1,399 @@ +/** + * Native SQLite integration via @rivetkit/sqlite-native. + * + * Attempts to load the native addon at runtime and provides a fallback-aware + * API for the database provider. The KV channel connection is initialized once + * per process and shared across all actors. + * + * The native VFS and WASM VFS are byte-compatible. See + * rivetkit-typescript/packages/sqlite-native/src/vfs.rs and + * rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts. + */ + +import { getRequireFn } from "@/utils/node"; +import { + getRivetEndpoint, + getRivetToken, + getRivetNamespace, +} from "@/utils/env-vars"; +import type { NativeSqliteConfig, RawAccess } from "./config"; +import { AsyncMutex } from "./shared"; + +// Type declarations for @rivetkit/sqlite-native. +// Declared inline to avoid a build-time dependency on the native addon, +// which may not be installed or compiled. + +/** Typed bind parameter matching the Rust BindParam napi struct. */ +interface NativeBindParam { + kind: "null" | "int" | "float" | "text" | "blob"; + intValue?: number; + floatValue?: number; + textValue?: string; + blobValue?: Buffer; +} + +interface NativeSqliteModule { + connect(config: { + url: string; + token?: string; + namespace: string; + }): NativeKvChannel; + openDatabase( + channel: NativeKvChannel, + actorId: string, + ): Promise; + execute( + db: NativeDatabase, + sql: string, + params?: NativeBindParam[], + ): Promise<{ changes: number }>; + query( + db: NativeDatabase, + sql: string, + params?: NativeBindParam[], + ): Promise<{ columns: string[]; rows: unknown[][] }>; + exec( + db: NativeDatabase, + sql: string, + ): Promise<{ columns: string[]; rows: unknown[][] }>; + closeDatabase(db: NativeDatabase): Promise; + disconnect(channel: NativeKvChannel): Promise; + getMetrics?(channel: NativeKvChannel): KvChannelMetricsSnapshot | undefined; +} + +/** Metrics snapshot for a single KV operation type. */ +export interface OpMetricsSnapshot { + count: number; + totalDurationUs: number; + minDurationUs: number; + maxDurationUs: number; + avgDurationUs: number; +} + +/** Aggregated KV channel metrics for all operation types. */ +export interface KvChannelMetricsSnapshot { + get: OpMetricsSnapshot; + put: OpMetricsSnapshot; + delete: OpMetricsSnapshot; + deleteRange: OpMetricsSnapshot; + actorOpen: OpMetricsSnapshot; + actorClose: OpMetricsSnapshot; +} + +// Opaque handles from the native addon. +type NativeKvChannel = object; +type NativeDatabase = object; + +// Cached detection result. +let nativeModule: NativeSqliteModule | null = null; +let detectionDone = false; + +// KV channels are pooled by endpoint/token/namespace so concurrent test +// runtimes do not tear down each other's connection state. +const kvChannels = new Map(); + +// Whether the process shutdown handler has been registered. +let shutdownRegistered = false; + +/** + * Reset the cached native SQLite detection state. + * For testing only. Allows tests to switch between native and WASM VFS + * backends within the same process. + * + * @param disable - If true, force detection to report native as unavailable. + * If false/undefined, reset so the next call re-detects. + * @internal + */ +export async function _resetNativeDetection(disable?: boolean): Promise { + if (nativeModule) { + const disconnectPromises = Array.from(kvChannels.values()).map( + async (channel) => { + try { + await nativeModule!.disconnect(channel); + } catch { + // Ignore cleanup errors + } + }, + ); + await Promise.all(disconnectPromises); + } + kvChannels.clear(); + + if (disable) { + detectionDone = true; + nativeModule = null; + } else { + detectionDone = false; + nativeModule = null; + } +} + +/** + * Attempts to load the @rivetkit/sqlite-native .node addon. + * Catches all failure modes: missing file, glibc mismatch, + * N-API version mismatch, corrupted binary. + */ +export function nativeSqliteAvailable(): boolean { + if (detectionDone) return nativeModule !== null; + detectionDone = true; + + try { + const requireFn = getRequireFn(); + nativeModule = requireFn( + /* webpackIgnore: true */ "@rivetkit/sqlite-native", + ) as NativeSqliteModule; + return true; + } catch { + nativeModule = null; + return false; + } +} + +/** + * Returns the loaded native module. Only valid after nativeSqliteAvailable() + * returns true. + */ +function getNativeModule(): NativeSqliteModule { + if (!nativeModule) { + throw new Error("native SQLite module not loaded"); + } + return nativeModule; +} + +/** + * Disconnect the singleton KV channel if it exists. Safe to call multiple times. + */ +export function disconnectKvChannel(): void { + if (nativeModule) { + for (const channel of kvChannels.values()) { + // Fire-and-forget the async disconnect. During process shutdown, + // we cannot reliably await Promises (beforeExit/signal handlers + // are synchronous). The WebSocket close frame is best-effort. + nativeModule.disconnect(channel).catch(() => { + // Ignore cleanup errors during shutdown. + }); + } + } + kvChannels.clear(); +} + +/** + * Register process shutdown handlers that clean up the singleton KV channel. + * Called once per process on first channel creation. Uses `beforeExit` for + * graceful exit and signal handlers for SIGTERM/SIGINT. + */ +function registerShutdownHandler(): void { + if (shutdownRegistered) return; + shutdownRegistered = true; + + const onShutdown = () => { + disconnectKvChannel(); + }; + + // beforeExit fires when the event loop drains. Signals fire on external + // termination. Both paths call disconnectKvChannel which is idempotent. + process.on("beforeExit", onShutdown); + process.on("SIGTERM", onShutdown); + process.on("SIGINT", onShutdown); +} + +/** + * Get or create the process-level KV channel connection. + * + * Derives the WebSocket URL from RIVET_ENDPOINT (defaults to + * http://127.0.0.1:6420 for local dev). Authenticates with RIVET_TOKEN. + * + * If the channel was previously disconnected (e.g., during shutdown or due + * to a permanent failure), a new channel is created automatically. + */ +function getKvChannelConfig(config?: NativeSqliteConfig) { + const endpoint = + config?.endpoint ?? getRivetEndpoint() ?? "http://127.0.0.1:6420"; + const token = config?.token ?? getRivetToken(); + const namespace = config?.namespace ?? getRivetNamespace() ?? "default"; + + // Convert HTTP(S) endpoint to WebSocket URL for the KV channel. + const wsUrl = endpoint + .replace(/^https:\/\//, "wss://") + .replace(/^http:\/\//, "ws://") + .replace(/\/$/, ""); + + return { + wsUrl, + token: token ?? undefined, + namespace, + key: `${wsUrl}\u0000${token ?? ""}\u0000${namespace}`, + }; +} + +function getOrCreateKvChannel(config?: NativeSqliteConfig): NativeKvChannel { + const mod = getNativeModule(); + const channelConfig = getKvChannelConfig(config); + const existing = kvChannels.get(channelConfig.key); + if (existing) return existing; + + const channel = mod.connect({ + url: channelConfig.wsUrl, + token: channelConfig.token, + namespace: channelConfig.namespace, + }); + kvChannels.set(channelConfig.key, channel); + + registerShutdownHandler(); + + return channel; +} + +/** + * Convert binding values to typed BindParam objects for the native addon. + * Uses Buffer for blobs instead of JSON arrays to avoid 20x serialization + * overhead. See docs-internal/engine/NATIVE_SQLITE_REVIEW_FIXES.md M7. + */ +function toNativeBindings(args: unknown[]): NativeBindParam[] { + return args.map((arg): NativeBindParam => { + if (arg === null || arg === undefined) { + return { kind: "null" }; + } + if (typeof arg === "bigint") { + return { kind: "int", intValue: Number(arg) }; + } + if (typeof arg === "number") { + if (Number.isInteger(arg)) { + return { kind: "int", intValue: arg }; + } + return { kind: "float", floatValue: arg }; + } + if (typeof arg === "string") { + return { kind: "text", textValue: arg }; + } + if (typeof arg === "boolean") { + return { kind: "int", intValue: arg ? 1 : 0 }; + } + if (arg instanceof Uint8Array) { + return { kind: "blob", blobValue: Buffer.from(arg) }; + } + throw new Error(`unsupported bind parameter type: ${typeof arg}`); + }); +} + +/** + * Get a snapshot of KV channel operation metrics. + * Returns undefined if the native module is not available or the channel is not connected. + */ +export function getKvChannelMetrics(): KvChannelMetricsSnapshot | undefined { + if (!nativeModule?.getMetrics) return undefined; + const channel = kvChannels.get(getKvChannelConfig().key); + if (!channel) return undefined; + return nativeModule.getMetrics(channel) as KvChannelMetricsSnapshot | undefined; +} + +/** + * Disconnect the KV channel for the current endpoint/token/namespace only. + * This is used by the local driver test harness so one test runtime does not + * shut down another concurrent runtime's native SQLite channel. + */ +export async function disconnectKvChannelForCurrentConfig( + config?: NativeSqliteConfig, +): Promise { + if (!nativeModule) { + return; + } + + const { key } = getKvChannelConfig(config); + const channel = kvChannels.get(key); + if (!channel) { + return; + } + + kvChannels.delete(key); + await nativeModule.disconnect(channel); +} + +/** + * Create a RawAccess database client backed by the native SQLite addon. + * The KV channel is shared per process; a new database is opened per actor. + */ +export async function createNativeRawAccess( + actorId: string, + config?: NativeSqliteConfig, +): Promise { + const mod = getNativeModule(); + const channel = getOrCreateKvChannel(config); + const nativeDb = await mod.openDatabase(channel, actorId); + let closed = false; + const mutex = new AsyncMutex(); + + const ensureOpen = () => { + if (closed) { + throw new Error("database is closed"); + } + }; + + return { + execute: async < + TRow extends Record = Record< + string, + unknown + >, + >( + query: string, + ...args: unknown[] + ): Promise => { + return await mutex.run(async () => { + ensureOpen(); + + if (args.length > 0) { + // The native addon validates binding types in Rust + // (bind_params). Convert bigint/Uint8Array to + // JSON-compatible representations. + const bindings = toNativeBindings(args); + const token = query + .trimStart() + .slice(0, 16) + .toUpperCase(); + const returnsRows = + token.startsWith("SELECT") || + token.startsWith("PRAGMA") || + token.startsWith("WITH"); + + if (returnsRows) { + const { rows, columns } = await mod.query( + nativeDb, + query, + bindings, + ); + return rows.map((row: unknown[]) => { + const rowObj: Record = {}; + for (let i = 0; i < columns.length; i++) { + rowObj[columns[i]] = row[i]; + } + return rowObj; + }) as TRow[]; + } + + await mod.execute(nativeDb, query, bindings); + return [] as TRow[]; + } + + // Multi-statement SQL (e.g., migrations) without parameters. + // Uses the native exec which loops sqlite3_prepare_v2 with + // tail pointer tracking. + const { rows, columns } = await mod.exec(nativeDb, query); + return rows.map((row: unknown[]) => { + const rowObj: Record = {}; + for (let i = 0; i < columns.length; i++) { + rowObj[columns[i]] = row[i]; + } + return rowObj; + }) as TRow[]; + }); + }, + close: async () => { + await mutex.run(async () => { + if (closed) return; + closed = true; + await mod.closeDatabase(nativeDb); + }); + }, + }; +} diff --git a/rivetkit-typescript/packages/rivetkit/src/db/shared.ts b/rivetkit-typescript/packages/rivetkit/src/db/shared.ts index 3e7b728683..6992b9ef37 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/shared.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/shared.ts @@ -200,6 +200,9 @@ export function createActorKvStore( poison: () => { poisoned = true; }, + deleteRange: async (start: Uint8Array, end: Uint8Array) => { + await kv.deleteRange(start, end); + }, }; } 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 c1f273b2df..913e7b30f7 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/mod.ts @@ -17,6 +17,7 @@ import { runActorConnTests } from "./tests/actor-conn"; import { runActorConnHibernationTests } from "./tests/actor-conn-hibernation"; import { runActorConnStateTests } from "./tests/actor-conn-state"; import { runActorDbTests } from "./tests/actor-db"; +import { runActorDbStressTests } from "./tests/actor-db-stress"; import { runConnErrorSerializationTests } from "./tests/conn-error-serialization"; import { runActorDestroyTests } from "./tests/actor-destroy"; import { runActorDriverTests } from "./tests/actor-driver"; @@ -33,6 +34,7 @@ import { runActorSandboxTests } from "./tests/actor-sandbox"; import { runActorStatelessTests } from "./tests/actor-stateless"; import { runActorVarsTests } from "./tests/actor-vars"; import { runActorWorkflowTests } from "./tests/actor-workflow"; +import { runCrossBackendVfsTests } from "./tests/cross-backend-vfs"; import { runManagerDriverTests } from "./tests/manager-driver"; import { runRawHttpTests } from "./tests/raw-http"; import { runRawHttpRequestPropertiesTests } from "./tests/raw-http-request-properties"; @@ -86,6 +88,8 @@ export interface DriverDeployOutput { endpoint: string; namespace: string; runnerName: string; + hardCrashActor?: (actorId: string) => Promise; + hardCrashPreservesData?: boolean; /** Cleans up the test. */ cleanup(): Promise; @@ -182,6 +186,22 @@ export function runDriverTests( } }); } + + // Cross-backend VFS compatibility runs once, independent of + // client type and encoding. Skips when native SQLite is unavailable. + runCrossBackendVfsTests({ + ...driverTestConfigPartial, + clientType: "http", + encoding: "bare", + }); + + // Stress tests for DB lifecycle races, event loop blocking, and + // KV channel resilience. Run once, not per-encoding. + runActorDbStressTests({ + ...driverTestConfigPartial, + clientType: "http", + encoding: "bare", + }); }); } @@ -200,6 +220,8 @@ export async function createTestRuntime( token: string; }; driver: DriverConfig; + hardCrashActor?: (actorId: string) => Promise; + hardCrashPreservesData?: boolean; cleanup?: () => Promise; }>, ): Promise { @@ -228,6 +250,8 @@ export async function createTestRuntime( driver, cleanup: driverCleanup, rivetEngine, + hardCrashActor, + hardCrashPreservesData, } = await driverFactory(registry); if (rivetEngine) { @@ -242,6 +266,8 @@ export async function createTestRuntime( endpoint: rivetEngine.endpoint, namespace: rivetEngine.namespace, runnerName: rivetEngine.runnerName, + hardCrashActor, + hardCrashPreservesData, cleanup, }; } else { @@ -294,11 +320,29 @@ export async function createTestRuntime( ); const port = address.port; const serverEndpoint = `http://127.0.0.1:${port}`; + managerDriver.setNativeSqliteConfig?.({ + endpoint: serverEndpoint, + namespace: "default", + }); logger().info({ msg: "test serer listening", port }); // Cleanup const cleanup = async () => { + // Disconnect only the current test runtime's native KV channel so + // concurrent local runtimes do not shut down each other's channel. + try { + const { disconnectKvChannelForCurrentConfig } = await import( + "@/db/native-sqlite" + ); + await disconnectKvChannelForCurrentConfig({ + endpoint: serverEndpoint, + namespace: "default", + }); + } catch { + // Native module may not be available. + } + // Stop server await new Promise((resolve) => server.close(() => resolve(undefined)), @@ -312,6 +356,8 @@ export async function createTestRuntime( endpoint: serverEndpoint, namespace: "default", runnerName: "default", + hardCrashActor: managerDriver.hardCrashActor?.bind(managerDriver), + hardCrashPreservesData: driver.name !== "memory", cleanup, }; } diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/test-inline-client-driver.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/test-inline-client-driver.ts index e70f11ea63..14bc92d800 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/test-inline-client-driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/test-inline-client-driver.ts @@ -246,7 +246,34 @@ export function createTestInlineClientDriver( getUpgradeWebSocket = getUpgradeWebSocketInner; }, kvGet: (_actorId: string, _key: Uint8Array) => { - throw new Error("kvGet not impelmented on inline client driver"); + throw new Error("kvGet not implemented on inline client driver"); + }, + kvBatchGet: (_actorId: string, _keys: Uint8Array[]) => { + throw new Error( + "kvBatchGet not implemented on inline client driver", + ); + }, + kvBatchPut: ( + _actorId: string, + _entries: [Uint8Array, Uint8Array][], + ) => { + throw new Error( + "kvBatchPut not implemented on inline client driver", + ); + }, + kvBatchDelete: (_actorId: string, _keys: Uint8Array[]) => { + throw new Error( + "kvBatchDelete not implemented on inline client driver", + ); + }, + kvDeleteRange: ( + _actorId: string, + _start: Uint8Array, + _end: Uint8Array, + ) => { + throw new Error( + "kvDeleteRange not implemented on inline client driver", + ); }, } satisfies ManagerDriver; return driver; diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-agent-os.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-agent-os.ts index 07e053f129..f77e2f8f2c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-agent-os.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-agent-os.ts @@ -1,9 +1,20 @@ +import { createRequire } from "node:module"; import { describe, expect, test } from "vitest"; import type { DriverTestConfig } from "../mod"; import { setupDriverTest } from "../utils"; +const require = createRequire(import.meta.url); +const hasAgentOsCore = (() => { + try { + require.resolve("@rivet-dev/agent-os-core"); + return true; + } catch { + return false; + } +})(); + export function runActorAgentOsTests(driverTestConfig: DriverTestConfig) { - describe.skipIf(driverTestConfig.skip?.agentOs)( + describe.skipIf(driverTestConfig.skip?.agentOs || !hasAgentOsCore)( "Actor agentOS Tests", () => { // --- Filesystem --- diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db-stress.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db-stress.ts new file mode 100644 index 0000000000..861035321c --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/actor-db-stress.ts @@ -0,0 +1,234 @@ +import { describe, expect, test } from "vitest"; +import { nativeSqliteAvailable } from "@/db/native-sqlite"; +import type { DriverTestConfig } from "../mod"; +import { setupDriverTest, waitFor } from "../utils"; + +const STRESS_TEST_TIMEOUT_MS = 60_000; + +/** + * Stress and resilience tests for the SQLite database subsystem. + * + * These tests target edge cases from the adversarial review: + * - C1: close_database racing with in-flight operations + * - H1: lifecycle operations blocking the Node.js event loop + * - Reconnect: WebSocket disconnect during active KV operations + * + * They run against the file-system driver with real timers and require + * the native SQLite addon for the KV channel tests. + */ +export function runActorDbStressTests(driverTestConfig: DriverTestConfig) { + const nativeAvailable = nativeSqliteAvailable(); + + describe("Actor Database Stress Tests", () => { + test( + "destroy during long-running DB operation completes without crash", + async (c) => { + const { client } = await setupDriverTest( + c, + driverTestConfig, + ); + + // Start multiple actors and kick off long DB operations, + // then destroy them mid-flight. The test passes if no + // actor crashes and no unhandled errors propagate. + const actors = Array.from({ length: 5 }, (_, i) => + client.dbStressActor.getOrCreate([ + `stress-destroy-${i}-${crypto.randomUUID()}`, + ]), + ); + + // Start long-running inserts on all actors. + const insertPromises = actors.map((actor) => + actor.insertBatch(500).catch((err: Error) => ({ + error: err.message, + })), + ); + + // Immediately destroy all actors while inserts are in flight. + const destroyPromises = actors.map((actor) => + actor.destroy().catch((err: Error) => ({ + error: err.message, + })), + ); + + // Both sets of operations should resolve without hanging. + // Inserts may succeed or fail with an error (actor destroyed), + // but must not crash the process. + const results = await Promise.allSettled([ + ...insertPromises, + ...destroyPromises, + ]); + + // Verify all promises settled (none hung). + expect(results).toHaveLength(10); + for (const result of results) { + expect(result.status).toBe("fulfilled"); + } + }, + STRESS_TEST_TIMEOUT_MS, + ); + + test( + "rapid create-insert-destroy cycles handle DB lifecycle correctly", + async (c) => { + const { client } = await setupDriverTest( + c, + driverTestConfig, + ); + + // Perform rapid cycles of create -> insert -> destroy. + // This exercises the close_database path racing with + // any pending DB operations from the insert. + for (let i = 0; i < 10; i++) { + const actor = client.dbStressActor.getOrCreate([ + `stress-cycle-${i}-${crypto.randomUUID()}`, + ]); + + // Insert some data. + await actor.insertBatch(10); + + // Verify data was written. + const count = await actor.getCount(); + expect(count).toBeGreaterThanOrEqual(10); + + // Destroy the actor (triggers close_database). + await actor.destroy(); + } + }, + STRESS_TEST_TIMEOUT_MS, + ); + + test( + "DB operations complete without excessive blocking", + async (c) => { + const { client } = await setupDriverTest( + c, + driverTestConfig, + ); + + const actor = client.dbStressActor.getOrCreate([ + `stress-health-${crypto.randomUUID()}`, + ]); + + // Measure wall-clock time for 100 sequential DB inserts. + // Each insert is an async round-trip through the VFS. + // If lifecycle operations (open_database, close_database) + // block the event loop, this will take much longer than + // expected because the action itself runs on that loop. + const health = await actor.measureEventLoopHealth(100); + + // 100 sequential inserts should complete in well under + // 30 seconds. A blocked event loop (e.g., 30s WebSocket + // timeout on open_database) would push this way over. + expect(health.elapsedMs).toBeLessThan(30_000); + expect(health.insertCount).toBe(100); + + // Verify the actor is still healthy after the test. + const integrity = await actor.integrityCheck(); + expect(integrity.toLowerCase()).toBe("ok"); + }, + STRESS_TEST_TIMEOUT_MS, + ); + + // This test requires native SQLite (KV channel WebSocket). + // When using WASM SQLite, there's no WebSocket to disconnect. + describe.skipIf(!nativeAvailable)( + "KV Channel Resilience", + () => { + test( + "recovers from forced WebSocket disconnect during DB writes", + async (c) => { + const { client, endpoint } = + await setupDriverTest(c, driverTestConfig); + + const actor = client.dbStressActor.getOrCreate([ + `stress-disconnect-${crypto.randomUUID()}`, + ]); + + // Write initial data to confirm the actor works. + await actor.insertBatch(10); + expect(await actor.getCount()).toBe(10); + + // Force-close all KV channel WebSocket connections. + // The native SQLite addon should reconnect automatically. + const res = await fetch( + `${endpoint}/.test/kv-channel/force-disconnect`, + { method: "POST" }, + ); + expect(res.ok).toBe(true); + const body = (await res.json()) as { + closed: number; + }; + expect(body.closed).toBeGreaterThanOrEqual(0); + + // Give the native addon time to detect the disconnect + // and reconnect. + await waitFor(driverTestConfig, 2000); + + // The actor should still work after reconnection. + // The native addon re-opens actors on the new connection. + await actor.insertBatch(10); + const finalCount = await actor.getCount(); + expect(finalCount).toBe(20); + + // Verify data integrity after the disruption. + const integrity = await actor.integrityCheck(); + expect(integrity.toLowerCase()).toBe("ok"); + }, + STRESS_TEST_TIMEOUT_MS, + ); + + test( + "handles disconnect during active write operation", + async (c) => { + const { client, endpoint } = + await setupDriverTest(c, driverTestConfig); + + const actor = client.dbStressActor.getOrCreate([ + `stress-active-disconnect-${crypto.randomUUID()}`, + ]); + + // Confirm the actor is healthy. + await actor.insertBatch(5); + + // Start a large write operation and disconnect + // mid-flight. The write may fail, but the actor + // should recover. + const writePromise = actor + .insertBatch(200) + .catch((err: Error) => ({ + error: err.message, + })); + + // Small delay to let the write start, then disconnect. + await new Promise((resolve) => + setTimeout(resolve, 50), + ); + + await fetch( + `${endpoint}/.test/kv-channel/force-disconnect`, + { method: "POST" }, + ); + + // Wait for the write to settle (success or failure). + await writePromise; + + // Wait for reconnection. + await waitFor(driverTestConfig, 2000); + + // Actor should recover. New operations should work. + await actor.insertBatch(5); + const count = await actor.getCount(); + // At least the initial 5 + final 5 should exist. + // The mid-disconnect 200 may or may not have committed. + expect(count).toBeGreaterThanOrEqual(10); + + const integrity = await actor.integrityCheck(); + expect(integrity.toLowerCase()).toBe("ok"); + }, + STRESS_TEST_TIMEOUT_MS, + ); + }, + ); + }); +} 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 index 9463bfb358..f495e528cd 100644 --- 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 @@ -10,6 +10,8 @@ const HIGH_VOLUME_COUNT = 1000; const SLEEP_WAIT_MS = 150; const LIFECYCLE_POLL_INTERVAL_MS = 25; const LIFECYCLE_POLL_ATTEMPTS = 40; +const REAL_TIMER_HARD_CRASH_POLL_INTERVAL_MS = 50; +const REAL_TIMER_HARD_CRASH_POLL_ATTEMPTS = 600; const REAL_TIMER_DB_TIMEOUT_MS = 180_000; const CHUNK_BOUNDARY_SIZES = [ CHUNK_SIZE - 1, @@ -163,6 +165,71 @@ export function runActorDbTests(driverTestConfig: DriverTestConfig) { dbTestTimeout, ); + test.skipIf(driverTestConfig.skip?.sleep)( + "preserves committed rows across a hard crash and restart", + async (c) => { + const { + client, + hardCrashActor, + hardCrashPreservesData, + } = await setupDriverTest(c, driverTestConfig); + if (!hardCrashPreservesData) { + return; + } + if (!hardCrashActor) { + throw new Error( + "hardCrashActor test helper is unavailable for this driver", + ); + } + + const actor = getDbActor(client, variant).getOrCreate([ + `db-${variant}-hard-crash-${crypto.randomUUID()}`, + ]); + + await actor.reset(); + await actor.insertValue("before-crash"); + expect(await actor.getCount()).toBe(1); + + const actorId = await actor.resolve(); + await hardCrashActor(actorId); + + const hardCrashPollAttempts = + driverTestConfig.useRealTimers + ? REAL_TIMER_HARD_CRASH_POLL_ATTEMPTS + : LIFECYCLE_POLL_ATTEMPTS; + const hardCrashPollIntervalMs = + driverTestConfig.useRealTimers + ? REAL_TIMER_HARD_CRASH_POLL_INTERVAL_MS + : LIFECYCLE_POLL_INTERVAL_MS; + + let countAfterCrash = 0; + for (let i = 0; i < hardCrashPollAttempts; i++) { + try { + countAfterCrash = await actor.getCount(); + } catch { + countAfterCrash = 0; + } + if (countAfterCrash === 1) { + break; + } + await waitFor( + driverTestConfig, + hardCrashPollIntervalMs, + ); + } + + expect(countAfterCrash).toBe(1); + const values = await actor.getValues(); + expect( + values.some((row) => row.value === "before-crash"), + ).toBe(true); + + await actor.insertValue("after-crash"); + expect(await actor.getCount()).toBe(2); + }, + lifecycleTestTimeout, + ); + test( "completes onDisconnect DB writes before sleeping", async (c) => { @@ -181,7 +248,25 @@ export function runActorDbTests(driverTestConfig: DriverTestConfig) { await waitFor(driverTestConfig, SLEEP_WAIT_MS + 250); await actor.configureDisconnectInsert(false, 0); - expect(await actor.getDisconnectInsertCount()).toBe(1); + // Poll for the disconnect insert to complete. + // Native SQLite routes writes through a WebSocket KV + // channel, which adds latency that can push the + // onDisconnect DB write past the fixed wait window + // under concurrent test load. + let count = 0; + for (let i = 0; i < LIFECYCLE_POLL_ATTEMPTS; i++) { + count = + await actor.getDisconnectInsertCount(); + if (count >= 1) { + break; + } + await waitFor( + driverTestConfig, + LIFECYCLE_POLL_INTERVAL_MS, + ); + } + + expect(count).toBe(1); }, dbTestTimeout, ); diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/cross-backend-vfs.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/cross-backend-vfs.ts new file mode 100644 index 0000000000..7f203d0abe --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/tests/cross-backend-vfs.ts @@ -0,0 +1,166 @@ +import { describe, expect, test } from "vitest"; +import { + nativeSqliteAvailable, + _resetNativeDetection, +} from "@/db/native-sqlite"; +import type { DriverTestConfig } from "../mod"; +import { setupDriverTest, waitFor } from "../utils"; + +const SLEEP_WAIT_MS = 500; +const CROSS_BACKEND_TIMEOUT_MS = 30_000; + +/** + * Cross-backend VFS compatibility tests. + * + * Verifies that data written by the WASM VFS can be read by the native VFS + * and vice versa. Both VFS implementations store data in the same KV format + * (chunk keys, chunk data, metadata encoding). These tests catch encoding + * mismatches like the metadata version prefix difference fixed in US-024. + * + * Skipped when the native SQLite addon is not available. + */ +export function runCrossBackendVfsTests(driverTestConfig: DriverTestConfig) { + const nativeAvailable = nativeSqliteAvailable(); + + describe.skipIf(!nativeAvailable)( + "Cross-Backend VFS Compatibility Tests", + () => { + test( + "WASM-to-native: data written with WASM VFS is readable with native VFS", + async (c) => { + // Restore native detection on cleanup + c.onTestFinished(async () => { + await _resetNativeDetection(); + }); + + // Phase 1: Force WASM VFS + await _resetNativeDetection(true); + + const { client } = await setupDriverTest( + c, + driverTestConfig, + ); + const actorId = `cross-w2n-${crypto.randomUUID()}`; + const actor = client.dbActorRaw.getOrCreate([actorId]); + + // Write structured data with various sizes to exercise + // chunk boundaries (CHUNK_SIZE = 4096). + await actor.insertValue("wasm-alpha"); + await actor.insertValue("wasm-beta"); + await actor.insertMany(10); + + // Large payload spanning multiple chunks + const { id: largeId } = + await actor.insertPayloadOfSize(8192); + + const wasmCount = await actor.getCount(); + expect(wasmCount).toBe(13); + + const wasmValues = await actor.getValues(); + const wasmLargePayloadSize = + await actor.getPayloadSize(largeId); + expect(wasmLargePayloadSize).toBe(8192); + + // Sleep the actor to flush all data to KV + await actor.triggerSleep(); + await waitFor(driverTestConfig, SLEEP_WAIT_MS); + + // Phase 2: Restore native VFS detection + await _resetNativeDetection(); + + // Recreate the actor. The db() provider now uses native + // SQLite, reading data written by the WASM VFS. + const actor2 = client.dbActorRaw.getOrCreate([actorId]); + + const nativeCount = await actor2.getCount(); + expect(nativeCount).toBe(13); + + const nativeValues = await actor2.getValues(); + expect(nativeValues).toHaveLength(wasmValues.length); + for (let i = 0; i < wasmValues.length; i++) { + expect(nativeValues[i].value).toBe( + wasmValues[i].value, + ); + } + + const nativeLargePayloadSize = + await actor2.getPayloadSize(largeId); + expect(nativeLargePayloadSize).toBe(8192); + + // Verify integrity + const integrity = await actor2.integrityCheck(); + expect(integrity).toBe("ok"); + }, + CROSS_BACKEND_TIMEOUT_MS, + ); + + test( + "native-to-WASM: data written with native VFS is readable with WASM VFS", + async (c) => { + // Restore native detection on cleanup + c.onTestFinished(async () => { + await _resetNativeDetection(); + }); + + // Phase 1: Use native VFS (default when addon is available) + await _resetNativeDetection(); + + const { client } = await setupDriverTest( + c, + driverTestConfig, + ); + const actorId = `cross-n2w-${crypto.randomUUID()}`; + const actor = client.dbActorRaw.getOrCreate([actorId]); + + // Write structured data with various sizes + await actor.insertValue("native-alpha"); + await actor.insertValue("native-beta"); + await actor.insertMany(10); + + // Large payload spanning multiple chunks + const { id: largeId } = + await actor.insertPayloadOfSize(8192); + + const nativeCount = await actor.getCount(); + expect(nativeCount).toBe(13); + + const nativeValues = await actor.getValues(); + const nativeLargePayloadSize = + await actor.getPayloadSize(largeId); + expect(nativeLargePayloadSize).toBe(8192); + + // Sleep the actor to flush all data to KV + await actor.triggerSleep(); + await waitFor(driverTestConfig, SLEEP_WAIT_MS); + + // Phase 2: Force WASM VFS + await _resetNativeDetection(true); + + // Recreate the actor. The db() provider now uses WASM + // SQLite, reading data written by the native VFS. + const actor2 = client.dbActorRaw.getOrCreate([actorId]); + + const wasmCount = await actor2.getCount(); + expect(wasmCount).toBe(13); + + const wasmValues = await actor2.getValues(); + expect(wasmValues).toHaveLength(nativeValues.length); + for (let i = 0; i < nativeValues.length; i++) { + expect(wasmValues[i].value).toBe( + nativeValues[i].value, + ); + } + + const wasmLargePayloadSize = + await actor2.getPayloadSize(largeId); + expect(wasmLargePayloadSize).toBe(8192); + + // Verify integrity + const integrity = await actor2.integrityCheck(); + expect(integrity).toBe("ok"); + }, + CROSS_BACKEND_TIMEOUT_MS, + ); + }, + ); +} diff --git a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/utils.ts b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/utils.ts index 929ce83089..c38ac3767f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/driver-test-suite/utils.ts @@ -17,6 +17,8 @@ export async function setupDriverTest( ): Promise<{ client: Client; endpoint: string; + hardCrashActor?: (actorId: string) => Promise; + hardCrashPreservesData: boolean; }> { if (!driverTestConfig.useRealTimers) { vi.useFakeTimers(); @@ -24,7 +26,14 @@ export async function setupDriverTest( } // Build drivers - const { endpoint, namespace, runnerName, cleanup } = + const { + endpoint, + namespace, + runnerName, + hardCrashActor, + hardCrashPreservesData, + cleanup, + } = await driverTestConfig.start(); let client: Client; @@ -33,7 +42,7 @@ export async function setupDriverTest( client = createClient({ endpoint, namespace, - runnerName, + poolName: runnerName, encoding: driverTestConfig.encoding, // Disable metadata lookup to prevent redirect to the wrong port. // Each test starts a new server on a dynamic port, but the @@ -64,6 +73,8 @@ export async function setupDriverTest( return { client, endpoint, + hardCrashActor, + hardCrashPreservesData: hardCrashPreservesData ?? false, }; } diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts index a8a7d340d8..ba999184b5 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts @@ -11,9 +11,6 @@ import { type AnyConn, CONN_STATE_MANAGER_SYMBOL } from "@/actor/conn/mod"; import { lookupInRegistry } from "@/actor/definition"; import { KEYS, - queueMetadataKey, - sqliteStoragePrefix, - workflowStoragePrefix, } from "@/actor/instance/keys"; import { type PreloadMap, @@ -109,10 +106,10 @@ export class EngineActorDriver implements ActorDriver { ); #isEnvoyStopped: boolean = false; - // HACK: Track actor stop intent locally since the runner protocol doesn't - // pass the stop reason to onActorStop. This will be fixed when the runner + // HACK: Track actor stop intent locally since the envoy protocol doesn't + // pass the stop reason to onActorStop. This will be fixed when the envoy // protocol is updated to send the intent directly (see RVT-5284) - #actorStopIntent: Map = new Map(); + #actorStopIntent: Map = new Map(); // Map of conn IDs to message index waiting to be persisted before sending // an ack @@ -148,7 +145,6 @@ export class EngineActorDriver implements ActorDriver { // HACK: Override inspector token (which are likely to be // removed later on) with token from x-rivet-token header - const token = config.token; // TODO: // if (token && runConfig.inspector && runConfig.inspector.enabled) { // runConfig.inspector.token = () => token; @@ -165,7 +161,7 @@ export class EngineActorDriver implements ActorDriver { const envoyConfig: EnvoyConfig = { version: config.envoy.version, endpoint: getEndpoint(config), - token, + token: config.token, namespace: config.namespace, poolName: config.envoy.poolName, metadata: { @@ -206,6 +202,33 @@ export class EngineActorDriver implements ActorDriver { }); } + async #discardCrashedActorState(actorId: string) { + const handler = this.#actors.get(actorId); + if (!handler) { + return; + } + + if (handler.alarmTimeout) { + handler.alarmTimeout.abort(); + handler.alarmTimeout = undefined; + } + + if (handler.actor) { + try { + await handler.actor.debugForceCrash(); + } catch (err) { + logger().debug({ + msg: "actor crash cleanup errored", + actorId, + err: stringifyError(err), + }); + } + } + + this.#actors.delete(actorId); + this.#actorStopIntent.delete(actorId); + } + getExtraActorLogParams(): Record { return { envoyKey: this.#envoy.getEnvoyKey() ?? "-" }; } @@ -270,6 +293,14 @@ export class EngineActorDriver implements ActorDriver { // No database overrides - will use KV-backed implementation from rivetkit/db + getNativeSqliteConfig() { + return { + endpoint: getEndpoint(this.#config), + token: this.#config.token, + namespace: this.#config.namespace, + }; + } + // MARK: - Batch KV operations async kvBatchPut( actorId: string, @@ -302,12 +333,12 @@ export class EngineActorDriver implements ActorDriver { actorId, new Uint8Array(), ); - const keys = entries.map(([key]) => key); + const keys = entries.map(([key]: [Uint8Array, ...unknown[]]) => key); logger().info({ msg: "kvList called", actorId, keysCount: keys.length, - keys: keys.map((k) => new TextDecoder().decode(k)), + keys: keys.map((k: Uint8Array) => new TextDecoder().decode(k)), }); return keys; } @@ -330,7 +361,7 @@ export class EngineActorDriver implements ActorDriver { actorId, prefixStr: new TextDecoder().decode(prefix), entriesCount: result.length, - keys: result.map(([key]) => new TextDecoder().decode(key)), + keys: result.map(([key]: [Uint8Array, ...unknown[]]) => new TextDecoder().decode(key)), }); return result; } @@ -377,33 +408,51 @@ export class EngineActorDriver implements ActorDriver { this.#envoy.destroyActor(actorId); } + async hardCrashActor(actorId: string): Promise { + const handler = this.#actors.get(actorId); + if (!handler) { + return; + } + + if (handler.actorStartPromise) { + await handler.actorStartPromise.promise.catch(() => undefined); + } + + logger().info({ + msg: "simulating hard crash for actor", + actorId, + }); + + await this.#discardCrashedActorState(actorId); + } + async shutdown(immediate: boolean): Promise { logger().info({ msg: "stopping engine actor driver", immediate }); - // TODO: We need to update the runner to have a draining state so: + // TODO: We need to update the envoy to have a draining state so: // 1. Send ToServerDraining - // - This causes Pegboard to stop allocating actors to this runner - // 2. Pegboard sends ToClientStopActor for all actors on this runner which handles the graceful migration of each actor independently + // - This causes Pegboard to stop allocating actors to this envoy + // 2. Pegboard sends ToClientStopActor for all actors on this envoy which handles the graceful migration of each actor independently // 3. Send ToServerStopping once all actors have successfully stopped // // What's happening right now is: // 1. All actors enter stopped state // 2. Actors still respond to requests because only RivetKit knows it's // stopping, this causes all requests to issue errors that the actor is - // stopping. (This will NOT return a 503 bc the runner has no idea the + // stopping. (This will NOT return a 503 bc the envoy has no idea the // actors are stopping.) - // 3. Once the last actor stops, then the runner finally stops + actors + // 3. Once the last actor stops, then the envoy finally stops + actors // reschedule // // This means that: - // - All actors on this runner are bricked until the slowest onStop finishes + // - All actors on this envoy are bricked until the slowest onStop finishes // - Guard will not gracefully handle requests bc it's not receiving a 503 - // - Actors can still be scheduled to this runner while the other + // - Actors can still be scheduled to this envoy while the other // actors are stopping, meaning that those actors will NOT get onStop - // and will potentiall corrupt their state + // and will potentially corrupt their state // // HACK: Stop all actors to allow state to be saved - // NOTE: onStop is only supposed to be called by the runner, we're + // NOTE: onStop is only supposed to be called by the envoy, we're // abusing it here logger().debug({ msg: "stopping all actors before shutdown", @@ -508,57 +557,50 @@ export class EngineActorDriver implements ActorDriver { }); } - /** - * Fetch remaining startup KV data in parallel and build a PreloadMap. - * PERSIST_DATA is already known (passed in), so we only fetch the - * remaining exact keys and prefix scans. - */ - async #preloadStartupKv( - actorId: string, - persistData: Uint8Array, - ): Promise<{ preloadMap: PreloadMap; entries: number }> { - const remainingExactKeys = [KEYS.INSPECTOR_TOKEN, queueMetadataKey()]; - - const prefixScans = [ - KEYS.CONN_PREFIX, - sqliteStoragePrefix(), - workflowStoragePrefix(), - ]; - - const [exactResults, ...prefixResults] = await Promise.all([ - this.#envoy.kvGet(actorId, remainingExactKeys), - ...prefixScans.map((prefix) => - this.#envoy.kvListPrefix(actorId, prefix), - ), - ]); + #buildStartupPreloadMap( + preloadedKv: protocol.PreloadedKv | null, + persistDataOverride?: Uint8Array, + ): { preloadMap: PreloadMap | undefined; entries: number } { + if (preloadedKv == null) { + return { preloadMap: undefined, entries: 0 }; + } - const allExactKeys = [KEYS.PERSIST_DATA, ...remainingExactKeys]; - const entries: [Uint8Array, Uint8Array][] = []; + const entries: [Uint8Array, Uint8Array][] = preloadedKv.entries.map( + (entry) => [new Uint8Array(entry.key), new Uint8Array(entry.value)], + ); - entries.push([KEYS.PERSIST_DATA, persistData]); - for (let i = 0; i < remainingExactKeys.length; i++) { - const value = exactResults[i]; - if (value !== null) { - entries.push([remainingExactKeys[i], value]); + if (persistDataOverride) { + let replaced = false; + for (const entry of entries) { + if (compareBytes(entry[0], KEYS.PERSIST_DATA) === 0) { + entry[1] = persistDataOverride; + replaced = true; + break; + } } - } - for (const prefixEntries of prefixResults) { - for (const entry of prefixEntries) { - entries.push(entry); + + if (!replaced) { + entries.push([KEYS.PERSIST_DATA, persistDataOverride]); } } entries.sort((a, b) => compareBytes(a[0], b[0])); - const requestedGetKeys = allExactKeys.slice().sort(compareBytes); - const requestedPrefixes = prefixScans.slice().sort(compareBytes); - - const preloadMap = createPreloadMap( - entries, - requestedGetKeys, - requestedPrefixes, - ); - return { preloadMap, entries: entries.length }; + const requestedGetKeys = preloadedKv.requestedGetKeys + .map((key) => new Uint8Array(key)) + .sort(compareBytes); + const requestedPrefixes = preloadedKv.requestedPrefixes + .map((prefix) => new Uint8Array(prefix)) + .sort(compareBytes); + + return { + preloadMap: createPreloadMap( + entries, + requestedGetKeys, + requestedPrefixes, + ), + entries: entries.length, + }; } async #envoyOnActorStart( @@ -566,6 +608,7 @@ export class EngineActorDriver implements ActorDriver { actorId: string, generation: number, actorConfig: protocol.ActorConfig, + preloadedKv: protocol.PreloadedKv | null, ): Promise { logger().debug({ msg: "engine actor starting", @@ -604,48 +647,61 @@ export class EngineActorDriver implements ActorDriver { const key = deserializeActorKey(actorConfig.key); try { - // Check if this actor already has persisted state. - let checkStart = performance.now(); - const [persistDataBuffer] = await this.#envoy.kvGet(actorId, [ - KEYS.PERSIST_DATA, - ]); - const checkPersistDataMs = performance.now() - checkStart; - - // For new actors there is no existing KV data to preload. let preloadMap: PreloadMap | undefined; + let persistDataBuffer: Uint8Array | null | undefined; + let checkPersistDataMs = 0; let initNewActorMs = 0; let preloadKvMs = 0; let preloadKvEntries = 0; - // 1 round-trip for the persist data check - let driverKvRoundTrips = 1; + let driverKvRoundTrips = 0; + + if (preloadedKv) { + const preloadStart = performance.now(); + const preloaded = this.#buildStartupPreloadMap(preloadedKv); + preloadMap = preloaded.preloadMap; + preloadKvEntries = preloaded.entries; + preloadKvMs = performance.now() - preloadStart; + persistDataBuffer = preloadMap?.get(KEYS.PERSIST_DATA)?.value; + logger().debug({ + msg: "received startup kv preload from start command", + actorId, + entries: preloadKvEntries, + durationMs: preloadKvMs, + }); + } + + if (persistDataBuffer === undefined) { + const checkStart = performance.now(); + const [persistData] = await this.#envoy.kvGet(actorId, [ + KEYS.PERSIST_DATA, + ]); + persistDataBuffer = persistData; + checkPersistDataMs = performance.now() - checkStart; + driverKvRoundTrips++; + } if (persistDataBuffer === null) { const initStart = performance.now(); const initialKvState = getInitialActorKvState(input); + const persistData = initialKvState[0]?.[1]; await this.#envoy.kvPut(actorId, initialKvState); initNewActorMs = performance.now() - initStart; driverKvRoundTrips++; + if (preloadedKv && persistData) { + const preloadStart = performance.now(); + const preloaded = this.#buildStartupPreloadMap( + preloadedKv, + persistData, + ); + preloadMap = preloaded.preloadMap; + preloadKvEntries = preloaded.entries; + preloadKvMs += performance.now() - preloadStart; + } logger().debug({ msg: "initialized persist data for new actor", actorId, durationMs: initNewActorMs, }); - } else { - const preloadStart = performance.now(); - const result = await this.#preloadStartupKv( - actorId, - persistDataBuffer, - ); - preloadMap = result.preloadMap; - preloadKvEntries = result.entries; - preloadKvMs = performance.now() - preloadStart; - driverKvRoundTrips++; - logger().debug({ - msg: "preloaded startup kv for existing actor", - actorId, - entries: preloadKvEntries, - durationMs: preloadKvMs, - }); } // Create actor instance @@ -727,7 +783,7 @@ export class EngineActorDriver implements ActorDriver { }); try { - this.#envoy.stopActor(actorId, undefined, stringifyError(error)); + this.#envoy.stopActor(actorId, undefined); } catch (stopError) { logger().debug({ msg: "failed to stop actor after start failure", @@ -786,7 +842,11 @@ export class EngineActorDriver implements ActorDriver { if (handler.actor) { try { - await handler.actor.onStop(reason); + if (reason === "crash") { + await handler.actor.debugForceCrash(); + } else { + await handler.actor.onStop(reason); + } } catch (err) { logger().error({ msg: "error in onStop, proceeding with removing actor", @@ -795,6 +855,11 @@ export class EngineActorDriver implements ActorDriver { } } + if (handler.alarmTimeout) { + handler.alarmTimeout.abort(); + handler.alarmTimeout = undefined; + } + this.#actors.delete(actorId); logger().debug({ msg: "engine actor stopped", actorId, reason }); diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/config.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/config.ts index 59972a7a74..d7eef77f79 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/config.ts @@ -7,10 +7,10 @@ import { ClientConfigSchemaBase, transformClientConfig } from "@/client/config"; * We include the client config since this includes the common properties like endpoint, namespace, etc. */ export const EngineConfigSchemaBase = ClientConfigSchemaBase.extend({ - /** Deprecated. Unique key for this runner. Runners connecting a given key will replace any other runner connected with the same key. */ - runnerKey: z.string().optional(), + /** Deprecated. Unique key for this envoy. Envoys connecting with a given key will replace any other envoy connected with the same key. */ + envoyKey: z.string().optional(), - /** How many actors this runner can run. */ + /** How many actors this envoy can run. */ totalSlots: z.number().default(100_000), }); 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 17ba8d9497..600736b847 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts @@ -1,5 +1,5 @@ import type { AnyClient } from "@/client/client"; -import type { RawDatabaseClient } from "@/db/config"; +import type { NativeSqliteConfig, RawDatabaseClient } from "@/db/config"; import type { ISqliteVfs } from "@rivetkit/sqlite-vfs"; import { type ActorDriver, @@ -65,6 +65,10 @@ export class FileSystemActorDriver implements ActorDriver { return {}; } + getNativeSqliteConfig(_actorId: string): NativeSqliteConfig | undefined { + return this.#state.nativeSqliteConfig; + } + async kvBatchPut( actorId: string, entries: [Uint8Array, Uint8Array][], @@ -131,6 +135,10 @@ export class FileSystemActorDriver implements ActorDriver { await this.#sqlitePool.shutdown(); } + async hardCrashActor(actorId: string): Promise { + await this.#state.hardCrashActor(actorId); + } + async startDestroy(actorId: string): Promise { await this.#state.destroyActor(actorId); } 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 2b1377fc53..c977d8773f 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 @@ -4,6 +4,7 @@ import { ActorDuplicateKey, ActorError, ActorNotFound } from "@/actor/errors"; import type { AnyActorInstance } from "@/actor/instance/mod"; import type { ActorKey } from "@/actor/mod"; import type { AnyClient } from "@/client/client"; +import type { NativeSqliteConfig } from "@/db/config"; import { type ActorDriver, getInitialActorKvState } from "@/driver-helpers/mod"; import type { RegistryConfig } from "@/registry/config"; import type * as schema from "@/schemas/file-system-driver/mod"; @@ -123,6 +124,7 @@ export class FileSystemGlobalState { #actors = new Map(); #actorCountOnStartup: number = 0; + #nativeSqliteConfig?: NativeSqliteConfig; #runnerParams?: { config: RegistryConfig; @@ -142,6 +144,10 @@ export class FileSystemGlobalState { return this.#actorCountOnStartup; } + get nativeSqliteConfig() { + return this.#nativeSqliteConfig; + } + constructor(options: FileSystemDriverOptions = {}) { const { persist = true, customPath, useNativeSqlite = true } = options; if (!useNativeSqlite) { @@ -205,6 +211,10 @@ export class FileSystemGlobalState { } } + setNativeSqliteConfig(config: NativeSqliteConfig): void { + this.#nativeSqliteConfig = config; + } + getActorStatePath(actorId: string): string { return getNodePath().join(this.#stateDir, actorId); } @@ -798,6 +808,41 @@ export class FileSystemGlobalState { } } + async hardCrashActor(actorId: string): Promise { + const actor = this.#actors.get(actorId); + if (!actor) { + return; + } + + if (this.isActorStopping(actorId)) { + await this.#waitForActorStop(actorId); + return; + } + + if (actor.loadPromise) { + await actor.loadPromise.catch(() => undefined); + } + if (actor.startPromise?.promise) { + await actor.startPromise.promise.catch(() => undefined); + } + + try { + if (actor.alarmTimeout) { + actor.alarmTimeout.abort(); + actor.alarmTimeout = undefined; + } + + if (actor.actor) { + await actor.actor.debugForceCrash(); + } + } finally { + this.#closeActorKvDatabase(actorId); + actor.stopPromise?.resolve(); + actor.stopPromise = undefined; + this.#actors.delete(actorId); + } + } + /** * Save actor state to disk. */ @@ -1367,13 +1412,12 @@ export class FileSystemGlobalState { ): Promise { 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`); + if (!entry.state && this.isActorStopping(actorId)) { + return; } + // KV database is independent of actor state and may be written + // during actor creation (e.g. native SQLite via KV channel). const db = this.#getOrCreateActorKvDatabase(actorId); const totalSize = estimateKvSize(db); validateKvEntries(entries, totalSize); @@ -1388,15 +1432,8 @@ export class FileSystemGlobalState { actorId: string, keys: Uint8Array[], ): Promise<(Uint8Array | null)[]> { - const entry = await this.loadActor(actorId); + await this.loadActor(actorId); await this.#waitForPendingWrite(actorId); - if (!entry.state) { - if (this.isActorStopping(actorId)) { - throw new Error(`Actor ${actorId} is stopping`); - } else { - throw new Error(`Actor ${actorId} state not loaded`); - } - } validateKvKeys(keys); @@ -1422,11 +1459,8 @@ export class FileSystemGlobalState { async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise { 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`); + if (!entry.state && this.isActorStopping(actorId)) { + return; } if (keys.length === 0) { @@ -1462,11 +1496,8 @@ export class FileSystemGlobalState { ): Promise { 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`); + if (!entry.state && this.isActorStopping(actorId)) { + return; } validateKvKey(start, "start key"); @@ -1491,15 +1522,8 @@ export class FileSystemGlobalState { limit?: number; }, ): Promise<[Uint8Array, Uint8Array][]> { - const entry = await this.loadActor(actorId); + await this.loadActor(actorId); await this.#waitForPendingWrite(actorId); - if (!entry.state) { - if (this.isActorStopping(actorId)) { - throw new Error(`Actor ${actorId} is destroying`); - } else { - throw new Error(`Actor ${actorId} state not loaded`); - } - } validateKvKey(prefix, "prefix key"); const db = this.#getOrCreateActorKvDatabase(actorId); diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts index 20a2270e41..d0ed666b90 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts @@ -1,5 +1,19 @@ import type { SqliteRuntimeDatabase } from "./sqlite-runtime"; +export class KvStorageQuotaExceededError extends Error { + readonly remaining: number; + readonly payloadSize: number; + + constructor(remaining: number, payloadSize: number) { + super( + `not enough space left in storage (${remaining} bytes remaining, current payload is ${payloadSize} bytes)`, + ); + this.name = "KvStorageQuotaExceededError"; + this.remaining = remaining; + this.payloadSize = payloadSize; + } +} + // Keep these limits in sync with engine/packages/pegboard/src/actor_kv/mod.rs. const KV_MAX_KEY_SIZE = 2 * 1024; const KV_MAX_VALUE_SIZE = 128 * 1024; @@ -54,9 +68,7 @@ export function validateKvEntries( const storageRemaining = Math.max(0, KV_MAX_STORAGE_SIZE - totalSize); if (payloadSize > storageRemaining) { - throw new Error( - `not enough space left in storage (${storageRemaining} bytes remaining, current payload is ${payloadSize} bytes)`, - ); + throw new KvStorageQuotaExceededError(storageRemaining, payloadSize); } for (const [key, value] of entries) { diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/manager.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/manager.ts index 11f71a8eb5..e9b984bb9f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/manager.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/manager.ts @@ -6,6 +6,7 @@ import { routeWebSocket } from "@/actor/router-websocket-endpoints"; import { createClientWithDriver } from "@/client/client"; import { createInlineWebSocket } from "@/common/inline-websocket-adapter"; import { noopNext } from "@/common/utils"; +import type { NativeSqliteConfig } from "@/db/config"; import { resolveGatewayTarget, type ActorDriver, @@ -35,6 +36,7 @@ export class FileSystemManagerDriver implements ManagerDriver { #actorDriver: ActorDriver; #actorRouter: ActorRouter; + #kvChannelShutdown: (() => void) | null = null; constructor( config: RegistryConfig, @@ -187,6 +189,14 @@ export class FileSystemManagerDriver implements ManagerDriver { throw new Error("unreachable: unknown gateway target type"); } + async hardCrashActor(actorId: string): Promise { + await this.#actorDriver.hardCrashActor?.(actorId); + } + + setNativeSqliteConfig(config: NativeSqliteConfig): void { + this.#state.setNativeSqliteConfig(config); + } + async getForId({ actorId, }: GetForIdInput): Promise { @@ -281,6 +291,32 @@ export class FileSystemManagerDriver implements ManagerDriver { : null; } + async kvBatchGet( + actorId: string, + keys: Uint8Array[], + ): Promise<(Uint8Array | null)[]> { + return await this.#state.kvBatchGet(actorId, keys); + } + + async kvBatchPut( + actorId: string, + entries: [Uint8Array, Uint8Array][], + ): Promise { + await this.#state.kvBatchPut(actorId, entries); + } + + async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise { + await this.#state.kvBatchDelete(actorId, keys); + } + + async kvDeleteRange( + actorId: string, + start: Uint8Array, + end: Uint8Array, + ): Promise { + await this.#state.kvDeleteRange(actorId, start, end); + } + displayInformation(): ManagerDisplayInformation { return { properties: { @@ -303,6 +339,14 @@ export class FileSystemManagerDriver implements ManagerDriver { this.#getUpgradeWebSocket = getUpgradeWebSocket; } + setKvChannelShutdown(fn: () => void): void { + this.#kvChannelShutdown = fn; + } + + shutdown(): void { + this.#kvChannelShutdown?.(); + this.#kvChannelShutdown = null; + } } function actorStateToOutput(state: schema.ActorState): ActorOutput { diff --git a/rivetkit-typescript/packages/rivetkit/src/manager/driver.ts b/rivetkit-typescript/packages/rivetkit/src/manager/driver.ts index fc288a2636..44a599d6a7 100644 --- a/rivetkit-typescript/packages/rivetkit/src/manager/driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/manager/driver.ts @@ -1,5 +1,6 @@ import type { Hono, Context as HonoContext } from "hono"; import type { ActorKey, Encoding, UniversalWebSocket } from "@/actor/mod"; +import type { NativeSqliteConfig } from "@/db/config"; import type { RegistryConfig } from "@/registry/config"; import type { GetUpgradeWebSocket } from "@/utils"; import type { ActorQuery, CrashPolicy } from "./protocol/query"; @@ -55,8 +56,52 @@ export interface ManagerDriver { **/ setGetUpgradeWebSocket(getUpgradeWebSocket: GetUpgradeWebSocket): void; + /** + * Clean shutdown of manager resources (timers, lock tables, etc.). + * Called after all actors have stopped. + */ + shutdown?(): void; + + /** + * Inject the KV channel shutdown callback. Called by the manager + * router so the driver can invoke it during shutdown. + */ + setKvChannelShutdown?(fn: () => void): void; + + /** + * Test-only helper that simulates an abrupt actor crash. + */ + hardCrashActor?(actorId: string): Promise; + + /** + * Inject native SQLite connection settings for driver-created actors. + */ + setNativeSqliteConfig?(config: NativeSqliteConfig): void; + /** Read a key. Returns null if the key doesn't exist. */ kvGet(actorId: string, key: Uint8Array): Promise; + + /** Batch get KV entries. Returns null for keys that don't exist. */ + kvBatchGet( + actorId: string, + keys: Uint8Array[], + ): Promise<(Uint8Array | null)[]>; + + /** Batch put KV entries. */ + kvBatchPut( + actorId: string, + entries: [Uint8Array, Uint8Array][], + ): Promise; + + /** Batch delete KV entries. */ + kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise; + + /** Delete KV entries in the half-open range [start, end). */ + kvDeleteRange( + actorId: string, + start: Uint8Array, + end: Uint8Array, + ): Promise; } export interface ManagerDisplayInformation { diff --git a/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts b/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts index fecc1da0f8..dae9de22b4 100644 --- a/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts +++ b/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts @@ -148,6 +148,11 @@ export async function actorGateway( return next(); } + // Skip KV channel routes - handled by the dedicated KV channel endpoint + if (c.req.path.endsWith("/kv/connect")) { + return next(); + } + // Strip basePath from the request path let strippedPath = c.req.path; if ( diff --git a/rivetkit-typescript/packages/rivetkit/src/manager/kv-channel.ts b/rivetkit-typescript/packages/rivetkit/src/manager/kv-channel.ts new file mode 100644 index 0000000000..aa95b0ab22 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/src/manager/kv-channel.ts @@ -0,0 +1,709 @@ +// KV Channel WebSocket handler for the local dev manager. +// +// Serves the /kv/connect endpoint that the native SQLite addon +// (rivetkit-typescript/packages/sqlite-native/) connects to for +// KV-backed database I/O. See docs-internal/engine/NATIVE_SQLITE_DATA_CHANNEL.md +// for the full specification. + +import type { WSContext } from "hono/ws"; +import { + PROTOCOL_VERSION, + type ToServer, + type ToClient, + type RequestData, + type ResponseData, + type ToServerRequest, + decodeToServer, + encodeToClient, +} from "@rivetkit/engine-kv-channel-protocol"; +import { KvStorageQuotaExceededError } from "@/drivers/file-system/kv-limits"; +import type { ManagerDriver } from "./driver"; +import { logger } from "./log"; + +// Ping every 3 seconds, close if no pong within 15 seconds. +// Matches runner protocol defaults (runner_update_ping_interval_ms=3000, +// runner_ping_timeout_ms=15000 in engine/packages/config/src/config/pegboard.rs). +const PING_INTERVAL_MS = 3_000; +const PONG_TIMEOUT_MS = 15_000; + +// Maximum actors a single connection can open. Prevents unbounded memory growth. +const MAX_ACTORS_PER_CONNECTION = 1_000; + +// Sweep interval for removing stale lock entries from dead connections. +const STALE_LOCK_SWEEP_INTERVAL_MS = 60_000; + +/** Per-connection state for the KV channel WebSocket. */ +interface KvChannelConnection { + /** Actor IDs locked by this connection. */ + openActors: Set; + + /** Timer for sending pings. */ + pingInterval: ReturnType | null; + + /** Timer for detecting pong timeout. */ + pongTimeout: ReturnType | null; + + /** Timestamp of the last pong received. */ + lastPongTs: number; + + /** Whether the connection has been closed. */ + closed: boolean; + + /** Reference to the WebSocket context for sending messages. */ + ws: WSContext | null; + + /** Per-actor request queues for sequential execution. */ + actorQueues: Map>; +} + +/** Instance-scoped state for a KV channel manager. */ +interface KvChannelManagerState { + actorLocks: Map; + activeConnections: Set; + staleLockSweepTimer: ReturnType | null; +} + +/** Return type of createKvChannelManager. */ +export interface KvChannelManager { + createHandler: (managerDriver: ManagerDriver) => { + onOpen: (event: any, ws: WSContext) => void; + onMessage: (event: any, ws: WSContext) => void; + onClose: (event: any, ws: WSContext) => void; + onError: (error: any, ws: WSContext) => void; + }; + shutdown: () => void; + _testForceCloseAllKvChannels: () => number; +} + +/** + * Create an instance-scoped KV channel manager. + * + * All lock state and timers are scoped to the returned object, so multiple + * manager instances in the same process (e.g., tests) do not share state. + */ +export function createKvChannelManager(): KvChannelManager { + const state: KvChannelManagerState = { + actorLocks: new Map(), + activeConnections: new Set(), + staleLockSweepTimer: null, + }; + + return { + createHandler(managerDriver: ManagerDriver) { + const conn: KvChannelConnection = { + openActors: new Set(), + pingInterval: null, + pongTimeout: null, + lastPongTs: Date.now(), + closed: false, + ws: null, + actorQueues: new Map(), + }; + + state.activeConnections.add(conn); + + return { + onOpen: (_event: any, ws: WSContext) => { + logger().debug({ msg: "kv channel websocket opened" }); + conn.ws = ws; + startPingPong(state, conn); + }, + + onMessage: (event: any, _ws: WSContext) => { + try { + let bytes: Uint8Array; + if (event.data instanceof ArrayBuffer) { + bytes = new Uint8Array(event.data); + } else if (event.data instanceof Uint8Array) { + bytes = event.data; + } else if (Buffer.isBuffer(event.data)) { + bytes = new Uint8Array(event.data); + } else { + logger().warn({ + msg: "kv channel received non-binary message, ignoring", + }); + return; + } + + const msg = decodeToServer(bytes); + handleToServerMessage(state, conn, managerDriver, msg); + } catch (err: unknown) { + logger().error({ + msg: "kv channel failed to decode message", + error: + err instanceof Error + ? err.message + : String(err), + }); + } + }, + + onClose: (_event: any, _ws: WSContext) => { + logger().debug({ msg: "kv channel websocket closed" }); + cleanupConnection(state, conn); + }, + + onError: (error: any, _ws: WSContext) => { + logger().error({ + msg: "kv channel websocket error", + error: + error instanceof Error + ? error.message + : String(error), + }); + cleanupConnection(state, conn); + }, + }; + }, + + shutdown() { + if (state.staleLockSweepTimer) { + clearInterval(state.staleLockSweepTimer); + state.staleLockSweepTimer = null; + } + state.actorLocks.clear(); + state.activeConnections.clear(); + }, + + _testForceCloseAllKvChannels() { + let closed = 0; + for (const conn of state.activeConnections) { + if (!conn.closed && conn.ws) { + const ws = conn.ws; + cleanupConnection(state, conn); + ws.close(1001, "test force disconnect"); + closed++; + } + } + return closed; + }, + }; +} + +function makeErrorResponse( + requestId: number, + code: string, + message: string, +): ToClient { + return { + tag: "ToClientResponse", + val: { + requestId, + data: { + tag: "ErrorResponse", + val: { code, message }, + }, + }, + }; +} + +function makeResponse(requestId: number, data: ResponseData): ToClient { + return { + tag: "ToClientResponse", + val: { requestId, data }, + }; +} + +function sendMessage(conn: KvChannelConnection, msg: ToClient): void { + if (conn.closed || !conn.ws) return; + const bytes = encodeToClient(msg); + // Copy to a fresh ArrayBuffer to satisfy WSContext.send() parameter type. + const copy = new ArrayBuffer(bytes.byteLength); + new Uint8Array(copy).set(bytes); + conn.ws.send(copy); +} + +function startPingPong( + state: KvChannelManagerState, + conn: KvChannelConnection, +): void { + conn.lastPongTs = Date.now(); + + conn.pingInterval = setInterval(() => { + if (conn.closed || !conn.ws) return; + + const ts = BigInt(Date.now()); + sendMessage(conn, { + tag: "ToClientPing", + val: { ts }, + }); + + // Check if the last pong was too long ago. + if (Date.now() - conn.lastPongTs > PONG_TIMEOUT_MS) { + logger().warn({ + msg: "kv channel pong timeout, closing connection", + }); + // Capture ws before cleanup nulls it. + const ws = conn.ws; + cleanupConnection(state, conn); + if (ws) { + ws.close(1000, "pong timeout"); + } + } + }, PING_INTERVAL_MS); +} + +function cleanupConnection( + state: KvChannelManagerState, + conn: KvChannelConnection, +): void { + conn.closed = true; + conn.ws = null; + state.activeConnections.delete(conn); + + if (conn.pingInterval) { + clearInterval(conn.pingInterval); + conn.pingInterval = null; + } + if (conn.pongTimeout) { + clearTimeout(conn.pongTimeout); + conn.pongTimeout = null; + } + + // Release all actor locks held by this connection. + for (const actorId of conn.openActors) { + if (state.actorLocks.get(actorId) === conn) { + state.actorLocks.delete(actorId); + } + } + conn.openActors.clear(); +} + +async function handleRequest( + state: KvChannelManagerState, + conn: KvChannelConnection, + managerDriver: ManagerDriver, + request: ToServerRequest, +): Promise { + const { requestId, actorId, data } = request; + + try { + const responseData = await processRequestData( + state, + conn, + managerDriver, + actorId, + data, + ); + sendMessage(conn, makeResponse(requestId, responseData)); + } catch (err: unknown) { + // Log the full error server-side but return a generic message to the + // client to avoid leaking internal details. Specific known error codes + // (actor_not_open, actor_locked, storage_quota_exceeded, etc.) are + // returned as structured responses before reaching this catch block. + logger().error({ + msg: "kv channel request error", + requestId, + actorId, + error: err instanceof Error ? err.message : String(err), + }); + sendMessage( + conn, + makeErrorResponse(requestId, "internal_error", "internal error"), + ); + } +} + +// Defense-in-depth: in the engine KV channel, resolve_actor verifies the actor +// belongs to the authenticated namespace. The local dev manager is +// single-namespace, so all actors implicitly belong to the same namespace and +// no cross-namespace access is possible. If a less-privileged auth mechanism is +// introduced for the dev manager, namespace verification should be added here. +async function processRequestData( + state: KvChannelManagerState, + conn: KvChannelConnection, + managerDriver: ManagerDriver, + actorId: string, + data: RequestData, +): Promise { + switch (data.tag) { + case "ActorOpenRequest": + return handleActorOpen(state, conn, actorId); + + case "ActorCloseRequest": + return handleActorClose(state, conn, actorId); + + case "KvGetRequest": + case "KvPutRequest": + case "KvDeleteRequest": + case "KvDeleteRangeRequest": { + // All KV operations require the actor to be open on this connection. + const lockHolder = state.actorLocks.get(actorId); + if (!lockHolder || lockHolder !== conn) { + if (lockHolder && lockHolder !== conn) { + return { + tag: "ErrorResponse", + val: { + code: "actor_locked", + message: `actor ${actorId} is locked by another connection`, + }, + }; + } + return { + tag: "ErrorResponse", + val: { + code: "actor_not_open", + message: `actor ${actorId} is not open on this connection`, + }, + }; + } + return await handleKvOperation(managerDriver, actorId, data); + } + } +} + +function handleActorOpen( + state: KvChannelManagerState, + conn: KvChannelConnection, + actorId: string, +): ResponseData { + // Reject if this connection already has too many actors open. + if (conn.openActors.size >= MAX_ACTORS_PER_CONNECTION) { + return { + tag: "ErrorResponse", + val: { + code: "too_many_actors", + message: `connection has too many open actors (max ${MAX_ACTORS_PER_CONNECTION})`, + }, + }; + } + + const existingLock = state.actorLocks.get(actorId); + if (existingLock && existingLock !== conn) { + // Unconditionally evict the old connection's lock. The old connection + // is either dead (network issue) or stale (same process reconnecting). + // Remove the actor from the old connection's openActors so its next KV + // request fails the fast-path check immediately with actor_not_open. + existingLock.openActors.delete(actorId); + logger().info({ + msg: "kv channel evicting actor lock from old connection", + actorId, + }); + } + + state.actorLocks.set(actorId, conn); + conn.openActors.add(actorId); + + // Start the stale lock sweep if not already running. + ensureStaleLockSweep(state); + + return { tag: "ActorOpenResponse", val: null }; +} + +function handleActorClose( + state: KvChannelManagerState, + conn: KvChannelConnection, + actorId: string, +): ResponseData { + if (state.actorLocks.get(actorId) === conn) { + state.actorLocks.delete(actorId); + } + conn.openActors.delete(actorId); + + return { tag: "ActorCloseResponse", val: null }; +} + +/** Start the stale lock sweep if not already running. */ +function ensureStaleLockSweep(state: KvChannelManagerState): void { + if (state.staleLockSweepTimer) return; + state.staleLockSweepTimer = setInterval(() => { + let removed = 0; + for (const [actorId, conn] of state.actorLocks) { + if (conn.closed) { + state.actorLocks.delete(actorId); + removed++; + } + } + if (removed > 0) { + logger().debug({ + msg: "kv channel stale lock sweep completed", + removedCount: removed, + remainingCount: state.actorLocks.size, + }); + } + // Stop the sweep if there are no more lock entries. + if (state.actorLocks.size === 0 && state.staleLockSweepTimer) { + clearInterval(state.staleLockSweepTimer); + state.staleLockSweepTimer = null; + } + }, STALE_LOCK_SWEEP_INTERVAL_MS); + // Allow the process to exit even if the sweep timer is still running. + state.staleLockSweepTimer.unref?.(); +} + +type KvRequestData = Extract< + RequestData, + | { readonly tag: "KvGetRequest" } + | { readonly tag: "KvPutRequest" } + | { readonly tag: "KvDeleteRequest" } + | { readonly tag: "KvDeleteRangeRequest" } +>; + +async function handleKvOperation( + managerDriver: ManagerDriver, + actorId: string, + data: KvRequestData, +): Promise { + switch (data.tag) { + case "KvGetRequest": { + const keys = data.val.keys.map( + (k) => new Uint8Array(k), + ); + + // Validate key count. + if (keys.length > 128) { + return { + tag: "ErrorResponse", + val: { + code: "batch_too_large", + message: "a maximum of 128 keys is allowed", + }, + }; + } + + // Validate individual key sizes. + for (const key of keys) { + if (key.byteLength + 2 > 2048) { + return { + tag: "ErrorResponse", + val: { + code: "key_too_large", + message: "key is too long (max 2048 bytes)", + }, + }; + } + } + + const results = await managerDriver.kvBatchGet(actorId, keys); + + // Return only found keys and values. + const foundKeys: ArrayBuffer[] = []; + const foundValues: ArrayBuffer[] = []; + for (let i = 0; i < keys.length; i++) { + const val = results[i]; + if (val !== null) { + foundKeys.push(new Uint8Array(keys[i]).buffer as ArrayBuffer); + foundValues.push(new Uint8Array(val).buffer as ArrayBuffer); + } + } + + return { + tag: "KvGetResponse", + val: { keys: foundKeys, values: foundValues }, + }; + } + + case "KvPutRequest": { + const keys = data.val.keys.map( + (k) => new Uint8Array(k), + ); + const values = data.val.values.map( + (v) => new Uint8Array(v), + ); + + if (keys.length !== values.length) { + return { + tag: "ErrorResponse", + val: { + code: "keys_values_length_mismatch", + message: + "keys and values arrays must have the same length", + }, + }; + } + + if (keys.length > 128) { + return { + tag: "ErrorResponse", + val: { + code: "batch_too_large", + message: + "a maximum of 128 key-value entries is allowed", + }, + }; + } + + // Validate sizes. + let payloadSize = 0; + for (let i = 0; i < keys.length; i++) { + if (keys[i].byteLength + 2 > 2048) { + return { + tag: "ErrorResponse", + val: { + code: "key_too_large", + message: "key is too long (max 2048 bytes)", + }, + }; + } + if (values[i].byteLength > 128 * 1024) { + return { + tag: "ErrorResponse", + val: { + code: "value_too_large", + message: "value is too large (max 128 KiB)", + }, + }; + } + payloadSize += + keys[i].byteLength + 2 + values[i].byteLength; + } + + if (payloadSize > 976 * 1024) { + return { + tag: "ErrorResponse", + val: { + code: "payload_too_large", + message: + "total payload is too large (max 976 KiB)", + }, + }; + } + + const entries: [Uint8Array, Uint8Array][] = keys.map( + (k, i) => [k, values[i]], + ); + + try { + await managerDriver.kvBatchPut(actorId, entries); + } catch (err: unknown) { + if (err instanceof KvStorageQuotaExceededError) { + return { + tag: "ErrorResponse", + val: { + code: "storage_quota_exceeded", + message: err.message, + }, + }; + } + throw err; + } + + return { tag: "KvPutResponse", val: null }; + } + + case "KvDeleteRequest": { + const keys = data.val.keys.map( + (k) => new Uint8Array(k), + ); + + if (keys.length > 128) { + return { + tag: "ErrorResponse", + val: { + code: "batch_too_large", + message: "a maximum of 128 keys is allowed", + }, + }; + } + + for (const key of keys) { + if (key.byteLength + 2 > 2048) { + return { + tag: "ErrorResponse", + val: { + code: "key_too_large", + message: "key is too long (max 2048 bytes)", + }, + }; + } + } + + await managerDriver.kvBatchDelete(actorId, keys); + + return { tag: "KvDeleteResponse", val: null }; + } + + case "KvDeleteRangeRequest": { + const start = new Uint8Array(data.val.start); + const end = new Uint8Array(data.val.end); + + if (start.byteLength + 2 > 2048) { + return { + tag: "ErrorResponse", + val: { + code: "key_too_large", + message: "start key is too long (max 2048 bytes)", + }, + }; + } + if (end.byteLength + 2 > 2048) { + return { + tag: "ErrorResponse", + val: { + code: "key_too_large", + message: "end key is too long (max 2048 bytes)", + }, + }; + } + + await managerDriver.kvDeleteRange(actorId, start, end); + + return { tag: "KvDeleteResponse", val: null }; + } + + default: { + // Should never happen since processRequestData routes only KV tags here. + const _exhaustive: never = data; + throw new Error(`unexpected request tag`); + } + } +} + +function handleToServerMessage( + state: KvChannelManagerState, + conn: KvChannelConnection, + managerDriver: ManagerDriver, + msg: ToServer, +): void { + switch (msg.tag) { + case "ToServerRequest": { + const { actorId } = msg.val; + + // Chain requests per actor so they execute sequentially, + // preventing journal write ordering violations. Cross-actor + // requests still execute concurrently since each actor has its + // own queue. See docs-internal/engine/NATIVE_SQLITE_REVIEW_FIXES.md H2. + const prev = conn.actorQueues.get(actorId) ?? Promise.resolve(); + const next = prev.then(() => + handleRequest(state, conn, managerDriver, msg.val).catch( + (err) => { + logger().error({ + msg: "unhandled error in kv channel request handler", + error: + err instanceof Error + ? err.message + : String(err), + }); + }, + ), + ); + conn.actorQueues.set(actorId, next); + + // Clean up the queue entry once it settles to avoid unbounded map growth. + next.then(() => { + if (conn.actorQueues.get(actorId) === next) { + conn.actorQueues.delete(actorId); + } + }); + break; + } + + case "ToServerPong": + conn.lastPongTs = Date.now(); + break; + } +} + +/** Validate the protocol version query parameter. Returns an error string or null. */ +export function validateProtocolVersion( + protocolVersion: string | undefined, +): string | null { + if (!protocolVersion) { + return "missing protocol_version query parameter"; + } + const version = Number.parseInt(protocolVersion, 10); + if (Number.isNaN(version) || version !== PROTOCOL_VERSION) { + return `unsupported protocol_version: ${protocolVersion} (server supports ${PROTOCOL_VERSION})`; + } + return null; +} diff --git a/rivetkit-typescript/packages/rivetkit/src/manager/router.ts b/rivetkit-typescript/packages/rivetkit/src/manager/router.ts index c05ede00b9..e876081411 100644 --- a/rivetkit-typescript/packages/rivetkit/src/manager/router.ts +++ b/rivetkit-typescript/packages/rivetkit/src/manager/router.ts @@ -48,6 +48,10 @@ import { } from "@/utils/router"; import type { ActorOutput, ManagerDriver } from "./driver"; import { actorGateway, createTestWebSocketProxy } from "./gateway"; +import { + createKvChannelManager, + validateProtocolVersion, +} from "./kv-channel"; import { logger } from "./log"; export function buildManagerRouter( @@ -56,6 +60,12 @@ export function buildManagerRouter( getUpgradeWebSocket: GetUpgradeWebSocket | undefined, runtime: Runtime = "node", ) { + const kvChannelManager = createKvChannelManager(); + + // Inject the KV channel shutdown into the driver so it can be + // called during the driver's teardown, after all actors have stopped. + managerDriver.setKvChannelShutdown?.(kvChannelManager.shutdown); + return createRouter(config.managerBasePath, (router) => { // Actor gateway router.use( @@ -355,6 +365,53 @@ export function buildManagerRouter( }); } + // GET /kv/connect - KV channel WebSocket endpoint for native SQLite + router.get("/kv/connect", async (c) => { + // Validate authentication. + if (isDev() && !config.token) { + logger().warn({ + msg: "RIVET_TOKEN is not set, skipping KV channel auth in development mode", + }); + } else { + const token = c.req.query("token"); + if (!config.token) { + return c.text("KV channel requires RIVET_TOKEN to be set", 403); + } + if ( + !token || + timingSafeEqual(config.token, token) === false + ) { + return c.json( + { + error: { + code: "unauthorized", + message: "invalid or missing authentication token", + }, + }, + 401, + ); + } + } + + // Validate protocol version. + const versionError = validateProtocolVersion( + c.req.query("protocol_version"), + ); + if (versionError) { + return c.text(versionError, 400); + } + + // Upgrade to WebSocket. + const upgradeWebSocket = getUpgradeWebSocket?.(); + if (!upgradeWebSocket) { + return c.text("WebSocket upgrades not supported on this platform", 500); + } + + return upgradeWebSocket(() => + kvChannelManager.createHandler(managerDriver), + )(c, noopNext()); + }); + // TODO: // // DELETE /actors/{actor_id} // { @@ -585,6 +642,13 @@ export function buildManagerRouter( return c.text(`Error: ${error}`, 500); } }); + + // Force-close all KV channel WebSocket connections. Used by + // stress tests to simulate network failures mid-operation. + router.post("/.test/kv-channel/force-disconnect", async (c) => { + const closed = kvChannelManager._testForceCloseAllKvChannels(); + return c.json({ closed }); + }); } if (config.inspector.enabled) { diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts index b2ec654cc4..d347bd6f79 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts @@ -28,7 +28,7 @@ export const EnvoyConfigSchema = z.object({ // Deprecated. totalSlots: z.number().default(() => getRivetTotalSlots() ?? 100000), - runnerKey: z.string().optional(), + envoyKey: z.string().optional(), }); export type EnvoyConfigInput = z.input; export type EnvoyConfig = z.infer; diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts index f04d240a81..51495b8c3c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts @@ -1,6 +1,12 @@ import { z } from "zod"; import { getRunMetadata } from "@/actor/config"; import type { ActorDefinition, AnyActorDefinition } from "@/actor/definition"; +import { + KEYS, + queueMetadataKey, + sqliteStoragePrefix, + workflowStoragePrefix, +} from "@/actor/instance/keys"; import { type Logger, LogLevelSchema } from "@/common/log"; import { ENGINE_ENDPOINT } from "@/engine-process/constants"; import { InspectorConfigSchema } from "@/inspector/config"; @@ -218,7 +224,7 @@ export const RegistryConfigSchema = z }); } - // configurerPool requires an engine (via endpoint or spawnEngine) + // configurePool requires an engine (via endpoint or spawnEngine) if ( config.serverless.configurePool && !parsedEndpoint && @@ -321,6 +327,30 @@ export function buildActorNames( // Actor options take precedence over run metadata metadata.icon = options.icon ?? runMeta.icon; metadata.name = options.name ?? runMeta.name; + metadata.preload = { + keys: [ + Array.from(KEYS.PERSIST_DATA), + Array.from(KEYS.INSPECTOR_TOKEN), + Array.from(queueMetadataKey()), + ], + prefixes: [ + { + prefix: Array.from(sqliteStoragePrefix()), + maxBytes: options.preloadMaxSqliteBytes ?? 786_432, + partial: true, + }, + { + prefix: Array.from(workflowStoragePrefix()), + maxBytes: options.preloadMaxWorkflowBytes ?? 131_072, + partial: false, + }, + { + prefix: Array.from(KEYS.CONN_PREFIX), + maxBytes: options.preloadMaxConnectionsBytes ?? 65_536, + partial: false, + }, + ], + }; // Remove undefined values if (!metadata.icon) delete metadata.icon; if (!metadata.name) delete metadata.name; @@ -440,26 +470,26 @@ export const DocServerlessConfigSchema = z }) .describe("Configuration for serverless deployment mode."); -export const DocRunnerConfigSchema = z +export const DocEnvoyConfigSchema = z .object({ totalSlots: z .number() .optional() .describe("Total number of actor slots available. Default: 100000"), - runnerName: z + poolName: z .string() .optional() - .describe("Name of this runner. Default: 'default'"), - runnerKey: z + .describe("Name of this envoy pool. Default: 'default'"), + envoyKey: z .string() .optional() - .describe("Deprecated. Authentication key for the runner."), + .describe("Deprecated. Authentication key for the envoy."), version: z .number() .optional() - .describe("Version number of this runner. Default: 1"), + .describe("Version number of this envoy. Default: 1"), }) - .describe("Configuration for runner mode."); + .describe("Configuration for envoy mode."); export const DocRegistryConfigSchema = z .object({ @@ -563,6 +593,6 @@ export const DocRegistryConfigSchema = z .describe("Port to run the manager on. Default: 6420"), inspector: DocInspectorConfigSchema, serverless: DocServerlessConfigSchema.optional(), - runner: DocRunnerConfigSchema.optional(), + envoy: DocEnvoyConfigSchema.optional(), }) .describe("RivetKit registry configuration."); diff --git a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/mod.ts b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/mod.ts index a4884b9641..782750eefc 100644 --- a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/mod.ts @@ -345,6 +345,39 @@ export class RemoteManagerDriver implements ManagerDriver { return response.value; } + async kvBatchGet( + _actorId: string, + _keys: Uint8Array[], + ): Promise<(Uint8Array | null)[]> { + throw new Error("kvBatchGet not supported on remote manager driver"); + } + + async kvBatchPut( + _actorId: string, + _entries: [Uint8Array, Uint8Array][], + ): Promise { + throw new Error("kvBatchPut not supported on remote manager driver"); + } + + async kvBatchDelete( + _actorId: string, + _keys: Uint8Array[], + ): Promise { + throw new Error( + "kvBatchDelete not supported on remote manager driver", + ); + } + + async kvDeleteRange( + _actorId: string, + _start: Uint8Array, + _end: Uint8Array, + ): Promise { + throw new Error( + "kvDeleteRange not supported on remote manager driver", + ); + } + displayInformation(): ManagerDisplayInformation { return { properties: {} }; } diff --git a/rivetkit-typescript/packages/rivetkit/tests/db-closed-race.test.ts b/rivetkit-typescript/packages/rivetkit/tests/db-closed-race.test.ts index 746a541b68..1a11ed0f1c 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/db-closed-race.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/db-closed-race.test.ts @@ -22,7 +22,7 @@ describe("database closed race condition", () => { const client = createClient({ endpoint: runtime.endpoint, namespace: runtime.namespace, - runnerName: runtime.runnerName, + poolName: runtime.runnerName, disableMetadataLookup: true, }); diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver-engine.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver-engine.test.ts index 4eb208d2c8..dd381d40b1 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver-engine.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver-engine.test.ts @@ -1,11 +1,14 @@ import { join } from "node:path"; import { createClientWithDriver } from "@/client/client"; import { createTestRuntime, runDriverTests } from "@/driver-test-suite/mod"; +import type { DriverTestConfig } from "@/driver-test-suite/mod"; +import { setupDriverTest } from "@/driver-test-suite/utils"; import { createEngineDriver } from "@/drivers/engine/mod"; import invariant from "invariant"; import { convertRegistryConfigToClientConfig } from "@/client/config"; +import { describe, expect, test, vi } from "vitest"; -runDriverTests({ +const driverTestConfig = { // Use real timers for engine-runner tests useRealTimers: true, skip: { @@ -57,9 +60,9 @@ runDriverTests({ registry.config.endpoint = endpoint; registry.config.namespace = namespace; registry.config.token = token; - registry.config.runner = { - ...registry.config.runner, - runnerName, + registry.config.envoy = { + ...registry.config.envoy, + poolName: runnerName, }; // Parse config only after mutating registry.config so the manager @@ -140,6 +143,10 @@ runDriverTests({ token, }, driver: driverConfig, + hardCrashActor: async (actorId: string) => { + await actorDriver.hardCrashActor?.(actorId); + }, + hardCrashPreservesData: true, cleanup: async () => { await actorDriver.shutdownRunner?.(true); }, @@ -147,4 +154,40 @@ runDriverTests({ }, ); }, +} satisfies Omit; + +runDriverTests(driverTestConfig); + +describe("engine startup kv preload", () => { + test("wakes actors with envoy-provided preloaded kv", async (c) => { + const { client } = await setupDriverTest(c, { + ...driverTestConfig, + clientType: "http", + encoding: "bare", + }); + const handle = client.sleep.getOrCreate(); + + await handle.getCounts(); + await handle.triggerSleep(); + + await vi.waitFor( + async () => { + const counts = await handle.getCounts(); + expect(counts.sleepCount).toBeGreaterThanOrEqual(1); + expect(counts.startCount).toBeGreaterThanOrEqual(2); + }, + { timeout: 5_000, interval: 100 }, + ); + + const gatewayUrl = await handle.getGatewayUrl(); + const response = await fetch(`${gatewayUrl}/inspector/metrics`, { + headers: { Authorization: "Bearer token" }, + }); + expect(response.status).toBe(200); + + const metrics: any = await response.json(); + expect(metrics.startup_is_new.value).toBe(0); + expect(metrics.startup_internal_preload_kv_entries.value).toBeGreaterThan(0); + expect(metrics.startup_kv_round_trips.value).toBe(0); + }); }); diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts index 6ddb862120..c6168f7689 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver-file-system.test.ts @@ -42,7 +42,7 @@ describe("file-system websocket hibernation cleanup", () => { const client = createClient({ endpoint: runtime.endpoint, namespace: runtime.namespace, - runnerName: runtime.runnerName, + poolName: runtime.runnerName, disableMetadataLookup: true, }); const conn = client.fileSystemHibernationCleanupActor diff --git a/rivetkit-typescript/packages/rivetkit/tsconfig.json b/rivetkit-typescript/packages/rivetkit/tsconfig.json index b23ac70edd..a3671f47d7 100644 --- a/rivetkit-typescript/packages/rivetkit/tsconfig.json +++ b/rivetkit-typescript/packages/rivetkit/tsconfig.json @@ -14,6 +14,7 @@ "@rivetkit/sqlite-vfs/wasm": ["../sqlite-vfs/src/wasm.ts"], // Used for test fixtures "rivetkit": ["./src/mod.ts"], + "rivetkit/errors": ["./src/actor/errors.ts"], "rivetkit/utils": ["./src/utils.ts"], "rivetkit/sandbox": ["./src/sandbox/index.ts"], "rivetkit/sandbox/docker": ["./src/sandbox/providers/docker.ts"], diff --git a/rivetkit-typescript/packages/rivetkit/vitest.config.ts b/rivetkit-typescript/packages/rivetkit/vitest.config.ts index f7424359d1..161dc6af32 100644 --- a/rivetkit-typescript/packages/rivetkit/vitest.config.ts +++ b/rivetkit-typescript/packages/rivetkit/vitest.config.ts @@ -7,4 +7,9 @@ export default defineConfig({ ...defaultConfig, // Used to resolve "rivetkit" to "src/mod.ts" in the test fixtures plugins: [tsconfigPaths()], + resolve: { + alias: { + "rivetkit/errors": resolve(__dirname, "./src/actor/errors.ts"), + }, + }, }); diff --git a/rivetkit-typescript/packages/sqlite-native/Cargo.lock b/rivetkit-typescript/packages/sqlite-native/Cargo.lock new file mode 100644 index 0000000000..f1e2f7f83a --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/Cargo.lock @@ -0,0 +1,988 @@ +# 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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +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 = "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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "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 = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[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.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[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.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[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", + "serde", + "serde_json", + "tokio", +] + +[[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 = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rivet-kv-channel-protocol" +version = "2.2.1" +dependencies = [ + "serde", + "serde_bare", + "vbare", + "vbare-compiler", +] + +[[package]] +name = "rivetkit-sqlite-native" +version = "2.1.6" +dependencies = [ + "futures-util", + "getrandom", + "libsqlite3-sys", + "lru", + "napi", + "napi-build", + "napi-derive", + "rivet-kv-channel-protocol", + "serde", + "serde_bare", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "urlencoding", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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_bare" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55386eed0f1ae957b091dc2ca8122f287b60c79c774cbe3d5f2b69fded660" +dependencies = [ + "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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +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 = "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.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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 = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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 = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vbare" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a63d4c173f6a6f7c8f524dcdda615e51f83d12bca9cc129f676229b995ca41" +dependencies = [ + "anyhow", +] + +[[package]] +name = "vbare-compiler" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce091703409d0a86ddd6c02c68794abce94e256bebe971724fa1e1296d309939" +dependencies = [ + "indoc", + "prettyplease", + "syn", + "vbare-gen", +] + +[[package]] +name = "vbare-gen" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2059af920d5d876dd7b737dac2c647aa872a58ef266d8af5bd660a2ec6c25bcb" +dependencies = [ + "heck", + "pest", + "pest_derive", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rivetkit-typescript/packages/sqlite-native/Cargo.toml b/rivetkit-typescript/packages/sqlite-native/Cargo.toml new file mode 100644 index 0000000000..43b84de555 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "rivetkit-sqlite-native" +version = "2.1.6" +edition = "2021" +license = "Apache-2.0" +description = "Native SQLite addon for RivetKit backed by KV channel protocol" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "2", default-features = false, features = ["napi6", "async", "serde-json"] } +napi-derive = "2" +libsqlite3-sys = { version = "0.30", features = ["bundled"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "net", "time", "macros"] } +tokio-tungstenite = "0.24" +futures-util = { version = "0.3", default-features = false, features = ["sink"] } +rivet-kv-channel-protocol = { path = "../../../engine/sdks/rust/kv-channel-protocol" } +serde = { version = "1", features = ["derive"] } +serde_bare = "0.5" +serde_json = "1" +lru = "0.12" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +urlencoding = "2" +getrandom = "0.2" + +[build-dependencies] +napi-build = "2" + +[workspace] + +[profile.release] +lto = true diff --git a/rivetkit-typescript/packages/sqlite-native/build.rs b/rivetkit-typescript/packages/sqlite-native/build.rs new file mode 100644 index 0000000000..f8bfd67ec9 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/rivetkit-typescript/packages/sqlite-native/index.d.ts b/rivetkit-typescript/packages/sqlite-native/index.d.ts new file mode 100644 index 0000000000..49ed86044b --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/index.d.ts @@ -0,0 +1,180 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +/** + * Typed bind parameter passed from JavaScript. + * + * Replaces `Vec` for statement parameters, avoiding 20x + * serialization overhead for blob data. Instead of JSON arrays of numbers, + * blobs are passed as `Buffer` (a single memcpy from JS heap to Rust). + * + * See docs-internal/engine/NATIVE_SQLITE_REVIEW_FIXES.md M7. + */ +export interface BindParam { + /** One of: "null", "int", "float", "text", "blob" */ + kind: string + intValue?: number + floatValue?: number + textValue?: string + blobValue?: Buffer +} +/** Configuration for connecting to the KV channel endpoint. */ +export interface ConnectConfig { + url: string + token?: string + namespace: string +} +/** Result of an execute() call. */ +export interface ExecuteResult { + changes: number +} +/** Result of a query() call. */ +export interface QueryResult { + columns: Array + rows: Array> +} +/** + * Open the shared KV channel WebSocket connection. + * + * In production, token is the engine's admin_token (RIVET__AUTH__ADMIN_TOKEN). + * In local dev, token is config.token (RIVET_TOKEN), optional in dev mode. + */ +export declare function connect(config: ConnectConfig): KvChannel +/** + * Open a database for an actor. Sends ActorOpenRequest optimistically. + * + * VFS registration and sqlite3_open_v2 run inside `spawn_blocking` because + * they trigger synchronous VFS callbacks that call `Handle::block_on()` for + * KV I/O. This is safe from a blocking thread but would deadlock or freeze + * the Node.js main thread if called via `rt.block_on()`. + */ +export declare function openDatabase(channel: KvChannel, actorId: string): Promise +/** + * Execute a statement (INSERT, UPDATE, DELETE, CREATE, etc.). + * + * SQLite operations run on tokio's blocking thread pool via `spawn_blocking`. + * VFS callbacks call `Handle::block_on()` from blocking threads (not tokio + * worker threads), which is safe. The Node.js main thread is never blocked. + * + * Three threading approaches were considered: + * + * 1. **spawn_blocking** (chosen): napi `async fn` dispatches to tokio's + * blocking thread pool (default cap 512). Simplest, idiomatic, tokio + * manages the pool. Minor downside: thread may change between queries + * (slightly worse cache locality). + * + * 2. **Dedicated thread per actor**: One `std::thread` per actor, receives + * SQL via mpsc, sends results via oneshot. Best cache locality, but + * requires manual lifecycle management and one idle thread per open actor. + * + * 3. **Channel + block-in-place**: Sync napi function, VFS callbacks send + * requests via `std::sync::mpsc` and block on `recv()`. Does NOT solve + * the core problem because the Node.js main thread is still blocked. + * + * See docs-internal/engine/NATIVE_SQLITE_REVIEW_FINDINGS.md Finding 1. + */ +export declare function execute(db: NativeDatabase, sql: string, params?: Array | undefined | null): Promise +/** + * Run a query (SELECT, PRAGMA, etc.). + * + * See `execute` for threading model documentation. + */ +export declare function query(db: NativeDatabase, sql: string, params?: Array | undefined | null): Promise +/** + * Execute multi-statement SQL without parameters. + * Uses sqlite3_prepare_v2 in a loop with tail pointer tracking to handle + * multiple statements (e.g., migrations). Returns columns and rows from + * the last statement that produced results. + * + * See `execute` for threading model documentation. + */ +export declare function exec(db: NativeDatabase, sql: string): Promise +/** + * Close the database connection and release the actor lock. + * Sends ActorCloseRequest to the server. + * + * Locks the db mutex and takes the Option, so concurrent/subsequent + * execute/query/exec operations see None and return "database is closed". + */ +export declare function closeDatabase(db: NativeDatabase): Promise +/** Close the KV channel WebSocket connection. */ +export declare function disconnect(channel: KvChannel): Promise +/** Per-operation metrics snapshot. */ +export interface OpMetricsSnapshot { + count: number + totalDurationUs: number + minDurationUs: number + maxDurationUs: number + avgDurationUs: number +} +/** All KV channel metrics (Layer 1). */ +export interface KvChannelMetricsSnapshot { + get: OpMetricsSnapshot + put: OpMetricsSnapshot + delete: OpMetricsSnapshot + deleteRange: OpMetricsSnapshot + actorOpen: OpMetricsSnapshot + actorClose: OpMetricsSnapshot + keysTotal: number + requestsTotal: number + batchAtomicCommits: number + batchAtomicPages: number +} +/** SQL execution metrics (Layer 0). */ +export interface SqlMetricsSnapshot { + execute: OpMetricsSnapshot + query: OpMetricsSnapshot + exec: OpMetricsSnapshot + spawnBlockingWait: OpMetricsSnapshot + sqliteStep: OpMetricsSnapshot + stmtCache: OpMetricsSnapshot + resultSerialize: OpMetricsSnapshot +} +/** VFS callback metrics. */ +export interface VfsMetricsSnapshot { + xreadCount: number + xreadUs: number + xwriteCount: number + xwriteUs: number + xwriteBufferedCount: number + xsyncCount: number + xsyncUs: number + commitAtomicCount: number + commitAtomicUs: number + commitAtomicPages: number +} +/** All metrics across all layers. */ +export interface AllMetricsSnapshot { + kvChannel: KvChannelMetricsSnapshot + sql: SqlMetricsSnapshot + vfs: VfsMetricsSnapshot +} +/** Get a snapshot of all metrics across all layers. */ +export declare function getMetrics(channel: KvChannel): AllMetricsSnapshot +export type JsKvChannel = KvChannel +/** + * A shared WebSocket connection to the KV channel server. + * One per process, shared across all actors. + * + * The tokio runtime is owned here so it is dropped when the channel is dropped, + * ensuring clean process exit after disconnect. The runtime MUST NOT be dropped + * before all actors have closed their databases. + */ +export declare class KvChannel { } +export type JsNativeDatabase = NativeDatabase +/** + * An open SQLite database backed by KV storage via the channel. + * + * The `db` field is wrapped in `Arc>>` so that + * `close_database` can atomically take the handle while concurrent + * `execute`/`query`/`exec` closures hold an Arc clone. Any operation + * that finds `None` returns a "database is closed" error. This prevents + * use-after-free if `close_database` runs between pointer extraction + * and `spawn_blocking` task execution. + * + * Field order matters for drop safety: `stmt_cache` is declared before `db` + * so cached statements are finalized before the database connection is closed. + */ +export declare class NativeDatabase { } diff --git a/rivetkit-typescript/packages/sqlite-native/index.js b/rivetkit-typescript/packages/sqlite-native/index.js new file mode 100644 index 0000000000..167fd643d5 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/index.js @@ -0,0 +1,324 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'sqlite-native.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.android-arm64.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'sqlite-native.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.android-arm-eabi.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.win32-x64-msvc.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.win32-ia32-msvc.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.win32-arm64-msvc.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'sqlite-native.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.darwin-universal.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'sqlite-native.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.darwin-x64.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.darwin-arm64.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'sqlite-native.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.freebsd-x64.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-x64-musl.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-x64-gnu.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-arm64-musl.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-arm64-gnu.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-arm-musleabihf.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + } + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-riscv64-musl.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-riscv64-gnu.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-riscv64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'sqlite-native.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./sqlite-native.linux-s390x-gnu.node') + } else { + nativeBinding = require('@rivetkit/sqlite-native-linux-s390x-gnu') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { KvChannel, NativeDatabase, connect, openDatabase, execute, query, exec, closeDatabase, disconnect, getMetrics } = nativeBinding + +module.exports.KvChannel = KvChannel +module.exports.NativeDatabase = NativeDatabase +module.exports.connect = connect +module.exports.openDatabase = openDatabase +module.exports.execute = execute +module.exports.query = query +module.exports.exec = exec +module.exports.closeDatabase = closeDatabase +module.exports.disconnect = disconnect +module.exports.getMetrics = getMetrics diff --git a/rivetkit-typescript/packages/sqlite-native/npm/darwin-arm64/package.json b/rivetkit-typescript/packages/sqlite-native/npm/darwin-arm64/package.json new file mode 100644 index 0000000000..6f456a9ab3 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/npm/darwin-arm64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rivetkit/sqlite-native-darwin-arm64", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit - macOS arm64", + "license": "Apache-2.0", + "os": ["darwin"], + "cpu": ["arm64"], + "main": "sqlite-native.darwin-arm64.node", + "files": ["sqlite-native.darwin-arm64.node"], + "engines": { + "node": ">= 20.0.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/npm/darwin-x64/package.json b/rivetkit-typescript/packages/sqlite-native/npm/darwin-x64/package.json new file mode 100644 index 0000000000..933de7d328 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/npm/darwin-x64/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rivetkit/sqlite-native-darwin-x64", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit - macOS x64", + "license": "Apache-2.0", + "os": ["darwin"], + "cpu": ["x64"], + "main": "sqlite-native.darwin-x64.node", + "files": ["sqlite-native.darwin-x64.node"], + "engines": { + "node": ">= 20.0.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/npm/linux-arm64-gnu/package.json b/rivetkit-typescript/packages/sqlite-native/npm/linux-arm64-gnu/package.json new file mode 100644 index 0000000000..f797a36b3e --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/npm/linux-arm64-gnu/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rivetkit/sqlite-native-linux-arm64-gnu", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit - Linux arm64 GNU", + "license": "Apache-2.0", + "os": ["linux"], + "cpu": ["arm64"], + "main": "sqlite-native.linux-arm64-gnu.node", + "files": ["sqlite-native.linux-arm64-gnu.node"], + "engines": { + "node": ">= 20.0.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/npm/linux-x64-gnu/package.json b/rivetkit-typescript/packages/sqlite-native/npm/linux-x64-gnu/package.json new file mode 100644 index 0000000000..716c52c8bc --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/npm/linux-x64-gnu/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rivetkit/sqlite-native-linux-x64-gnu", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit - Linux x64 GNU", + "license": "Apache-2.0", + "os": ["linux"], + "cpu": ["x64"], + "main": "sqlite-native.linux-x64-gnu.node", + "files": ["sqlite-native.linux-x64-gnu.node"], + "engines": { + "node": ">= 20.0.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/npm/win32-x64-msvc/package.json b/rivetkit-typescript/packages/sqlite-native/npm/win32-x64-msvc/package.json new file mode 100644 index 0000000000..a5045b2a9b --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/npm/win32-x64-msvc/package.json @@ -0,0 +1,13 @@ +{ + "name": "@rivetkit/sqlite-native-win32-x64-msvc", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit - Windows x64 MSVC", + "license": "Apache-2.0", + "os": ["win32"], + "cpu": ["x64"], + "main": "sqlite-native.win32-x64-msvc.node", + "files": ["sqlite-native.win32-x64-msvc.node"], + "engines": { + "node": ">= 20.0.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/package.json b/rivetkit-typescript/packages/sqlite-native/package.json new file mode 100644 index 0000000000..2a8e48c3a7 --- /dev/null +++ b/rivetkit-typescript/packages/sqlite-native/package.json @@ -0,0 +1,43 @@ +{ + "name": "@rivetkit/sqlite-native", + "version": "2.1.6", + "description": "Native SQLite addon for RivetKit backed by KV channel protocol", + "license": "Apache-2.0", + "main": "index.js", + "types": "index.d.ts", + "engines": { + "node": ">= 20.0.0" + }, + "napi": { + "name": "sqlite-native", + "triples": { + "defaults": false, + "additional": [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc" + ] + } + }, + "files": [ + "index.js", + "index.d.ts", + "package.json" + ], + "scripts": { + "build": "napi build --platform --release", + "prepublishOnly": "napi prepublish -t npm" + }, + "optionalDependencies": { + "@rivetkit/sqlite-native-linux-x64-gnu": "2.1.6", + "@rivetkit/sqlite-native-linux-arm64-gnu": "2.1.6", + "@rivetkit/sqlite-native-darwin-x64": "2.1.6", + "@rivetkit/sqlite-native-darwin-arm64": "2.1.6", + "@rivetkit/sqlite-native-win32-x64-msvc": "2.1.6" + }, + "devDependencies": { + "@napi-rs/cli": "^2.18.0" + } +} diff --git a/rivetkit-typescript/packages/sqlite-native/sqlite-native.linux-x64-gnu.node b/rivetkit-typescript/packages/sqlite-native/sqlite-native.linux-x64-gnu.node new file mode 100755 index 0000000000000000000000000000000000000000..c108848343fd23278d04abbea0fae5a341e3c6ad GIT binary patch literal 4497345 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 +S+v0{Q)196z^fNY{2`bT(9sI)}&ET1S5xesn-+ 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Ð!(vM}dn+$z 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
MPA+QdlcKV=h-!mQIa z(d9MtK7(Ch=2_|kEQ57lr#*EJyTjaajxXYu!43FxD$o;u33P2k?gi2#+*8SZZ&-L6 z5prP}^s>E>dFfN|`)b?8Nc=f<>|(q0N7$(^$c?m%tFWD|0g`%aM{KBiq5Bq2O(DM!A!R`2qTG<5; z4U4SSb};}JwXur~wr94ri(LG^tL5-`kWIJj!-&FZRb@2t^^~nr0Vm-y|Qs zkpn$_>>>|l!D3hhc{ET2`l1icmr4Kh3f;upin2h4(JW%x+s?j--? zC|_7G0lU79onQcZGws5&i+GROMHc>?b#_q%bKy_008V+2{H(W&0$A`AatSYlCHQkU zVBhz#=QG$9mOxh__St9`eke8(k8uBs*l#!W5iTIS=w<4IdAhu zQJ%Y~k8h(t^t?yIM21ox?tRs7L3?56pp?P?S@S#4EaB7diMd zD@h-JHarV+p=&pK2koL6%!a*S4jd`{jHj=HxiAkF+8n~Rhjd*I(HNH1a*!VRi*ksu zu&{wcEPx{3L3+rA8=x0%hd#I$`e6wSz*8{W+)nwlaEQ7ekba6oOo9H^4zX4GJ355+ zA?bE;hye8Y9U>R{dpg81=uLNsE*~MUze6mO9OMuOVL<>r_oB~Ghsc8742N)izgkz?Gy$01AUyL0v5u)MdUNxDei_v z{hVSC^!Im)<-{+5bw4Bj1CR#`;IlAmpi>mGT?CkS{yFx9bwA>G7~&MSNe*+0y)YM! zWV?4b>ESOM;S^K$%Hz)|vI#GQdHBUBr#J{RVbsU?N3$Qyg?X?99)!NJPT~KAaF_)> z#_G|qyry?Wj7%IbHZmKALcH^UXr&EAC}$W6urJ6-9_XNX5Zr!MX+R#Q_L+U{1eiF zxu2uQKGOM;e13slzmoAu{}A;6dcQ?q=!aP{9?XT_@7N#az%p0@qxK`W1igycA7Jo)8t0!{5B%MAkF#~$~y2L7&o9+^OU?DsN z1A|5wA#XC{@X#~OC6>d?87^@Umd(O0-;nNXmuL(#vs@wovu<*Ub+8cbfS$Q7QG$HW zJoLoxpYIZ$Z`p65)EE73$6okzVHPaAgZ-d)5%FOTw0%eXY?sJ@g)kfX?sSP~VJO_>Gw*VVWv~cth5ozI2j(mx-uLWx5AmRPnM*7{AMgFx6=px^5>a1c z$CcO}`d3pf#4CWOWc)|4%MXHnE|<6)=D>Gh$)n`!NAy~QT17BVS>lcg%}rxE!Yoc z!CY7%{o}(#ndB6{l0rP+v@kIjX2Ml42lo0IJ*J0=+hFz#if;ek?|LV ziRJhUZw(Vi@n&r!mm4+b7Y&TrUhMVMFzvmOc)cN6Ye zgVFBSe$FL_XgB?m?DSE=p zKkyT;5YB*}(_vzVjgtJQ5+8bc zxkWAvq`8H596frw#SB;iZ-Y7g-C{2c3?}^()R%x;^nyM(5_*S_4lKxUiz4KEhe`R^ zdjxtBUIyEqMBXU3$cANO+#-YRMdRJ#QJ9-a{NvQKiEa^q*)SWHz(VPt#P$>9ANrtY z3j4uqxCaKNqSr|{%`NVR1#m0$PIrr+r2iqe=x~bo54%Mc^gimQt&5z;(d#tZVHPZW z(k%*L&U)d{_vN!7>kJod z3D0tei@ImYCtOZ=wkKR9<1c}S@R!sI7kw+yvv#;x01IFq%&8MD#uBd#9>ZT05svQE zo2YQ{C@g_{pg%fX&|^Wr8*-pG201V*He8g*aOh!1Ur8M4BhOzyTqB$Igae%p- zln)*fC9Y+IJJa}Pd3(7;jq{kfWw=+ADd#ShZyg@V;k<2lEMuiSJhrfY`|#*|4euBp zYnZ)rcx+{bo8(dF4%XJ0pM|@e-@oO@9Lt|f&(*%HA0GSkD|~u*>=l=IFl~3Qd|uU` z*&~KT^*;M4j{EH!^A+Q>@fqX(NBn^M!x~pJ`=Ir+!adAvOwW5zlsLd93oLwA9;|aK za}UXbRc5>jAbD6Gj7`I11xu_j`>69|opIQps6FO9SonhboVrDOPXF5QsHHJ?vCMre ze%*buuKf4SGuU`LoQJsiGwTpHp0V#k?aOoK6BnL0o;bHpe{qRz-pN*Bmf1h*m&V+l z_In{cNAvKIIQf(PJJLDs_jSPBOY&#qFTfEU*|k~Gf`!w zFB1(GCK~T(>p#XkEX>HnX4bNq*u(tHO!N)cZ&oJOG6piSlT99C;q{rwXVlO2?pape zknwL|>wRM;nk>%C#DW&>-kgb*%paGDP0Ssi@#-S?_k@g}_31x96RXqsq)cpQa&jgP zu(%)-10(c5B@;_oIyDm|`%&dOapg4aSzf3;>!*9EQk$=*x2xAK$F<^O-u|)j4(l1I z-5K_Su_zPUn0sd?hK(}*yE3tunRm-G?azw1=p|_TS?9FT+MQ*;n4GO0YX$qy{P~$k z+MUA%&WELCnOM){;!Nyf?Gi7s8lzn?6ARh=P$t%}e7SWnyVAalHP4lqn8)JP_Ki`> z#1>Ysvrgu(w{IQhxj`N*-6RiYZ;=OUx5=YZ|2yQtCfBFlX}#8!tdmb#e@`a*#~J?% znOMcpaK?hrPzvGmF|~{$W0G!U1NvU3q;_OSE;#b8t%( zm^rc~7N{?>#0s~h<-=NHXyP1>Zix-b^Q^Kuyd@5>#-T@;pPejaT4JI0&DNG!B(98T zi3&4qEm2mUIjJS~i>nJ-B0tpE|0%{zJ=N=G)fdieiMic;XG^SN@~)QHr@q3?;^ty` zA8DTVv_w-}U(yl-;&@+6%kKXt4O8arDn_v~R3)+64FP zp_a%FSI>3gc(^6%X*q{Yw11y#i6yLZp?3Mr&Xd(gTcSN<{4dyFHmfbMUc1~D`^4fl z>zL%8eX}K2vRG@0oy>hlzS`%R^gEaDw!{iHzh_@q_<`}&m$-u!?qTwzdokI5aWQLq zoFj7$`!U7%zqOtg`^dTC^6#Aw8_X$>z3ve+T*idk(|*s(Kkdh9Q?)-}-^AI!+3z&w z1{VI=5<{k$mpLY{=*J?rvh*+et6$+zOYCNa9=H(6e_H(8QGc(!Uc@>VDz6V}jn(4n zVXaXT*9N!7mee7wv5(cmTVuca;t{PeZ@PX=M%ZVr6jz6~##ZKzZ1w%qejQ~zHh4gJ z@wKg9gXTPkwZ?KbxKVxXb*)iLJ-RhEE6=sGMtjzIk7$i$Y_=Jf#gXPu+j$Y*+&1^4 zy)~Ay#FZ@bfOfgDt-k*{PtKibogJ-FN!xQv+MYYo_FS&M+}Rp?#1%GKWkq>oTx$#& zX?<*Gc6@70W05&lxhSpYvb4UdHCCndET{F{nAUS^TA#GW&a|G5w4MjjdJY|>zPmL# zS!9+~&S#U0)A}Cyru8hP^<0m6=l0_`^}n(oEIzBAjo-A!cIJPp z{w?zRo%3X}S3Qf*sb}GN^E3Yk>(1GaeeM-2f7Fi67qnyJPuj7*U%TVw_h;=`eNj7B zUb0UtH?5bYmz~22%Ku_sR{z=>E7JIY{bu=Z?g<-zcW&uD;$CL{VV{|2+gs&(ur>0` z{L{T=lXcc#Y4tM#c9L}* zHX;_WI(S5^V|mDk*u~P}>Q6S_Yt%D$gnA}J)u$dgB03is_oxxEgbi+B{z`SL}bVCoDs2<#lnavvvTf;SYw<9_lV=X5pjSChmG}pjeRU~ z9xGhL8ke)l)lAMG5$jo8IwH2Seu3w*vT@;vXz$RD(-`j`5jkeK)x6DR#$9MXK44ts zwdKx(W+gOxWN;Rz4(u=2p-+b?y?!6(hnUfBZb#zAMjOY5p{3 zjzzYgZlA9IBC0EM#L7Dxr24?PwQ`$$J^w~y~>L`$m(szSMCP@vHd*r zvHg7K#^o%t!5RmaS~u4)x!rxZ(0E+N63ff1gA0vUSvMjUj5j~GimU6LpEz@`Jl<|R zu3-K?c{01%{mYy0QS+vL!Tn%^`(X6ZeX5US>Yn}@%It2iP=|2L|u7~d((D2q`Y!eTO1VE*p~S7 zVpv=Bv-Y~S$e(FH=Cs8;ah03IwPV|2u{blgEmpAkhPKEo@^$#ewphq|t}WIpFPzX8 z+gP067DK!BXFr=K>c`y4ZBb6k7qrE8Ca21S#na@`<9;n{i@D6ctt}R@^7gi<7{7Ez zTdWkvqBgIYG~c`0{F!Gxi`(K5>um2e?tA3T(%EhP{;EH7?{pu|F^;&&8jFRtSf#vl zZd>eS?!2}b)+c`^EOLM)E@1Tn>tp>wc{97rx+a+a1Ll2~^IqN-OT-PXU|iA`YnWj{ zefF}p*vJZZu~4+$ce_7V$V#VHRuitpLIREz=|5p9QWiAyrZgVco-R@ji=G?P<{oK*!->mlG zPV;DATxT8P_@wq)A1g19~lq3H)q>lu1eix{VadZJm=W|&&!Lo z$J$~KGgbRtQ2u52gtf1z9v=0pEY?H;jzEX}m`M z;^I9cV;eJ{9vPYG#^HW(VS{nF8Q`~U&QU6caYVei^RF_jf|blJU!CCN8JBs ztS@!<$VfimUbl_%9vb__72@3JQL#&$?-&)yO!JHz6^mF%M#WavxjpUI=Lu~WoA0fo zVxRKWJj5E?XX!tGRBTXRIC)eQ#3im^on@96jEbSlhxj?L@!427DmF6rHshziXA&KVVp({Z?t4c1vMjEc-;!&|vzdGBGmDeSsVkrxkItP}Dqaynu`Fz;8;^qqb_j=`* zkBarm8{Eax%26@z!{+_ysMxH&#)B+eDKF)9&Y$ZXu2#?LwWDI2@>FSPwUG5C6>}Z_@5zdGWw*BDO*nixhc$)M0GV@&iol&uhpJbVB+x6#Pd5G72 zcU1JfWl;S6d-CH`J4VG?9{D5l@r<43;kGA7MQ3hMjD5;I;VCTe?R$*NJ6UD^*T&~6 z?&YmK$SQ{&?;brn%4-VUtKS%h2@9!>QT|=z>yYaizjrPtIB(8pgNx}OJ+Ykmzng~@ z?qlYk*263_Z&lAeHo2I|E50sSVTIX$xt}a?FY7$y-jx4Mag&|%&G+i47+B%^>)_Fm z71ueR4K8Mr%Xx?;CPPNY24=aHXK|VS$zCWbSz-@{iCD*BgS7gI#!7D z7mtpOtZ|9*@@3Y??1zn$##fGxo$3o$jgDAgU#~W<@?yz2SNJ+!JKAeQ<$sGj#mTLs zW2Lx!hjE#`%f2daawltdkM^?%`G0D391?%_{?RdarFnkrJXrdPa`m;JTCX_s3;kGO zeAM@=UwTsJDZY<#5zEhxj@8UOr(AvQ$o43(cvO3=W1aid=U(eso~K$b`&k&?9y`>R zvhA^0T%6hNcYW43yWPJ>#XO|GGSD8wPSbBryZ0ozN6cO+ueY?vB5{eU=!MH(!>1n` z%pPyPh0cKsSbuAKEMfN4_E@fec2T?EZMZM2hzsYp$1dhCZjWust5>(jeimK(a6XGqx5r9m zeqle;ynm;?xb%DZtg`=m+hZy7``Tl-^75bThq(D-dklSt_$BLSvDqFAS@^5{X6Vjr_>o!42~-(r86um@spvcFHsB?B8nn za6oyL3(|I6!Ul`X-!&#Suz2?v|ITzi+|4TYv&pvineUThqMsRNjhk6NCgzE=T+AF- zFwZj2PRrBzKQ$&ci{sPsV#0lFY;c}uTjzc9O1fyf4|8f_BWY@wk2dg!9~Z*qmW{Iu_5Y^EyD{M7j{%Nlds#XR@1M2`>i=eu#)=d<`Gx9CM{=9d2ye6Ca!QBvwyS>Heb+Qeg04Cnd~<%n;deJ{QsWkKU zzIpYk_&;GN?+$WVU9UGfjy?(%(x}vi}N3oH>+%>?XPnV%j^$x_xQfFTE5KPVBL&0V}0*4FXw7k z=Hj$HSF*_^>Kix8hsB%Zu|d9T-5d2KE)q9bX7d*7rw6LU+zsvrz$%8E2ZGZ2V&nMjj?HXJmuHI|E#l`#F zTbA!P-UsCQnX$1$ec=K7C(ds)jyU&_{S#NYmGy_6cg4N@yzy9iWNd6PBxm21Sg-#a%p+5e#P zdCoZM^IR#eb1RF_n=g%Du+Qq7T(D7|FFFS{xu3!T0B^m?ie z?>Bcpujz<><#lclH?Qr8J&eySttY56@Jv0Zt5x+7wV&~_Zd&S=KUSf`KbM>*pJlDba+3w`Ug52^$;QQTU@@11-n5}li0oJ*o$}e@qdS)N*h~@fKxl0`1w%=*|gnj&?`F`O1S>`6@ ze<<(NpP2Vc#^FBgi%(jgIG*l^z7N?qu1n*enJG!8+|0}i_Ki7C z`-<-qT*k(q9B>bq z{hM{M#>`gh{6|L=m^tX)uCT6ucEm<;aZqO*WPWgG%>A14;tJ-5bjD)sDy)hdqdH@i zI5WD_&!2pq(Fa-k^Sm>*h_mgTkrS7CI%8kjkB3<8?TqAddG>Y2d{((xyO>~nah5y9 zg=wA9{tff6z{>Q_SjWQ5&Zx6C&>73MPv&&`S+9Ov#~QaWb8KhqW`PINc&>atBIXk2 z-)LS|xPwjZP0QcZ>37?{PTt(^<(bD&KUTpuMaL{a+3C}avkI3PVc+6FQ?dlmbitL(>h~Q+J2$D zS>YjOPw$NWE3J?7S;-rRHEv>^+t^^8P3~p9!+A5qVOJTK39}qvjtiLQ5*Arx=?wX> z!X0d!ssAeDywf?dw%B@EXO#_ZXOp{A-_z-}0opGy9uux+mg|}0X69LAfxB4bK9+ch zWoE9C2m4s%T-Lad&G&W28s^UK^u5?P+{Q|wGxoE_q1VcfeT;M66Bb!ylNDyp(~pJo z&CA@<&d8SJ$N4O9F^gQz5=$&|9V^_#5eSKcxoIWo9mA)RC`)FruWc}Js&$=?+YU^69{uAb9 zhRayK*}Tl(kTuJ4Ss%zUadwz9~*tTXco=XQ@gSz+Bcaj)^y z^81X>DwA&+|1jeoOz|6t$Q_>+6h z@_y@M_GMo;Ywh=6-5-{@oHdqMJfJ;Gtg*^nY5m{r=PmY$18i^s;~&uho#lexw%E9WNT)=wK3hgdpq zTqMd%7ntw6^5k-H{=#ulV!U5oOju)}f1yuyR5aoC;u zGhyah^QV@K$1E#MR+~4C8S9M8HWsdzM;bH7{3pi6lGGdI&FUKa#q3SyxywE?VU+`{ zafQ4}H(S5B!j0m>TH~bcxJz8U#Xd1^v#-jtW#|8W{q7hSJH=%-(*B<`&fW4~KQ5NB z`6>Ir>OK0i&UN|~?;q!9Y05c``HKF^n-4fYap}Qvp0lPuvpeMTusqV3&9wa{{nwl4 zbK^Yc!F_vF`?TE`?X&XSm#shbE6Q2;s{Lkp>$n*D1K%&U$^TRG`PMk!tK?C$Ugfb} ze&PZ*i<{iX?04NCnR{B^ zpEe)ovHVl}&&)38%*xN4?*{jZ2}{pd4~xIH9@d|=-)uJIaj){<8i&=r_JyVA2mw&s!(6fADpuyv6}GSW4^v=w36o-?=F-aamgbXY;W6^0;WfUwpv&So)j!c3R)x z?K|__!qPvC|6}C`$Hl6&9k;UhPxqE(dgGM$t-80Yv!A(FjLZDLoV)(Tf7=IfiCb9Z z4mMa*Uw+lTrsX`8&gYX0^FMKJM~wF~Q}eO&GukuDCg(GTj`w^u`^@Fcvcx>svB*s< zaU095v&MbQ963IQJfI)jnd3ATnPZ)c*gR@{EKBQ0kB^;<&heg~slID`%w?fxd@M}u z8y^K`Cye*}HSM{Ed2V2lTUp{xmf2v12h#e9<0IKt~g#n43C2wy?}Stg*>D zhdgY&W9%cd9AJ(MSePz9)@R5st6YuR9hF-M-va0j#8!#tZTa>(cOV>=t1#yHk~GRsBGaT&{8#VX6Jb0eGF%FOGX zC$nrY&jV>a+dgmHxz@o9=P_ZPSuSOcE1Bn77Fc1CTUh1}CU39~*4Si&Lmtupjn=`; zo2-Ln=F;+c_MORb?mw%nvz8kl2iZJ+d<@&H{R#46>8<8xZoc-ZCu+|;_p-o)Y57UU zf7H58)}A?5Sm74dxPzGm@=ZNOzD!PazaHah?iUN3%OV#t7W(>OmMd7}8s<*-^}{@? zEO0wZ+|4rgv%>g-b+V0_x7l}=nPY{E(t0jqovY|UU0&0xo*UD8Ze@)-Q{Q2qtIE%G zeylOiI+rrD$oa9%Dy#35ACq^JZ&+fEWiDcc z%h=#5Hd$uo0{g@)w=&0_EVID|4={6~`?ZD3d|zYfBHtHS<^oo@gjE)qxx_lY%*)&h zHo2PR4_Y^?+`;^Z+{d(@*{|@!@=IM|oy>kjJ>v@d{Z;cY$KsXtlgU-?B}?qzD*si^ zk7aIVjk{R6TK-?t{u<}QEEhA+A`4v2GB>34*P5Rt?q#jy9)8_^a2iY3`Tol?ceC(u z_1};e`&s7#=2qKxmbr!%ZeW#F*0?<_zutar(~nseK4Cvu;cC{no|zl0i@BS8{j$no z-_(x@8ysMh3z)gtIBCoZ3v12CI(IX7i~GzX<6Gvt)z<~9%(KDeY;ra8x5=AD?qr$! zSmluaQD1i6EOQ~NT$YyK?$2?SSY_O4zM6d4#^O4E-m}K7%-kivw4Rx7%Y%K)+-+PI zJ}IBHe7*Bw_EYwQ6?T3{Ip?u>kMBRsecHS%a63!f&CCYtPsnqNWr3Z}1Dz~%A-D&-U#$$m#2*&d!<;(oT?j>WBd&?>-Y<$kw1Iv&2dSGd@ z^|0`mbNsIHzG$7SvC1ZQvhXGQ&N7F5PdWRUf82SeG1oEsW$jty0hT!Q`}(nym9IEw zHoxWjACv#_^~YjOKCE*O8*H-4Av>(&+rHnh@Pu_S*=}7de_wm%cgW`l+Wo-3G4~_; z#uBTnaXXvb&19#2OY0dwRL?fXkB!3;=dr>(^H0h*jqCQ2g{O?eGMlV%=#R|%v~{w= zY#RU6*FQ^K%*@Z+Z{~mQ{;|%@Z2ZdKqu69-r#$w!f6Q<$6E0+y1?ISdd9GoB8(3tO zC2nV#yIEnARSx~Je3`J$xoj}cCYLgPZJ(LtI#yU^gF927^?l$c#{Z3RSZTO7%>34$ z`>e9_NqIkKz05zaKMQ}bPFC4pm&ZQ)%*r3#OXgqj=Ky2B^LR=-&SmD${#;?aXdafh zg>^Pqd`Uh}+n1*AL#%NHlb7ue>#U~nU+fE;9P(4+|J6EKIN(09$qMU#vu`XO^!<02 zcFeK)s(%+S{~v$PVI{g^*v~kqD;6>t+!d>t=Xz$2=<*B)^+UU2;2Gm`5vxab`I(V< zhjn?jgK`f0x%QbZ&r=h(bom{E`M8~hR_)l}fixb`6@9zye_NO5p&5T!Tw)bUx)cv=&x%{rJlvny7z#4;O;?04#Wt^fDNUt<55eV=hy<+e0F*Lm&L|GchP%-Yhf zSjWPJU9p$N<<9##ULp@RF71k4%ool3yml*`8>^SQA8dT2D~9|*{S{r&&dNuvpN%W+ z6LYKV(?0dr*x%HWJXqr%Rz7AN#&yQ|qw(3E`f>M?jn(#%nH#K+b?#;9Cg=8oJa2bj znZLt)tlZfZL;qyI?y?W8Zs>{<3mdy)H;WHj$A0r|>hhc{^`GmC9ZVkSii4~@-WBuz zEdH{2ng6Q#)UV0wMfG2|?~HBoXYwuQn#Q#*&muA2ckDZxPuM@k_vFXI4)-^$|ABLC z%HxOjnYo?zg-xzw<;T8WSo?|fy{z4n?h6}S%Y3~nHZt>+b77ss{$jmP%bSIt%9}M- z((+x_&G?yf_^bIijYZ~I<`OnpX7Y^jndjEDp1axLkOS&}ZXcNEG{zqLk(M`{Pa6N$ zI$8a_uj9X&=Xv#Pyr7=-KU@Fb*>q1>IpFJqg@elfVZK+)&&I!;<3Z>AZ}))3SM4XW zk;FdM1|>1@Py29q5+xRnNMaq!Lz8HKMIJ{cp7&(EM|){QB>Mhk+~G;A zWT7>QIHY}B5;^8ZCb6DH)>s&oc-FFc+mjgfZ~ew5v4q7=^D)LJv5D2hxUbsB{zR_Y zAEO@|(-Y72(QjrFt61TBHfAMph{ZX|<1qhDPGU#u8xo&H$>ZeD`tkB-<>bV3dJc=+ z!o+7Z9u~DTlGwn)dy^P4SpC_F=j|Nk{eVeqW&V8SL-fBWiPda!Ju@Fj{94>NMdKfS zn9n;gKAYUa%nErh`A8C(*J#H+R?oIZQwOgGFtG6Z5c@%F?qRi}FiRbei=JNp(uN{}y26?i~ zU5xt^&+U-MgZ6=i&n9tzxkr;&`#SS}!8%y`lKhVrKkj^4Vj+#U*bnBoIW6Z-R=Ahh zFUxcIVg5eo{8;9E##fS9&V;L3;O4ZR+gaycHhGB2SKY^qd6;FH%UI=V#?~aZvBUbl3SWoM@FO9!veXaKU`_{+$kF1~No!YbUlf-i@j9*VYkHYw@ zv;36%(Wc+i&Vjj~*UO)pIWEztN86hH+VXO?M1ycm79oM?XtPb;nA^YrA6| z%iNketlLY{^?zM=EMg4rjs{UcE@D3u?U?QEj_u6!bjNO1=>vNGyt6wN zvc^IhPwkG)tW4{U1FTN(j>Y5E&+PW>XXDSZ{x17CSH3L1Q8_bjvYy1eoX5hv?pT_} z$94N&WF04TM|-z&4lsLScWhvCa(C=wW`Xh^<)?JVY8KuhPu9=qj%mHxooOFg>vA>ysJBwv-ob~v+-W#6RhJb`LT3%cPwM^e0fdOe_40zVEz*4IY}Ou z+GkcjXn*?k|FC^w?ecCvtK~=J%hDCyJ}XTBmEEy1Ex)omhE9?<4Bb;l;=*H~}rP1Z3@|F!aC{ucdNWto|>ywaElncVI?j*-V5-7%l_d$eQW zKKF=CW~Q5`;vO;cfcwD8gXWpxUOprbW;b=mdRDoE%}3n7taE$JIkL!=ta2mERqLFo z{!8|grN_Ht3+r3lM>f8!|19NScdwZLhVx>4Q~oS+KMUXPj=8ht^Ihl8=1=7{z@K$T zj+tL5XX97he#Wf-@9gUw`}_yz%fcU(v;HUhaIAU$Y#*3;(Yd7YU){gd1M+#jy#Hog zHn@m|f5@A)gYseWigTUIL;ADyZ|B3pf9!W!eppW|c!PLIkM|=Q@9>`3$w3&VP16SGJ6#IQHXr==&BvDn%Z8(C}X@%}<_r_a5cXI;HLo_}ILCusj><4x*` zl`Kx~iS2Am?TNPI4vTrm7@v90Wp#Q_tYPWcp4iRu8+yDSP@Zq@i6V=~$uo^}J>Cas zpLvj_fi%{9(-JyB(Obx#~h%RkZM{bu^F>G2#A`QBvTPLUrMrIzh8^Pg0Hs`Y=$ zdRV_-zRYh_ewz6nQqJrnJ+Y0os&y_j?&Hpjjj!~?R@T1S<9%52;*isg^ELA``wjcY z!nb;&{cZN)JMv-U3FpDwlh*fk`90MWt62G&ePRCRJ)UQw{1^6(6|P|JmpxHtk(*P0 zWuKVr(e54Q;W9RUEiY!Db*?NibB6JLW4~GDdKQ0cKUw>o@-ywWe+T=UT^0J>EYg&!%~p zf7!mK{>A-D{i}QYZs&KvJ>q}3Esg)y<9oGn54k64{O_Kaw%EE~wQsC(6YKx6-qh%g z1@BQmsMq^)tdlEQKCCx3vc_$xgL~s3GedeKd9VBr?~NjJuTjs+5xsGMg`vICewKaY z0Lxs+{E^zT$hB!bH!^orujebs_qDyTp9zO9(Qa68EM@a`yp26?=k3n>1Lix!`dD6M{M5zryI7v@>5bJ)&gzX_%)GDHYgF_* z$2g4h<-y$hm0zO$MeZXr7u#3nFOe5(m)fUGjr&3Of%y-+Czr`*g?w2q$(MzX+XvQG z_j=v3ao4zSY~I`(d(!xpUa!xU&u!*q?N0mrA^q<%4r_P2Pb{pLZ(6>=efqFG@3oIC z-QOGQnLJ>fY5ZCDe1-TC`Lf7OEIsb)g^jOU&*j#$tv6ON|1J43TkDO^kEnmbc&vWU z*VPsB{JwEm_(89KlbQdA*16LBKX%_)ezG^Vu*My%KjmCLYTjM$W9rYHCo8+{=at4~ zACq6X@2oxR>ov7uUs?WxeZA_i7_`rL%y1=>{pwl$v-PdAZ~v4R8?QKbmZC2@ua@VL zeX%U{b$zje#iRS8{~F^A?~9czw)Dkz)<^Vt?T~rf`eG3aqx<|FPM+<3UXQ5#n7(K) znRjeoEM>N%&wB#R+o_)UaecAiWAfz^7RL9*I@Y@SVkhf~{@2N`TYolt^ryeXdYz&6 z6Z&G|$CXbsFAJ0VygyI>DSh6DXWr?3v7Ci$UuFU1N%`*41$^P5Sb8TPDW9?)1jm6dKS^k85DQkB_Uup$p= zea!q&eOkV=FXrB5AL`bbx~tFojLg5=`tP>RU$_@6{ZgJx_Q>ax#{aeaS!Rt@?qcIN z?!|id?G^d4bjW>WbI1g*6;*%4gebFgGD?9mfqFJpxId&HwB_#8_4bxiR2lj%L2 z;C)2ecTb2#%=Jx({jATJ5c4+3cjg4Ild=!9wP$(G1h12_t~V)X@y*&ZciebJ~zEM0E>%wKQZjrQfn39*ySoAv+fVe#v= zd`Qey;_7Yk5hu4#i1lgtI(a@Ue$05Rd~t%;$eMrmgqXic`{yRaI+piYFH241Gylp2 zuWKRDi}ePV28Va&uBRy8jdGZ{P4>oAmaJ*%uS(=pL| ztBgBtVl4Qg@_zL!&YT$gSeUK+OUef(#wKRwn3oBMKCa!d#%F_TnS1@j*ut1Q(T5=_ zf78TR#!PNv>}2J{iP8CG<)=*a`UUMz)t-fg6Jy9%~($_I<1T&Yu`dSz9{M`!FShn&^ES%DIA><<`yOB@?6X>&CfkqGyxK z>qE*}yxe@>(Eno-eIA_hkJ|?(w>bZ8`rkS+wlQCx82N8%f4g&G^)B^)BcP0KXt49`?h{xvo4mtZX9O5 zp>z3;`X`mAKINV<^V5m3ca`r~&TP|tVe!DkIK#e*ircGeG@6m2`~&yYzzk4(SUOo}42M@))s%nzO9 zbH9vx`{|qEeo%m6noehJt>y_P`j~{VlN9FlOp+%aXKgYeX?^Kr+peHli~oQ zdr~aeX}sP^es-z8UpdRuCPntg+Rah_6X!bD{LH-B{7>4yx0;9b`IBNjlQSm8K9=7- z$?r<6fAOSP$LxD1MU8dtW%<36{0_xn{LaO`-#aNbvwYvA=-g%A%A_c<`k6^lXXZiWKQlg;vH6ho zG5fIcXSCa_JoQoQVDT||GTCDNKNo*_QmkU(tCM0UOI!8ZEswA3$I>?@`8}#}w@>nK zT>JNZ=gH>INq&E-U44?D)f(@aNip}A^8B^+GM=3jyVIDRzmn%~C&dO9em5x&vHrrO zSg}W*FPfK`e`xn>OZ>Q=RBz&)9;x@>bv`6IU5uE zV@n!O?)PV^yr=g^UqgAe-{(1TR)4HveQv+k2D?9R?DyFq*8i6N*v;xG{jvIY&gHCr z&zw!qt>5P{Syw?h>)grAx&5)9l@ImD+`Zxz{jrj@mGWYJReub9PQR=Bqmz|u`(s%e zm-?f^%&q2U^S1t2_`LFU`Z0I6et(e9XZvF@mM~aHnK2UIWuD> z$D)_)6DzEAC{Gc z|6QD)9IKc;V{+_GV-ER;eK>1!EN1y^^RmM2ta3LSg~>jj#kfl+#~KzdoE*dcX}>O- z>^0xoe_(QKX5(V>rST<`eV&Z=mreHG660L1o=ItP9Af!q`TR>>YbVEAmhU#+A??_o z`l-pWg0&6uNWIs3|84xwP4;_5`}WPrexGQb?@W#Zso$F%xmS(%^klCY_x13{$$qbB zU4PQ=Ki0e7e5|}^eayckPnMX8!BJ&D8_Y3YwofenWwOr+@%c3eCi{Kk;3y295{sEV zVoGdGQz&`R^E7A%O*E5w`xjkXN^OK>VLI-nY&KDtY5EP>Y6EWfaM#F zcceVlPKnLT-a5szALMhVb+BHU;_r5YBiT76mcCYA&&Z3}-%N@2VfOcB?U+AkJxpS1 zB(D<>ni|WQIdQ7bV6fiPrbdPNv!?nS5cw>h8Y_77$EL={jP>w<__~{>#@ZHnal7*T zol|2~Ynsp0*sMJBovE=w98XM*UCjT)JR_|0DRJtqsU9t>{ufhY8*{&$8l5A#XR7D9 z^Vd^jH=BQ$8goYtj{9CPFRNV5%u7@KUT$z)&&@2epS6~0u``WFO^eQU>mEJLv+C{7 zm}#+})rr$$-WdIlndbLzgX4mk)BN3Zu-|`A^IT~Ab-^^hL$^Q6rbV5F6~^h1|4q|k zHA{CZXXe3ap3iI@U!NB1nE&py*vs5g)BOHSyJx1w1{Qxm&G(|gvHBm=;t$f!TYe$3Esi zJ>BzE)o+;Ya}}J+ebYS)-#8CUkD=4Fe{g!tXZ2zEvG;NF9HSqb;@Z_SJm*yZTW7>B zHhGBkPtAyh)AiplBQ`U+e?}Z+mcwRP7w0k_&_9j2jg<#y_%I{mZJZJFSpMvc*ud;V zGosGq;TfJQD&90B)-ZY0yew3e&vMVcIKy-P_1mH!YujhUKIWdD5zA&9f7guI$>z_D zKfq^Z#7fqFJ|i|U`NfRb&jyn@+W*RUEbo~S>)7ND7N4Er`J2}H8}-aKtb=84X63gt zV(9Db+wW$0ub*+BmnR#4m=SGr^?P|nEM?)ZGyEB6{sZP^=I{394ff-e8J_FUf9c2K zp&8NnM&rLaBTDosG_j2p4tbOD2WP!k+qn$Q#)dS0Z8r9?HY^)+=UM;jvYzK}JZ??n z;aR`Wl3zwYCau}n#KxFx9ALg98;g%KUuQNprY73IMc&=nSiPQCGh-KXeKVu)ba_mi>Ae}8JTo@4I(??+w_9J38<&{>_ZttgSHbBK@wM={17ZvC4Sw)c(4ev5AeFW=8+J%yX;$@7Dh_ z@?d_WJXn2XW(--Ze9O#O$_6*F`F;EH9{clynLg9Ue*DPzj3=%Cz4EIoXXB^V&&+P) zo~3=mxJ;g#8Es4C@%+qK%-Wx4#tv3qoawzE`n_a*jDz-xnSaXfZ0C9OEPt0!-ZCqu zox_o{VqNN(S+SSRDYIf}!8lWAMTOO4X2n6~r_YLkbLBa6R&3_^C(iO5EAz80^~zbE zXQkg&+NbfFS+U@J=X3L{*vdwERwPU1ahHCqaW|WH&x*bajJt7GtYGG$S#f|>PPX2nL9{yxiR85r;2tXO%Gb^UXe_llVBU$Y`U zU_SOUIW)_&+V$h|)MR!XVxxCQt|1tV+C_d^iN$XzD%Cn!14vNJs-w-zkhZlMdjDbj&;mltDO1U zjPpU`-7z~>v3aL?c*DKo58DS0{m|fe|1+~=zqrH=Ebp1^c^&Fsm>o-4-ap&#OXTyS z^2?3$(rnMs7#veyneDk2^5QUlYUV&3yu!MhNaoEn{RReK|$&CZC`J?uC zt(fK824d-zgX5>=0k0)V&wC&?@|ODsV#8JXGs8!|J`nR(Sr4~~>)#rPCE~{SjmOfD z2BPh1W?{=g*1u8*T}wUFNw-ySwLjO`vw`=Xfrh`upd^PS)9A zg9n&>V27%kdvH#yVESL%fB$d9pexUaK`S$ZI))uFv^>Z^eU*C(L#(;+@EG&I zZPGG-!o`7m4v(e(PuYC`fARjq!{7e@`(N5_vH$I_93H+S|M$PNY`Oonf9CLb^ZzOO z?;Iun|8`}50`uSh(st|oFZbZ#;niRN{m;MFW0U_aQTEZa+9$pK@Bg3XyvzTZ8xN1;{%_mM|NrX$&Urw2cFW;0BCYpdI_FSpUjD@4(emGt z|6RYbgTFmIru}bOI(Amsia#74x29$PCy#569_s&DaP-i0oV@zI>KCT<{~vX409IL5 z?*A{&Vc^WboQ^Xvb7r7A6%{4jsEb8Kb!LWf&`?oHF;S0-N{WhxN{UGi873+f71dan zbBnrIRNR`1Zc|cGkx`8`72U?7?qps{|!d+oKk8u)7iYkFjzx>A6h@x;z}a%ViXGhVqPUcI+qVBMYhcSY}xL>l9j z8ioi9^{L+BMI`@mWKH~Toz|t5|Cu`f)8nyf=(Es=7S^eDE3X~#qYxTF7ZOx0?DiMS(Zl@)z)>oEwXE4K?bL>qqsoy zAbq77o-f1md8-XH&neluMz73s5=fdp`0M|@K>aKCa*0h2gIx@^NdR=GWpFF}a%5Hp z+IFFF2{sMZ0p{-0b70+Iw*L~ji(vQNZ}GY0RzNFWho6Dg$-R^3p1rhtq{%L@@YTS# z2;cQ?z7vggMXrDoT=Hs#ui=vg+K)Q&z@@z1(DsdI%O==>1ph_@YsG0z4<=&C8 zN4$pE^7VGHl$2`*-lOmqy7lhxM*b!xlYExooBgzn?G)&@-e7_(tI4QAto4>*E<%7N7!t^-zI@`ZT zZWm7{_B9G$4}1s1{MNAet(^?Q&DuFinmNhu+Xd=oOHclBIC8(0BswXHwJ%E)qmP*a z^;kXvqBrH)7C^MHcby4=q8(8?cG6u^_m>9Jm4ClLHM{HKj6FS8MknjCrQqp?zY+dt zxyz>gf3!3Lm+LYHRoqMA#&?VduogRc|{-FZ(7w+ZK{ts;SM+NHZ0$^u$ z>>#v7Xnj8}(0qPc@kaa*G~1`T(^WznfOdR<%z9`agytVRtKiK*n}Rkh4#zHwdwi;l zTD37{AB4Z=X9bEbo#p3N%033I653DQ`T!R_Oha4vd3H=sfz5#}gC*VkM#sgXW3dqx zCm#4kfqH?ocToA>BIOgiioA~-omUNM7Jp@p7bL6XuK}!hzCeABdv%hf^Hx%P<22IV z7Kyw?QsaC1>w)FIRx>E?x06e4c1jIzAiXh2n z9G-c2p5Q*4&!2QYLZ5{`MUdl()pqZVz8MaoFG62}Uhhs1{YK~#%SflH_#lk} zeFA#*pTp^gpbtXt@}$37r%!<`L0^RaIyW61M0RKvt>+|8p!|to8KTEp=snP{bIXWy zI%RBuJ{OW+{6aVMy5$1(5V5$^jxIe%SMrf%CRM^e3jf023)H)1f@9lHQ+Aw-p1sN8 zm-aXef9r9Dd{fuOZ_QDndaN+Ar9b8~$x3W4R87)%Y}vbHT#Np~Ze%G#6@0Z@;d8gK zy9_1L#zwCAt5*11wiT*ZaPP?FR%qSOdZ5X@pEd-o8`^JuG@4C~Lu=k%=!u!9z%pP$ zm#dn;!(gpoJKgoyHYDYX3|a*}$u6+euNd8SmletpeAzOcoYir}9(W*2UUl#vEiY6z zaxa(2XaZ|~dZAj8px)M{TSxCJwX@W|uowP{nnG3R9v@D%$N%V(XjIyB6u#n@7b=!Z zvvM2FoHKf6b_|hn9fCLW%0fM!^HclMqhJGIcevZmZEtTj{Zr~tQbPZzD^wRqV!IB` zSbmdL{+C!lxc^<@Gij1W0Y(i+`Ro(0U%4AW2N->U4@G6I)VJrO0R9pCo_Mx z+tRVEBa){0YlfyyEL4ByKSzJ!Ulz2=bezx)&m25Ikn)+aEnp54z1}x6u|8zVdw}#k zCl{*MhL!gTyXRW%5C1?TZR+N?{lSZ~b<^#S?H)X}P@UxV2QJ-3|B&Su+pLHG$Qgy| zQnzf!UZUf6NklfEWK8olB42eHMr?Tm{`s>C)$h5NOJwkQLv`@% zLUl6njiX<8AL}c0WQc#^bAIYbTcLWZeF=X?@O1 z8%WP*M|fAaP>uW32dWh(KbXf{aY{e4*~yPL>$@G?MQ7<_x?TtK&T;NYCjD|AIU^r0RF82l<*?Uy zn3X=F*LX-Mc2WFv;=fN6sxMgiIl8o;*SPu__B9d-vJ2#-_-jGdug412bGdi=-RW7M zDmv?d=Qr@YLU_2uCl7)Z-Cw9qk@ldhdJV#rZ?slMQVKUlBr%79Luuu)YkhENqFUN|pC)8V( zcCbko)&QRz^EC}t;m_B$H_tj|XOOkYGN#Uxqq5Z4?+TT;0l9Sj!A8NTvc3f42q~4n zkoD7o<)^SGAC?4Lc{H1rE~Ki!Vt*)96H?y#cq;nbJn^cj$nEi%v`-`SQ64b7IbYM$ z(XM>`OhWQ#hgST!S0_DSNgp-{RsqIxj=m)A{uJf&@f`rG@?kSzwXU?H<0D`-VE+gm zbKHzd{@Gi$G*}7PM)HzN^5l3I_AU|!=JCVvdtuLgL)x{|wBZP{=6!i`dMe1J5PPI@^C5A5m}ON$s&!gPm;Euw2Kjb1nIdq zFFSe1VShEyVeg3dqDX1C;^*ME{#vL$$L~OSogQ&-3w&PBQ~4ChTaZ=0E~ai5S=o3z z+MB0iK_xsr@bnkN)O&qAgBFkIXat`8Sj^0M`7Ptw1lS0ey*|R2c%NQ5We*5yxJEji zq@9DWeSOT+Hj7|wV3MU=!k3TG44AvEd39WMfQ?ysQrY;Z% zLFPH*)#KZ3H?PNpPWZo4NtyCP_#c({)r=qe;C*iuWr(_Xv-8R>dlp&^c^cj=)iJe4 z%4?S+a<-LsJs<#bbgYox%dU~g z+w53jM?8J8>t3g8AYI3+VoJt7U%Gx5uP2@8w3l@GJV^brq~ogNPhRI82YZoK7bkA2 zj}PYAaZ}{+oUlpQCH*1NkGwjjj{iTTm%du^JmSjwm}=r4Kc9}g&iMB#@ZsGt-B;AL zGLJr0?-P=GG(ekyCN?3L=&}`Tx*?{%BY;#vBZxn3u{Foez zu4TE$XdM?iaet)WyK%~j3P~n)TSQJx##^@(IIm`^4)8m zFg$}Eo@*Q)k>3PQ{e@on;;Y)hdj$8F-yZ9IzUrAF&P9Bc$U7kNE{dts)+p~8$B-xG zj#VP>VsE)q;3@D4@Gw+r#w7^4v)xhhi3*Jw@szMd%^QB ziK#n0X*-;>!m}TqPI&I}@LcWii2P}I=HT(mKLTEI>9Nb7{{s38JYnUR*oIg2)WGF2 z^(N6bPne@u5&LjkEkB_xUV$GGnwdip70LE4y?r72Z_J?ZBbp* zBS9QnacxZf-71%pc29gf>w7NqCmt7e@jXqXufHy)db!WeC(U!X`goH}&#mo_Z}fIk z)}%z25=VD+$ISe~jGLlJc^-eE_8IUo#J<74mo!tP8M%pZ)XLNL*`ztg=EG-SW#3C0 zUOD5NbTRdxk_P=#Pu*sEe+SO~2D{0$`$+3{uHxbEFlh#J^EuO&?an6wUxqYGq$!p( zTn+qnfh~gRH5p?su04w8A^XiP4eKgbpW;P!$Ez8>__hlEtT*O7&)dsJBMAa7(t9S% zo**g5QTSTnlcj071X}@X2NV02ORz*0?f=%8Iw$~_l(zzGABctHWwD|A%)N-h7WOI&n0rcE@9pM}sCvmE<#z zoXqX3=PA627cu^>k+%-sX?X2+bk*eycU?q>?eMk^#MIl?RF~+k5Ow-F8RdoqtbET}`@d+vPkLKh-t0HLb@ojea0uxh<(f6^q`n_da!5 zVrSjR>KKVR>xB|N4uJK8oxr{H$-R;Dv@OeqfL5R}=*hX5TC(WIH-*~6w}b5A#CWzvuu&`?WZvdP5bLJZh(tJ(iP+!|`;ePrwqwzgCF(a?@sm?#f)Q`lLxI{P8V0DlEb99qIekGNh zL;mhy`xV#j+G7??2HNk5@uooKBVY0uAdl&#n0h2E5AXcz%OQDmhUFo8S|pF=KgZMp z_mB`Z7c}VWOnY&t7B>| zzFw__)z#bfmxq;2>UjuRgC*SkQd#xQqFz?8zJXhN(<+F$aA{|s3#p~qeskL`>% zo&8V#H*%K*%94Lajm!i7zsXCa^ZJX71Gm@l+R;7n^Jc9|ONP%(r>gdLw59XPwb;=mfG7)$7%_B@b7- zS6f?A-|cF5dFVy8)lt$k)~r`wleTx#=%C(AQl202q>_MM>X>>N@#@RhD;^{0ORyTS zUa-v;Ch=nf*Z^3EV5|*eTi7DwH9y;+9SNX`tX^m}wdds{ zxJxt?{=?L-{^i62>GfuhCwqOP?~aPe>$+#4)zq(7cZy%DXQ?sTzfOpvmmX-NC#_et z;twRsr@+2D}|y^eUJ9e*~-zEUpoQ zErDggHoCAFif;vT$3Ep?Ens4@a+UK}4b}|C5|i#jc8o@-qGO*X=p`3n{~{=;kD+&0 zAB!%AtjW%jsr1KDP|Gf#BAdizmeaK^J;OK;uCo4a|vI(}_>`*w&pt+y#; z9lT||;wYlvHt^}~eIXUK^;W{B{mxt0oBh4HHtdgeH-w05k~YX7tFLdps$W~#LLLSU z=B1G}hOGYE)^nb;ubw!0F*Iih^eXStik?Pv8k<-|#?T#4y*%Pu>gt%E-?`qI!%2Oc zz~*1E!94RdgU<|WaC?&=be*h zhb(5IoFw~M(v&o8F#9KnLn8N^Kt%Mj04;gL2K5&1{pEBmOrEz|`-rUC*V5j|y7Jhv zR(+?N;Yjr%s}ou8k$Os*sScfn=Xg}t{HxzFPjzd=`8N&wLDC-T-k>g)v^lzWt+z&B zo0p?yUPP0+ivFFDrw1OZu!GZt0(?W3KOstfn;n1zFd5WaX?kHLpd-`;aw_ ztf$&ut(4^`vR07wMJZRVPxr4S4hD6OFjpe=ZrFi+-Tc(+Eo-_%$V%_c$|7FH5cK+9 zyB(m4txv)iyN&+Nz3~UUK*Mb%e}|#<3~o@b7n-pPk)dOKp659aBV=sP-%0!m@6&_F zsGJz<`GJmB;%DPin~^tIP^50o<{dN!w610YI{rh}6tXT3E8C(sCaMh?j29`J%!3w@ zHy8Bnbsty@SVn-*HayXGf|S?j{}AcaYl{M(!MOa~1*?@y>X@ph zEXbP3mLaGuhrF_QIN@7A7Mtuq)+n-88&f3TVR)<3MJgigw0eI%JE*_XskQFrk+q1d z>w?!^h+PXjG5@Z>wW{5)?Fq_8#}N&?ng5pr@vaM5i{}(M&o9iJ&Ntq? z!8^UjqI66!PTJ--6shB)2y*&_gyg3C{ypOA4%YZKPnv@l6g}m!T4J#3J@^k~y@-3` zb1xJ(k*KHh$vD#leF?g~r`|VT_1Qt>k3nr0HqKNde;oM}nIa{}FuCiWvFiU}Z~ZeR zu+5W(12`3~Pp>oFOj%pFT>-BJzo>v8Ik5waktEK`V#c@UpG3N=i=21L5TTM_b6|Jn^CPJI-nnyG$O$b?Z6t3)R;sH=eIi(vf88<4 zX75@;FFnyonTU&sa5rO7p#RZ>R3-V$kk8>8SJzWByvy*m-h`f{OgU|@=aMF7kM7TH zVhP&6o*T5wnALPL{_B<^buRZ=`?TVY6YaReJ7%R}?I+zN>Aq#RTcgCl>4-jWB7JV| z4+Gt#8XUi6gL8Ke)D5P0?8QKW9o`UQ&4rWeb{yrRJ4W3`WIAkEx6i`3atZzhY;U<%r(3hdFK;Ol^ zX(#5l)~r@^kUWL><9)!$er5{Eeik9DeHMPata;S;ZgD#)Be% zOyq;dzcRnojl@ zoMg{I@KL(n_3-w>`;Ukb2a&b3hJ2-rlknEwyLuUg_b9wQYve6Cjd)>=ylHszKeDzm zbimuNM&4m~2iC|t1@G({c^Ba=xo`FI(mnY0AMqo+P0|P0>uDLBFHqx*mIr1Y{N5R^ zwSt?xv73;k4LU^L{_MQaWmBzwn;M394BidF@wCrZNBkZ^PkF3GZKK1xdWL2L| z{DrKGxX+H8K9%U7DKm02D+}5Sl_q%)vJNBbM;=*btweA2b>gqcS8~EH($w*H5E)$) zMe4Rd8Lsso&aLv#$ug4W4CdFQKRIkn^u{?)57hH9_H|T~MH_jH|3{JfwB+aVr3`Oo zJ>ny-@dTDC{)S0Y_qigo{^?3%`Co>Y6y~D|v3mHY;qQ5Ilnyo> zT}H3UjaZ~FcK%6kKM%6=R+6_NYY>(c%?(Gx)dR@j^Y zUnDNeEx&(G72S7?{Uai81$o22{l~~Fm-?TDz5XFMrjYzPp!Y-9@BQYMKWCls@sLTC ztTWaiZxVS2R-W8kL&~{Cx{(dVfw4~tdI>@7Vo|YqzlNwv*5g@9xK#fOUHnrmJd?%6 z>Pt=^YTCs717ZkeECl+RySC3 zeKFpicyGi#Z;pH;$VhQ#;s(Vp9zk9Whk8B0eSppQZT|isaZ=w(77S*R#p)rEBhM&! zCxl7CE&5;PYTO$HviOe{_y?X{tO|nb;m`M5LHWX@_WL!=8X&qSiwZ+b} z4woEfonH9c;7i;{`9&9zJ$4+W=l6`s%=JARnWOIr9AwQiz;R?v+*GWjO+*)aBibwz za@uPWXcqbu^k-Xi^GcZMFALBP3>2$h3(XlLeLnpmuTO7;wc-uP9W2g$$J~CdIO18$ ziM}kb{5#?!)IWo~F61TdD^~Tvw&9z{oEKyoyXlqx9Yl~G*G3+P$zz2)rlcd}+Kf-- zk+%fN+8s|viv#D}VwuLy(=2o>%@pf%mjm*Vq1ClLCGw}BeB>qGZsZLhtNX!XH58^7 z&-2^h?Pa)A(H6dm!Tbxwwd9>I66ifK*vM;+YnRBK)U0q2JAvhl?M;B?wNxK}ZYrkzi z6x^I?-QN!)tBsvpmssVHqq{M8ozEP!%GgH5C;D7v40StahSWDL^{qUgagPHMFOz(n z_Ot67em1tGZ6j@lv`_X-?mE(rkaj4&QN1?m&ezzDYkm3!?(;45gFAFBKJp0Z57lo} z4|8vo8(*J2$MpxUd4$+l;sSa8vT;=#%fMR)Z$slo^?t^cz`pNYzaxwYSk~3c=>Hlx zVu|iX$fNJ{jcSXh-=KGy0o%{!`&@>mE8nU8>>O#^&)le96>MjI-xT;9X=gkW*&9@D z%9i0T%|G?c8`bN>^6>bqt{_RrTvL_4yq`SMuit2%x$s-Yqyu2BVE5(oBiFy{>9fo6 zM(0BO_#ow$J{h}^cT_aeLGnEHkaM{L+LD@x4K*XH`n=U`Nb1%L?*zOr=3Xu- z%MjQs*zfcBv1(a3J2SW_+V37gR@Vg^)!Y3(K+lV{-?a|Kzr;5!HbJkHr{W^Y*|yP% za~!{kjBFpo^Q`imTV!=2YXVtotlPt3brTzyL{`hC8&#pdZgyPa+xHi|L5w|#=$WaZ zD!!O?!cT5g_lr$AKF%JaIcJ&#-3pd`c%%AEa2@>P+>K%KE08sctbt!{RQp6YZKKy^GCF2|ryZxK zA<|ErB^v*c;oP-1(t&h!a;#R%v|`+j#k#oL>jHD=0P!eE;D-A47aC^A;faGzXjIF{YRrqTc@89PP<1JO_i1s* z4+PD3#NM0XtKJkh=k%bzy%F<8iaBVV(B?PC)eh0E%p*_Ib4Q{Umh1li^6#}lWoF%g zDm4)xHE)S49ydt+(o#Q(L)yU`lW|2jqR%@^vzqaIFV5=egE{WZdtDf8Q&uJ|gM)8vvUI8xQk4p1EXnHqh@h#O0Bz zI*hEU*Tj{5&WbZ;+he0QHjNep1~d|!nzB`1&UmyVt~LgbwLV`SeSg?oB2C%4kTt$5 zuD+7>(XO@nbk>zcPYId}rbSl=kWsZet{$@Vol`$=ymoC!d9;ttzk=r|C&tbC%L4wei?|Y4}FqD>)_pf8O(7j*NL^EWS4$xZYU_J->s0 z-WOkW&q57s)$mTi`#$coW00PYq{HSTV(;yws~V0w-}sR>>;Y>5dvr~082M$0+Rgdz z$Xa=S+?@ZD6T7)>ilqw{-8`Y{&Um^Z2V^ct8B+|lUH8PDZ}OzUYQTEIHu?L4j;RSc zofk2pfA?iZP;KOPBct`h$UvT4(hdV)EhBMtk^sgItuf*@`FfX*dSszhbaw#W9(bSX zUVsd;@~@&k_r_J-`eCZz}vk>-sIKH zFV@Ig53kz4wlZ|WTfau$eem|Lk#`#2nKkk*!JGKEwUsy3Nj$Sg-bQ$b*T5@&q#NEj zc=huHr@wmFlOkXDu4{;GjFYbKqj6_%xt<4@=O{UQ2O;Lyvpo z$eRCjTs^opS)UD)C1qVf)}^0|>t`aws?~xeu0_9KFSg1rV^sy%2-pt<(_YBg3*{2*vt5IJ+mIq+~?{b_A-=7Qvi z%~iaKcAbyw{T5Old){-Kb-a*XDM>Ncg|`XbHh8yMdG3{+ry_&D4ruMr`W#x1UaZwq zphSnhZ-6!g-z z$JJCmKXTj3zfULZ)yqy!Z3_sh;1c_t1?xJ= zi_L>&PJfyw?J`)^T~9ObFFe5|Ya$SPj^$4{HFM0b8%R z3|}kQQ9igL(+;^LZ70~g3#$R^13MT=s56Dh(V^`N&GKAix62n|aAIqdBA;~6k#xKU zxHrQ4KM$CE4nx~lm~hIShGhY47)8cZ1h;0s%Zm1~n4|yMR4q3~{B2;pfA*Ua$ z`i@?PprLn=vM(M`Lbk}%)X@%n$~95zB<{f=rAyoXLpsM{r9Y?=K>`h7*Q6S3J& z=tE~Fa-L61nFpYCHz%C2-I>!NJUc0jtoq)Vu0OKIki`&pthR4jt^Ojb;>|oqLe^8Y zBi;XzmH+yL`35p)Shpg-4{Y$Pg!-YB)#?AC>#!dW@|0owqeT8Z@)Kt#oV6ZDuf}%$ z6QKTn!T4IZvc7i*CfpxC^GstV{|HqZD{4I=ocO}#t!u01`L;Pxp z{$veNe18Y>lJ85XXL4`Gh87(gOB~q`t@Q&5_3p6o)tgty%Q?MyA#dkM5mbi1Me-Ti zmr$9oe&-p-L$W!>J3x|0^DX%EdlKsGHRW+-P!}dr=m^5~v%3^|93YQ@4<$nP`HByi zgJ<~D2~YgF2)6$~LX|<6OZ&fG;y19TYsB=OB-kX_^=s^lkA+M$r7ubz?Z~T}O*rdl zg7tt6{~+P4feAJUwja!nB?a3LHsZq$fGr$KIA>6m!#4xg@T-K{&AqX&EA%@p@}ge_ z&;s=7-&*5{H;!@UvBFpKR@TEFvBqe_C+l!*0l&t6)TraNI{1d+>k6}3&w72xD~=h) zHuCI8R?njewamTc`^ZzaXz=Kg10g?S?^Dr}&5TYnNt(*Vg!63=#&u7cF9oDoBu)Mw z6YlT(>NNWOS@QXWCry*yXIpVA^BvMWEM;@Y^0JM_-h=X>Hq_3By*}&! z*q9HS0Xyu&j)3KF@aC}uR_()LZ^K@FSUFg~532^7@L}~}M|@ZdSaFdz-wv=^AJz-j z=EH`-27TBl*rX4e1Uu@(X2D8|z4^|ArG3~kSceZQzKwlQM~*Y&1vW12mIBKMv-`08 zUjvo^tK(k!%?KjPgV{e!?~tvg^dW91O? z60hFm=)4uNqhRG=`uQ^NYUaND=^1PS_xkR5;2ZMoV;m&Bw?zgD?p!B}ODco>SU>*l zHJemn@Z7+^jyD)M+{^mL8G%ow^?21m9+kT{nK91$t(QUB%W#f)!Sp73kgcR#Bcxd( zjraXw{c$~U(j4!^a%{}3YtF!X_{2?N?-}{(5HcQjntdEe7BE)!Y;x9dWffO!zZyKh zVUsy4fVP(LLGT9f;s87YUIP9sc<3uBK0K-UyTPjh@Iml;a2dPAmd>*HN5LDxD=ay@ zt*iUoLGU*4YQecgXNSOg!Spu)kRii#gmq}q6o$=5&y3E5SG|My>ZDDo#TwW2^OUrF z?<9H}BN3^!dUd{4@E$&Slk-l8=H=N7VZ+~TR`CPP@Ev^JCS5+sLa=tQNgvh&cEE+z zz&8lCbm}H`S%e=ZKG5+AQ#G@P>pVL^(QA?tUg~wHY0_1lzDa#o0QBs9o9ZL+nCR>% z^tm%OsV{Laa_zG7THm;BZ<2rF4%Uy)+@y*veVDZpd)?Ani~S#0tb=-_ehs8+Y~JLY zJ1V+u1#1C2m-}p8Og%UiT-Q#X=hi~*hj#$pNy*pLn>`VBzGl5NIzm(gzb=z(|Dgjk zz&=IV#95ovJ)Sa$tsmVG=E}6~BnHs`*_+h!E&XTp6fKX~iXu;`Umd*5=WJ5Dgjd^H z=ibPTmYH=%Rm3tL-Tv^d@ImM;u*xM^4_NHHO(upXeKpu1*wh7^RJo`9Ij1&a^cLL^ zaT$Oq#}sKguiC_ZHUyZqm3T^C+5DJYWB{G#BD_OaZ&ELZRW9jg`FCQkU?&K`r>XYx z=_z|1Jq4|;bCdIJq()e3z&gSH5izXf--o=W>o+-TqI+55jDbtrjzFuwag%Bd9$RFB z;NIrEG0-pbf=L=#3&`qz=cfPjUJ_}u>bp?UyEb{|XZ2uBKCA_7ADA?WTxtF~zz&aW zQqPO>gZ4Ik%f$ZToU_{|-V^a|$Iy07kyuFE|HxAYX-CHA+l@0Mjo;Hx|Yo; zB^G}ZwEfVov=WGnHZZlY$$Wp9TanQXR#&=N9nZZwMasuW@_?ZYLCb8}Y`*P73rA1R z)ACcWjzb&Tx;b>-FFc3fsZMQnz2D0jB)<21m*Ls>tj(&(>bp)`m^I;?cQOAEGM|w- zSp6X7ea>d*+rWCirZ;hRP8I!%ZcZ|~ zspjVlw9)5oR-HmKK9p{+#p|&ndSRZ&cSssJOmqHuYfq!wpERk;;Pz>P*8GCa>LMuv zbrrjgM9ZRYJ9afY?=On)6JN4G zx@65}b)!`_Uz_`VNhYMpwf)wq(%DT-@d2hDl-|%kYkJn`L7pxVm7A(%aT!M9i)qri(h|xtKST$IVU$pz; zLGS#5ecJ)g%6I6q3M-L&2ziMen~k40_95GQ84u;@qr`N?4)A593`2~6J2$Iy!fe8` z9(qYo#>ZT%tR{;#@=&`rtIgtTT>a8I$ADcFDZ&g*pmjR6j~*sX)t=3&i6q#N{sx`s zZydZ0{F#DtmGCzO)(N&j0BVOj*Y-XKK6<)khbCWVpQ-D4;;PS6#a=7lgTFpwv)OMK zDl7WJRoAVMCAQg(ys_rZW-d#a=|B3Ys{}9XijNqEz5-p34`*b@2hqU&HqSk;X9?a}c%SOtLCH7$UhM6H)$%lElxdyX$NOeTSrI}Gp4<(t(BQYU+^+@$;Pvz$Ksz=PV(r{Eob(`I#( zB{#>fd!Ho+zECaiO^aP7h8Yi$)#a3t7uKba?dCkMYUm|zUcC-tr>*dI!@E^vaaHlx z3DyTzD!xGab)QI2M@}*SX@#UCzjxxP5%{}q%F59857rA76QNv!O@sCL_~yX+!HP7O z;addj^YP`spLh^#i^V7N#UxnO_-5z)6PFE4**4G$UmCuW&!P`)do*K?@;weTc+tVE z%bY`~;9i!|=QoTXzkUMWDV3lZ9Pt>S{(46?Xx_U%AZQ+^Tn!{Wik$MVY*vT;v9}$A zd5RhRO5hR|a>R~nK0v*{wORd{dnuFFjb1Z|d`Rm?hwEwm>>va0(oZ+5@8xvJc;@^ZP|D5UJaQ97 zCF(ES``YVXmwq&FVjts6`Pz8v;O&3$>b%lF+u=S;k2}0I0C&Hx^z#uL?26Fi9IFGzEGF_ ze!#Df)II3$oD%g%N$j=t_q%z;w>84M+)-lA?KSPE=O5PDw2=?E;v5;bdf-o9RiZu_ zW>d4?xG(rnU32bs8hJCwt9@IES-Z-ypC*_6{3gd##Q#f~5+6c8Z!b~hVSd85XJRJM z&docuCCJMlZ|WT-isftZ_nnbj&HJt0;B|MDn0J73`;+f1hD=bGk#lyMLPFuD4B5?* z$M8Uj`DSZS9v+{^DnLNLB)ENJ5H4}5*me5D`2V32=Nr-uV9j8Q_m-%Q!D}{q;)|Yk zy(U=xVxGtvLDs~-VXI=mtJWd8_eJ*KRaUBc<^GsHBlml~VxMW!A9_Zq^G>ikK5;pmHJuGGfUM-ROaCEzHV-4K5n0y- z)a%KVtBv#(_tDnKx-r6!pg!2-Zcmv51~e#VBt||LL=ewwV&ehw8LlWb`y4qY%8ZBB zH^?85r(^aBwWdE$!r$=RQuAy~)>%HP*I6!<0WgJ3X`dtT)I6{BDL)gF@vUkl6NcoN8w)|rZ-Pt=7aVDYP0?9D6)E9RO+moi2O;gZm|Arc?gNT z&rEm+w-Rk=vy^3zpfd_ZR_x!9e?qA<=L@mpHSIlaM%Fm8)>!Y5Goj46Oe4tZdihhg zW2xf;vPO}W^WLI82k<;g2>GN!#;hCFew6uqT`7BF5WISy;8|io;~wMOP$};|^5}nc zY2ewWu0DK;HxkZ$4@P7iMb^NMQhm-=*3bEDEfOVtfweZ=cqUJ$0outUGZw@5xm$!A}EsXBjc`8*>upN|GLr14=AKQ@gr2JR_U zA6mzcT)W6w*F7g>�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