From 0d4625aecdf89e37e7a3643d469352a41eead2ee Mon Sep 17 00:00:00 2001 From: MasterPtato Date: Mon, 24 Nov 2025 18:57:53 -0800 Subject: [PATCH] fix(pb): implement events and commands for actor wfs --- Cargo.lock | 3 + engine/packages/api-builder/src/middleware.rs | 8 +- engine/packages/engine/src/commands/db/mod.rs | 10 - engine/packages/engine/src/commands/start.rs | 16 +- engine/packages/engine/src/util/db.rs | 26 +- .../guard-core/src/websocket_handle.rs | 6 +- .../guard-core/tests/simple_websocket.rs | 38 +- .../guard/src/routing/pegboard_gateway.rs | 2 +- engine/packages/metrics/src/buckets.rs | 4 +- .../pegboard-gateway/src/keepalive_task.rs | 2 +- engine/packages/pegboard-gateway/src/lib.rs | 32 +- .../pegboard-gateway/src/shared_state.rs | 8 +- engine/packages/pegboard-runner/Cargo.toml | 10 +- .../src/actor_event_demuxer.rs | 25 +- engine/packages/pegboard-runner/src/conn.rs | 437 ++++++++++++----- .../pegboard-runner/src/tunnel_to_ws_task.rs | 6 +- .../pegboard-runner/src/ws_to_tunnel_task.rs | 111 ++--- .../packages/pegboard-serverless/src/lib.rs | 17 +- engine/packages/pegboard/src/keys/runner.rs | 191 ++++++++ .../src/ops/runner/update_alloc_idx.rs | 5 +- engine/packages/pegboard/src/utils.rs | 19 +- .../pegboard/src/workflows/actor/destroy.rs | 37 +- .../pegboard/src/workflows/actor/mod.rs | 459 ++++++++++++++---- .../pegboard/src/workflows/actor/runtime.rs | 245 +++++++++- .../pegboard/src/workflows/runner2.rs | 139 +++--- engine/packages/universaldb/Cargo.toml | 1 + .../src/driver/postgres/database.rs | 9 +- .../src/driver/rocksdb/database.rs | 3 +- .../rocksdb/transaction_conflict_tracker.rs | 8 +- .../src/driver/rocksdb/transaction_task.rs | 3 +- engine/packages/universaldb/src/utils/keys.rs | 2 + engine/packages/universaldb/src/utils/mod.rs | 8 + .../sdks/rust/api-full/src/apis/actors_api.rs | 12 +- engine/sdks/rust/api-full/src/apis/ns_api.rs | 12 +- .../rust/runner-protocol/src/versioned.rs | 75 +++ engine/sdks/schemas/runner-protocol/v4.bare | 10 +- .../typescript/runner-protocol/src/index.ts | 134 +++-- engine/sdks/typescript/runner/src/actor.ts | 22 +- engine/sdks/typescript/runner/src/mod.ts | 154 +++--- .../sdks/typescript/runner/src/stringify.ts | 52 +- engine/sdks/typescript/runner/src/tunnel.ts | 3 - .../sdks/typescript/test-runner/src/index.ts | 18 +- pnpm-lock.yaml | 431 ++++++++-------- 43 files changed, 1906 insertions(+), 907 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a941174f1f..69cb0e9246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3523,6 +3523,7 @@ dependencies = [ "pegboard", "pegboard-actor-kv", "rivet-config", + "rivet-data", "rivet-error", "rivet-guard-core", "rivet-metrics", @@ -3534,6 +3535,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tracing", + "universaldb", "universalpubsub", "url", "vbare", @@ -6717,6 +6719,7 @@ dependencies = [ "deadpool-postgres", "foundationdb-tuple", "futures-util", + "hex", "lazy_static", "rand 0.8.5", "rivet-config", diff --git a/engine/packages/api-builder/src/middleware.rs b/engine/packages/api-builder/src/middleware.rs index 67168cd2cc..df834fe88d 100644 --- a/engine/packages/api-builder/src/middleware.rs +++ b/engine/packages/api-builder/src/middleware.rs @@ -186,13 +186,7 @@ pub async fn http_logging_middleware( ); // Update metrics - metrics::API_REQUEST_PENDING.add( - -1, - &[ - KeyValue::new("method", method_clone.to_string()), - KeyValue::new("path", path_clone.clone()), - ], - ); + metrics::API_REQUEST_PENDING.add(-1, &[KeyValue::new("method", method_clone.to_string()), KeyValue::new("path", path_clone.clone())]); let error_code: String = if status.is_success() { String::new() diff --git a/engine/packages/engine/src/commands/db/mod.rs b/engine/packages/engine/src/commands/db/mod.rs index d3caff16aa..686103ee54 100644 --- a/engine/packages/engine/src/commands/db/mod.rs +++ b/engine/packages/engine/src/commands/db/mod.rs @@ -22,10 +22,6 @@ pub enum SubCommand { pub enum DatabaseType { #[clap(alias = "ch")] Clickhouse, - #[clap(alias = "wfd")] - WorkflowData, - #[clap(alias = "wfi")] - WorkflowInternal, } impl SubCommand { @@ -48,12 +44,6 @@ impl SubCommand { DatabaseType::Clickhouse => { crate::util::db::clickhouse_shell(config, shell_ctx).await? } - DatabaseType::WorkflowData => { - crate::util::db::wf_sqlite_shell(config, shell_ctx, false).await? - } - DatabaseType::WorkflowInternal => { - crate::util::db::wf_sqlite_shell(config, shell_ctx, true).await? - } } Ok(()) diff --git a/engine/packages/engine/src/commands/start.rs b/engine/packages/engine/src/commands/start.rs index b6f9200c72..c0d2922b20 100644 --- a/engine/packages/engine/src/commands/start.rs +++ b/engine/packages/engine/src/commands/start.rs @@ -115,15 +115,14 @@ async fn verify_engine_version( pools .udb()? .run(|tx| async move { - let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")) - .context("failed to parse cargo pkg version as semver")?; + let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).context("failed to parse cargo pkg version as semver")?; - if let Some(existing_version) = - tx.read_opt(&keys::EngineVersionKey {}, Serializable).await? - { + if let Some(existing_version) = tx.read_opt(&keys::EngineVersionKey {}, Serializable).await? { if current_version < existing_version { - return Ok(Err(anyhow!("{}", formatdoc!( - " + return Ok(Err(anyhow!( + "{}", + formatdoc!( + " Rivet Engine has been rolled back to a previous version: - Last Used Version: {existing_version} - Current Version: {current_version} @@ -131,7 +130,8 @@ async fn verify_engine_version( (If you know what you're doing, this error can be disabled in the Rivet config via `allow_version_rollback: true`) " - )))); + ) + ))); } } diff --git a/engine/packages/engine/src/util/db.rs b/engine/packages/engine/src/util/db.rs index 788cc837d3..5c03646c68 100644 --- a/engine/packages/engine/src/util/db.rs +++ b/engine/packages/engine/src/util/db.rs @@ -1,7 +1,6 @@ -use std::{path::Path, result::Result::Ok, str::FromStr}; +use std::{path::Path, result::Result::Ok}; use anyhow::*; -use rivet_util::Id; use serde_json::json; pub struct ShellQuery { @@ -74,26 +73,3 @@ pub async fn clickhouse_shell( Ok(()) } - -pub async fn wf_sqlite_shell( - config: rivet_config::Config, - shell_ctx: ShellContext<'_>, - _internal: bool, -) -> Result<()> { - let ShellContext { queries, .. } = shell_ctx; - - let _pools = rivet_pools::Pools::new(config.clone()).await?; - - // Combine all queries into one command - for ShellQuery { - svc: workflow_id, - query: _query, - } in queries - { - let _workflow_id = Id::from_str(workflow_id).context("could not parse input as Id")?; - - todo!(); - } - - Ok(()) -} diff --git a/engine/packages/guard-core/src/websocket_handle.rs b/engine/packages/guard-core/src/websocket_handle.rs index 2a3c50a4b3..fdb317a411 100644 --- a/engine/packages/guard-core/src/websocket_handle.rs +++ b/engine/packages/guard-core/src/websocket_handle.rs @@ -2,7 +2,7 @@ use anyhow::*; use futures_util::{SinkExt, StreamExt, stream::Peekable}; use hyper::upgrade::Upgraded; use hyper_tungstenite::HyperWebsocket; -use hyper_tungstenite::tungstenite::Message as WsMessage; +use hyper_tungstenite::tungstenite::Message; use hyper_util::rt::TokioIo; use std::sync::Arc; use tokio::sync::Mutex; @@ -12,7 +12,7 @@ pub type WebSocketReceiver = Peekable>>>; pub type WebSocketSender = - futures_util::stream::SplitSink>, WsMessage>; + futures_util::stream::SplitSink>, Message>; #[derive(Clone)] pub struct WebSocketHandle { @@ -31,7 +31,7 @@ impl WebSocketHandle { }) } - pub async fn send(&self, message: WsMessage) -> Result<()> { + pub async fn send(&self, message: Message) -> Result<()> { self.ws_tx.lock().await.send(message).await?; Ok(()) } diff --git a/engine/packages/guard-core/tests/simple_websocket.rs b/engine/packages/guard-core/tests/simple_websocket.rs index ead8c7a5ce..2672993d52 100644 --- a/engine/packages/guard-core/tests/simple_websocket.rs +++ b/engine/packages/guard-core/tests/simple_websocket.rs @@ -175,25 +175,25 @@ async fn start_websocket_server() -> SocketAddr { match message_result { Ok(msg) => { match &msg { - hyper_tungstenite::tungstenite::Message::Text(text) => { - println!("Server: Received text message: {}", text); - }, - hyper_tungstenite::tungstenite::Message::Binary(data) => { - println!("Server: Received binary message of {} bytes", data.len()); - }, - hyper_tungstenite::tungstenite::Message::Ping(_) => { - println!("Server: Received ping"); - }, - hyper_tungstenite::tungstenite::Message::Pong(_) => { - println!("Server: Received pong"); - }, - hyper_tungstenite::tungstenite::Message::Close(_) => { - println!("Server: Received close message"); - }, - _ => { - println!("Server: Received unknown message type"); - } - } + hyper_tungstenite::tungstenite::Message::Text(text) => { + println!("Server: Received text message: {}", text); + } + hyper_tungstenite::tungstenite::Message::Binary(data) => { + println!("Server: Received binary message of {} bytes", data.len()); + } + hyper_tungstenite::tungstenite::Message::Ping(_) => { + println!("Server: Received ping"); + } + hyper_tungstenite::tungstenite::Message::Pong(_) => { + println!("Server: Received pong"); + } + hyper_tungstenite::tungstenite::Message::Close(_) => { + println!("Server: Received close message"); + } + _ => { + println!("Server: Received unknown message type"); + } + } println!("Server: Echoing message back"); match write.send(msg).await { diff --git a/engine/packages/guard/src/routing/pegboard_gateway.rs b/engine/packages/guard/src/routing/pegboard_gateway.rs index b3501b4bd2..45ed922d24 100644 --- a/engine/packages/guard/src/routing/pegboard_gateway.rs +++ b/engine/packages/guard/src/routing/pegboard_gateway.rs @@ -210,7 +210,7 @@ async fn route_request_inner( .to_workflow_id(actor.workflow_id) .graceful_not_found() .send() - .await; + .await?; if res.is_none() { tracing::warn!( diff --git a/engine/packages/metrics/src/buckets.rs b/engine/packages/metrics/src/buckets.rs index b719898714..720e459ff8 100644 --- a/engine/packages/metrics/src/buckets.rs +++ b/engine/packages/metrics/src/buckets.rs @@ -1,8 +1,8 @@ pub const BUCKETS: &[f64] = &[ // For otel 0.0, // Added - 0.001, 0.0025, - // Copied from https://docs.rs/prometheus/latest/src/prometheus/histogram.rs.html#25-27 + 0.001, + 0.0025, // Copied from https://docs.rs/prometheus/latest/src/prometheus/histogram.rs.html#25-27 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, // Added 25.0, 50.0, 100.0, 250.0, 500.0, ]; diff --git a/engine/packages/pegboard-gateway/src/keepalive_task.rs b/engine/packages/pegboard-gateway/src/keepalive_task.rs index 1632cd68b0..3ea3378956 100644 --- a/engine/packages/pegboard-gateway/src/keepalive_task.rs +++ b/engine/packages/pegboard-gateway/src/keepalive_task.rs @@ -52,7 +52,7 @@ pub async fn task( ctx.op(pegboard::ops::actor::hibernating_request::upsert::Input { actor_id, gateway_id, - request_id, + request_id }), // Keep alive in flight req during hibernation shared_state.keepalive_hws(request_id), diff --git a/engine/packages/pegboard-gateway/src/lib.rs b/engine/packages/pegboard-gateway/src/lib.rs index 2019bcb584..d0a5a14aa0 100644 --- a/engine/packages/pegboard-gateway/src/lib.rs +++ b/engine/packages/pegboard-gateway/src/lib.rs @@ -14,7 +14,7 @@ use rivet_guard_core::{ request_context::RequestContext, websocket_handle::WebSocketReceiver, }; -use rivet_runner_protocol as protocol; +use rivet_runner_protocol::{self as protocol, PROTOCOL_MK1_VERSION}; use rivet_util::serde::HashableMap; use std::{sync::Arc, time::Duration}; use tokio::sync::{Mutex, watch}; @@ -161,13 +161,16 @@ impl CustomServeTrait for PegboardGateway { .subscribe::(("actor_id", self.actor_id)), // Read runner protocol version udb.run(|tx| async move { - tx.with_subspace(pegboard::keys::subspace()); + let tx = tx.with_subspace(pegboard::keys::subspace()); - tx.read( - &pegboard::keys::runner::ProtocolVersionKey::new(runner_id), - Serializable, - ) - .await + let protocol_version_entry = tx + .read_opt( + &pegboard::keys::runner::ProtocolVersionKey::new(runner_id), + Serializable, + ) + .await?; + + Ok(protocol_version_entry.unwrap_or(PROTOCOL_MK1_VERSION)) }) )?; @@ -305,13 +308,16 @@ impl CustomServeTrait for PegboardGateway { .subscribe::(("actor_id", self.actor_id)), // Read runner protocol version udb.run(|tx| async move { - tx.with_subspace(pegboard::keys::subspace()); + let tx = tx.with_subspace(pegboard::keys::subspace()); - tx.read( - &pegboard::keys::runner::ProtocolVersionKey::new(runner_id), - Serializable, - ) - .await + let protocol_version_entry = tx + .read_opt( + &pegboard::keys::runner::ProtocolVersionKey::new(runner_id), + Serializable, + ) + .await?; + + Ok(protocol_version_entry.unwrap_or(PROTOCOL_MK1_VERSION)) }) )?; diff --git a/engine/packages/pegboard-gateway/src/shared_state.rs b/engine/packages/pegboard-gateway/src/shared_state.rs index 535b3f907d..0ee669698e 100644 --- a/engine/packages/pegboard-gateway/src/shared_state.rs +++ b/engine/packages/pegboard-gateway/src/shared_state.rs @@ -527,12 +527,10 @@ impl SharedState { let reason = 'reason: { if let Some(hs) = &req.hibernation_state { if let Some(earliest_pending_ws_msg) = hs.pending_ws_msgs.first() { - if now.duration_since(earliest_pending_ws_msg.send_instant) - > HWS_MESSAGE_ACK_TIMEOUT - { + if now.duration_since(earliest_pending_ws_msg.send_instant) > HWS_MESSAGE_ACK_TIMEOUT { break 'reason Some(MsgGcReason::WebSocketMessageNotAcked { first_msg_index: earliest_pending_ws_msg.message_index, - last_msg_index: req.message_index + last_msg_index: req.message_index, }); } } @@ -543,7 +541,7 @@ impl SharedState { timeout=%hibernation_timeout.as_secs_f64(), "checking hibernating state elapsed time" ); - if hs_elapsed> hibernation_timeout { + if hs_elapsed > hibernation_timeout { break 'reason Some(MsgGcReason::HibernationTimeout); } } else if req.msg_tx.is_closed() { diff --git a/engine/packages/pegboard-runner/Cargo.toml b/engine/packages/pegboard-runner/Cargo.toml index b2df569e73..6a618c779f 100644 --- a/engine/packages/pegboard-runner/Cargo.toml +++ b/engine/packages/pegboard-runner/Cargo.toml @@ -23,15 +23,17 @@ rivet-guard-core.workspace = true rivet-metrics.workspace = true rivet-runner-protocol.workspace = true rivet-runtime.workspace = true -serde.workspace = true -serde_json.workspace = true +rivet-data.workspace = true serde_bare.workspace = true -tokio.workspace = true +serde_json.workspace = true +serde.workspace = true tokio-tungstenite.workspace = true +tokio.workspace = true tracing.workspace = true +universaldb.workspace = true +universalpubsub.workspace = true url.workspace = true vbare.workspace = true -universalpubsub.workspace = true pegboard.workspace = true pegboard-actor-kv.workspace = true diff --git a/engine/packages/pegboard-runner/src/actor_event_demuxer.rs b/engine/packages/pegboard-runner/src/actor_event_demuxer.rs index 8244d3fcb7..8dadd18916 100644 --- a/engine/packages/pegboard-runner/src/actor_event_demuxer.rs +++ b/engine/packages/pegboard-runner/src/actor_event_demuxer.rs @@ -11,35 +11,39 @@ const GC_INTERVAL: Duration = Duration::from_secs(30); const MAX_LAST_SEEN: Duration = Duration::from_secs(30); struct Channel { - tx: mpsc::UnboundedSender, + tx: mpsc::UnboundedSender, handle: JoinHandle<()>, last_seen: Instant, } pub struct ActorEventDemuxer { ctx: StandaloneCtx, + runner_id: Id, channels: HashMap, last_gc: Instant, } impl ActorEventDemuxer { - pub fn new(ctx: StandaloneCtx) -> Self { + pub fn new(ctx: StandaloneCtx, runner_id: Id) -> Self { Self { ctx, + runner_id, channels: HashMap::new(), last_gc: Instant::now(), } } /// Process an event by routing it to the appropriate actor's queue - #[tracing::instrument(skip_all)] - pub fn ingest(&mut self, actor_id: Id, event: protocol::mk2::Event) { + pub fn ingest(&mut self, actor_id: Id, event: protocol::mk2::EventWrapper) { + tracing::debug!(runner_id=?self.runner_id, ?actor_id, index=?event.checkpoint.index, "actor demuxer ingest"); + if let Some(channel) = self.channels.get(&actor_id) { let _ = channel.tx.send(event); } else { let (tx, mut rx) = mpsc::unbounded_channel(); let ctx = self.ctx.clone(); + let runner_id = self.runner_id; let handle = tokio::spawn(async move { loop { let mut buffer = Vec::new(); @@ -49,13 +53,15 @@ impl ActorEventDemuxer { break; } - if let Err(err) = dispatch_events(&ctx, actor_id, buffer).await { + if let Err(err) = dispatch_events(&ctx, runner_id, actor_id, buffer).await { tracing::error!(?err, "actor event processor failed"); break; } } }); + let _ = tx.send(event); + self.channels.insert( actor_id, Channel { @@ -104,13 +110,18 @@ impl ActorEventDemuxer { } } +#[tracing::instrument(skip_all, fields(?runner_id, ?actor_id))] async fn dispatch_events( ctx: &StandaloneCtx, + runner_id: Id, actor_id: Id, - events: Vec, + events: Vec, ) -> Result<()> { + tracing::debug!(count=?events.len(), "actor demuxer dispatch"); + let res = ctx - .signal(pegboard::workflows::actor::Events { inner: events }) + .signal(pegboard::workflows::actor::Events { runner_id, events }) + .to_workflow::() .tag("actor_id", actor_id) .graceful_not_found() .send() diff --git a/engine/packages/pegboard-runner/src/conn.rs b/engine/packages/pegboard-runner/src/conn.rs index 573ff52d44..e9310cbc96 100644 --- a/engine/packages/pegboard-runner/src/conn.rs +++ b/engine/packages/pegboard-runner/src/conn.rs @@ -5,12 +5,15 @@ use std::{ use anyhow::Context; use futures_util::StreamExt; +use futures_util::TryStreamExt; use gas::prelude::Id; use gas::prelude::*; use hyper_tungstenite::tungstenite::Message; use pegboard::ops::runner::update_alloc_idx::{Action, RunnerEligibility}; +use rivet_data::converted::{ActorNameKeyData, MetadataKeyData}; use rivet_guard_core::WebSocketHandle; use rivet_runner_protocol::{self as protocol, versioned}; +use universaldb::prelude::*; use vbare::OwnedVersionedData; use crate::{errors::WsError, utils::UrlData}; @@ -50,7 +53,7 @@ pub async fn init_conn( let mut ws_rx = ws_rx.lock().await; // Receive init packet - let (runner_name, runner_id, workflow_id) = if let Some(msg) = + let (runner_name, runner_id, workflow_id, init) = if let Some(msg) = tokio::time::timeout(Duration::from_secs(5), ws_rx.next()) .await .map_err(|_| WsError::TimedOutWaitingForInit.build())? @@ -64,147 +67,107 @@ pub async fn init_conn( } }; - let init_packet = versioned::ToServer::deserialize(&buf, protocol_version) - .map_err(|err| WsError::InvalidPacket(err.to_string()).build()) - .context("failed to deserialize initial packet from client")?; - - let (runner_name, runner_id, workflow_id) = - if let protocol::ToServer::ToServerInit(protocol::ToServerInit { - name, - version, - total_slots, - .. - }) = &init_packet - { - // Look up existing runner by key - let existing_runner = ctx - .op(pegboard::ops::runner::get_by_key::Input { - namespace_id: namespace.namespace_id, - name: name.clone(), - key: runner_key.clone(), - }) - .await - .with_context(|| { - format!( - "failed to get existing runner by key: {}:{}", - name, runner_key - ) - })?; - - let runner_id = if let Some(runner) = existing_runner.runner { - // IMPORTANT: Before we spawn/get the workflow, we try to update the runner's last ping ts. - // This ensures if the workflow is currently checking for expiry that it will not expire - // (because we are about to send signals to it) and if it is already expired (but not - // completed) we can choose a new runner id. - let update_ping_res = ctx - .op(pegboard::ops::runner::update_alloc_idx::Input { - runners: vec![pegboard::ops::runner::update_alloc_idx::Runner { - runner_id: runner.runner_id, - action: Action::UpdatePing { rtt: 0 }, - }], - }) - .await - .with_context(|| { - format!("failed to update ping for runner: {}", runner.runner_id) - })?; - - if update_ping_res - .notifications - .into_iter() - .next() - .map(|notif| matches!(notif.eligibility, RunnerEligibility::Expired)) - .unwrap_or_default() - { - // Runner expired, create a new one - Id::new_v1(ctx.config().dc_label()) - } else { - // Use existing runner - runner.runner_id - } - } else { - // No existing runner for this key, create a new one - Id::new_v1(ctx.config().dc_label()) - }; - - // Spawn a new runner workflow if one doesn't already exist - let workflow_id = if protocol::is_mk2(protocol_version) { - ctx.workflow(pegboard::workflows::runner2::Input { - runner_id, - namespace_id: namespace.namespace_id, - name: name.clone(), - key: runner_key.clone(), - version: version.clone(), - total_slots: *total_slots, - protocol_version, - }) - .tag("runner_id", runner_id) - .unique() - .dispatch() - .await - .with_context(|| { - format!( - "failed to dispatch runner workflow for runner: {}", - runner_id - ) - })? - } else { - ctx.workflow(pegboard::workflows::runner::Input { - runner_id, - namespace_id: namespace.namespace_id, - name: name.clone(), - key: runner_key.clone(), - version: version.clone(), - total_slots: *total_slots, - }) - .tag("runner_id", runner_id) - .unique() - .dispatch() - .await - .with_context(|| { - format!( - "failed to dispatch runner workflow for runner: {}", - runner_id - ) - })? - }; - - (name.clone(), runner_id, workflow_id) - } else { - tracing::debug!(?init_packet, "invalid initial packet"); - return Err(WsError::InvalidInitialPacket("must be `ToServer::Init`").build()); - }; + let init = Init::new(&buf, protocol_version)?; - if protocol::is_mk2(protocol_version) { - ctx.signal(pegboard::workflows::runner2::Init {}) - .to_workflow_id(workflow_id) - .send() + // Look up existing runner by key + let existing_runner = ctx + .op(pegboard::ops::runner::get_by_key::Input { + namespace_id: namespace.namespace_id, + name: init.name().to_string(), + key: runner_key.clone(), + }) + .await + .with_context(|| { + format!( + "failed to get existing runner by key: {}:{}", + init.name(), + runner_key + ) + })?; + + let runner_id = if let Some(runner) = existing_runner.runner { + // IMPORTANT: Before we spawn/get the workflow, we try to update the runner's last ping ts. + // This ensures if the workflow is currently checking for expiry that it will not expire + // (because we are about to send signals to it) and if it is already expired (but not + // completed) we can choose a new runner id. + let update_ping_res = ctx + .op(pegboard::ops::runner::update_alloc_idx::Input { + runners: vec![pegboard::ops::runner::update_alloc_idx::Runner { + runner_id: runner.runner_id, + action: Action::UpdatePing { rtt: 0 }, + }], + }) .await .with_context(|| { - format!( - "failed to forward initial packet to workflow: {}", - workflow_id - ) + format!("failed to update ping for runner: {}", runner.runner_id) })?; + + if update_ping_res + .notifications + .into_iter() + .next() + .map(|notif| matches!(notif.eligibility, RunnerEligibility::Expired)) + .unwrap_or_default() + { + // Runner expired, create a new one + Id::new_v1(ctx.config().dc_label()) + } else { + // Use existing runner + runner.runner_id + } } else { - // Forward to runner wf - ctx.signal(pegboard::workflows::runner::Forward { inner: init_packet }) - .to_workflow_id(workflow_id) - .send() - .await - .with_context(|| { - format!( - "failed to forward initial packet to workflow: {}", - workflow_id - ) - })?; - } + // No existing runner for this key, create a new one + Id::new_v1(ctx.config().dc_label()) + }; - (runner_name, runner_id, workflow_id) + // Spawn a new runner workflow if one doesn't already exist + let workflow_id = if protocol::is_mk2(protocol_version) { + ctx.workflow(pegboard::workflows::runner2::Input { + runner_id, + namespace_id: namespace.namespace_id, + name: init.name().to_string(), + key: runner_key.clone(), + version: init.version(), + total_slots: init.total_slots(), + protocol_version, + }) + .tag("runner_id", runner_id) + .unique() + .dispatch() + .await + .with_context(|| { + format!( + "failed to dispatch runner workflow for runner: {}", + runner_id + ) + })? + } else { + ctx.workflow(pegboard::workflows::runner::Input { + runner_id, + namespace_id: namespace.namespace_id, + name: init.name().to_string(), + key: runner_key.clone(), + version: init.version(), + total_slots: init.total_slots(), + }) + .tag("runner_id", runner_id) + .unique() + .dispatch() + .await + .with_context(|| { + format!( + "failed to dispatch runner workflow for runner: {}", + runner_id + ) + })? + }; + + (init.name().to_string(), runner_id, workflow_id, init) } else { return Err(WsError::ConnectionClosed.build()); }; - Ok(Arc::new(Conn { + let conn = Arc::new(Conn { namespace_id: namespace.namespace_id, runner_name, runner_key, @@ -213,5 +176,213 @@ pub async fn init_conn( protocol_version, ws_handle, last_rtt: AtomicU32::new(0), - })) + }); + + match init { + Init::Mk2(init) => handle_init(ctx, &conn, init).await?, + Init::Mk1(init) => { + // Forward to runner wf + ctx.signal(pegboard::workflows::runner::Forward { + inner: protocol::ToServer::ToServerInit(init), + }) + .to_workflow_id(workflow_id) + .send() + .await + .with_context(|| { + format!( + "failed to forward initial packet to workflow: {}", + workflow_id + ) + })?; + } + } + + Ok(conn) +} + +enum Init { + Mk2(protocol::mk2::ToServerInit), + Mk1(protocol::ToServerInit), +} + +impl Init { + fn new(buf: &[u8], protocol_version: u16) -> Result { + if protocol::is_mk2(protocol_version) { + let init_packet = versioned::ToServerMk2::deserialize(&buf, protocol_version) + .map_err(|err| WsError::InvalidPacket(err.to_string()).build()) + .context("failed to deserialize initial packet from client")?; + + let protocol::mk2::ToServer::ToServerInit(init) = init_packet else { + tracing::debug!(?init_packet, "invalid initial packet"); + return Err(WsError::InvalidInitialPacket("must be `ToServer::Init`").build()); + }; + + Ok(Init::Mk2(init)) + } else { + let init_packet = versioned::ToServer::deserialize(&buf, protocol_version) + .map_err(|err| WsError::InvalidPacket(err.to_string()).build()) + .context("failed to deserialize initial packet from client")?; + + let protocol::ToServer::ToServerInit(init) = init_packet else { + tracing::debug!(?init_packet, "invalid initial packet"); + return Err(WsError::InvalidInitialPacket("must be `ToServer::Init`").build()); + }; + + Ok(Init::Mk1(init)) + } + } + + fn name(&self) -> &str { + match self { + Init::Mk2(init) => &init.name, + Init::Mk1(init) => &init.name, + } + } + + fn version(&self) -> u32 { + match self { + Init::Mk2(init) => init.version, + Init::Mk1(init) => init.version, + } + } + + fn total_slots(&self) -> u32 { + match self { + Init::Mk2(init) => init.total_slots, + Init::Mk1(init) => init.total_slots, + } + } +} + +pub async fn handle_init( + ctx: &StandaloneCtx, + conn: &Conn, + init: protocol::mk2::ToServerInit, +) -> Result<()> { + // We send the signal first because we don't want to continue if this fails + ctx.signal(pegboard::workflows::runner2::Init {}) + .to_workflow_id(conn.workflow_id) + .send() + .await + .with_context(|| { + format!( + "failed to send signal to runner workflow: {}", + conn.workflow_id + ) + })?; + + let missed_commands = ctx + .udb()? + .run(|tx| { + let init = init.clone(); + async move { + let tx = tx.with_subspace(pegboard::keys::subspace()); + + // Populate actor names if provided + if let Some(actor_names) = &init.prepopulate_actor_names { + // Write each actor name into the namespace actor names list + for (name, data) in actor_names { + let metadata = serde_json::from_str::< + serde_json::Map, + >(&data.metadata) + .unwrap_or_default(); + + tx.write( + &pegboard::keys::ns::ActorNameKey::new(conn.namespace_id, name.clone()), + ActorNameKeyData { metadata }, + )?; + } + } + + if let Some(metadata) = &init.metadata { + let metadata = MetadataKeyData { + metadata: + serde_json::from_str::>( + &metadata, + ) + .unwrap_or_default(), + }; + + let metadata_key = pegboard::keys::runner::MetadataKey::new(conn.runner_id); + + // Clear old metadata + tx.delete_key_subspace(&metadata_key); + + // Write metadata + for (i, chunk) in metadata_key.split(metadata)?.into_iter().enumerate() { + let chunk_key = metadata_key.chunk(i); + + tx.set(&tx.pack(&chunk_key), &chunk); + } + } + + let runner_actor_commands_subspace = pegboard::keys::subspace().subspace( + &pegboard::keys::runner::ActorCommandKey::subspace(conn.runner_id), + ); + + // Read missed commands + tx.get_ranges_keyvalues( + RangeOption { + mode: StreamingMode::WantAll, + ..(&runner_actor_commands_subspace).into() + }, + Serializable, + ) + .map(|res| { + let (key, command) = + tx.read_entry::(&res?)?; + match command { + protocol::mk2::ActorCommandKeyData::CommandStartActor(x) => { + Ok(protocol::mk2::CommandWrapper { + checkpoint: protocol::mk2::ActorCheckpoint { + actor_id: key.actor_id.to_string(), + index: key.index, + }, + inner: protocol::mk2::Command::CommandStartActor(x), + }) + } + protocol::mk2::ActorCommandKeyData::CommandStopActor(x) => { + Ok(protocol::mk2::CommandWrapper { + checkpoint: protocol::mk2::ActorCheckpoint { + actor_id: key.actor_id.to_string(), + index: key.index, + }, + inner: protocol::mk2::Command::CommandStopActor(x), + }) + } + } + }) + .try_collect::>() + .await + } + }) + .custom_instrument(tracing::info_span!("runner_process_init_tx")) + .await?; + + // Send init packet + let init_msg = versioned::ToClientMk2::wrap_latest(protocol::mk2::ToClient::ToClientInit( + protocol::mk2::ToClientInit { + runner_id: conn.runner_id.to_string(), + metadata: protocol::mk2::ProtocolMetadata { + runner_lost_threshold: ctx.config().pegboard().runner_lost_threshold(), + }, + }, + )); + let init_msg_serialized = init_msg.serialize(conn.protocol_version)?; + conn.ws_handle + .send(Message::Binary(init_msg_serialized.into())) + .await?; + + // Send missed commands + if !missed_commands.is_empty() { + let msg = versioned::ToClientMk2::wrap_latest(protocol::mk2::ToClient::ToClientCommands( + missed_commands, + )); + let msg_serialized = msg.serialize(conn.protocol_version)?; + conn.ws_handle + .send(Message::Binary(msg_serialized.into())) + .await?; + } + + Ok(()) } 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 5920570ad4..f1b7d59adf 100644 --- a/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs +++ b/engine/packages/pegboard-runner/src/tunnel_to_ws_task.rs @@ -2,9 +2,7 @@ use anyhow::Result; use gas::prelude::*; use hyper_tungstenite::tungstenite::Message; use pegboard::pubsub_subjects::GatewayReceiverSubject; -use rivet_runner_protocol::{ - self as protocol, PROTOCOL_MK1_VERSION, PROTOCOL_MK2_VERSION, versioned, -}; +use rivet_runner_protocol::{self as protocol, PROTOCOL_MK2_VERSION, versioned}; use std::sync::Arc; use tokio::sync::watch; use universalpubsub as ups; @@ -48,7 +46,7 @@ async fn recv_msg( ) -> Result> { let tunnel_msg = tokio::select! { res = tunnel_sub.next() => { - if let NextOutput::Message(tunnel_msg) = res.context("pubsub_to_client_task sub failed")? { + if let NextOutput::Message(tunnel_msg) = res? { tunnel_msg } else { tracing::debug!("tunnel sub closed"); 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 7bb98b9466..44e71ac7b8 100644 --- a/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs +++ b/engine/packages/pegboard-runner/src/ws_to_tunnel_task.rs @@ -3,17 +3,15 @@ use bytes::Bytes; use futures_util::TryStreamExt; use gas::prelude::Id; use gas::prelude::*; -use hyper_tungstenite::tungstenite::Message as WsMessage; use hyper_tungstenite::tungstenite::Message; use pegboard::pubsub_subjects::GatewayReceiverSubject; use pegboard::utils::event_actor_id; use pegboard_actor_kv as kv; use rivet_guard_core::websocket_handle::WebSocketReceiver; -use rivet_runner_protocol::{ - self as protocol, PROTOCOL_MK1_VERSION, PROTOCOL_MK2_VERSION, versioned, -}; +use rivet_runner_protocol::{self as protocol, PROTOCOL_MK2_VERSION, versioned}; use std::sync::{Arc, atomic::Ordering}; use tokio::sync::{Mutex, MutexGuard, watch}; +use universaldb::utils::end_of_key_range; use universalpubsub::PublishOpts; use universalpubsub::Subscriber; use vbare::OwnedVersionedData; @@ -28,7 +26,7 @@ pub async fn task( eviction_sub2: Subscriber, ws_to_tunnel_abort_rx: watch::Receiver<()>, ) -> Result { - let mut event_demuxer = ActorEventDemuxer::new(ctx.clone()); + let mut event_demuxer = ActorEventDemuxer::new(ctx.clone(), conn.runner_id); let res = task_inner( ctx, @@ -97,7 +95,7 @@ async fn recv_msg( }; match msg { - WsMessage::Binary(data) => { + Message::Binary(data) => { tracing::trace!( data_len = data.len(), "received binary message from WebSocket" @@ -105,7 +103,7 @@ async fn recv_msg( Ok(Ok(Some(data))) } - WsMessage::Close(_) => { + Message::Close(_) => { tracing::debug!("websocket closed"); return Ok(Err(LifecycleResult::Closed)); } @@ -388,67 +386,22 @@ async fn handle_message_mk2( } } protocol::mk2::ToServer::ToServerTunnelMessage(tunnel_msg) => { - handle_tunnel_message_mk2(&ctx, &conn, tunnel_msg) + handle_tunnel_message_mk2(&ctx, tunnel_msg) .await .context("failed to handle tunnel message")?; } - protocol::mk2::ToServer::ToServerInit(init) => { - // We send the signal first because we don't want to continue if this fails - ctx.signal(pegboard::workflows::runner2::Init {}) - .to_workflow_id(conn.workflow_id) - .send() - .await - .with_context(|| { - format!( - "failed to send signal to runner workflow: {}", - conn.workflow_id - ) - })?; - - let init_data = ctx - .activity(ProcessInitInput { - runner_id: conn.runner_id, - namespace_id: conn.namespace_id, - last_command_idx: init.last_command_idx.unwrap_or(-1), - prepopulate_actor_names: init.prepopulate_actor_names, - metadata: init.metadata, - }) - .await?; - - // Send init packet - let init_msg = versioned::ToClientMk2::wrap_latest( - protocol::mk2::ToClient::ToClientInit(protocol::mk2::ToClientInit { - runner_id: conn.runner_id.to_string(), - last_event_idx: init_data.last_event_idx, - metadata: protocol::mk2::ProtocolMetadata { - runner_lost_threshold: ctx.config().pegboard().runner_lost_threshold(), - }, - }), - ); - let init_msg_serialized = init_msg.serialize(conn.protocol_version)?; - conn.ws_handle - .send(Message::Binary(init_msg_serialized.into())) - .await?; - - // Send missed commands - if !init_data.missed_commands.is_empty() { - let msg = versioned::ToClientMk2::wrap_latest( - protocol::mk2::ToClient::ToClientCommands(init_data.missed_commands), - ); - let msg_serialized = msg.serialize(conn.protocol_version)?; - conn.ws_handle - .send(Message::Binary(msg_serialized.into())) - .await?; - } + // NOTE: This does not process the first init event. See `conn::init_conn` + protocol::mk2::ToServer::ToServerInit(_) => { + tracing::debug!("received additional init packet, ignoring"); } // Forward to actor wf protocol::mk2::ToServer::ToServerEvents(events) => { for event in events { - event_demuxer.ingest(Id::parse(event_actor_id(&event.inner))?, event.inner); + event_demuxer.ingest(Id::parse(event_actor_id(&event.inner))?, event); } } - protocol::mk2::ToServer::ToServerAckCommands(_) => { - ack_commands(&ctx).await?; + protocol::mk2::ToServer::ToServerAckCommands(ack) => { + ack_commands(&ctx, conn.runner_id, ack).await?; } protocol::mk2::ToServer::ToServerStopping => { ctx.signal(pegboard::workflows::runner2::Stop { @@ -793,21 +746,43 @@ async fn handle_message_mk1(ctx: &StandaloneCtx, conn: &Conn, msg: Bytes) -> Res Ok(()) } -async fn ack_commands(ctx: &StandaloneCtx) -> Result<()> { - // ctx.udb()?.run(|| { - // let last_ack = ; - // let stream = .read_ranges_keyvalues({ - // limit: - // }); - // }).await?; +async fn ack_commands( + ctx: &StandaloneCtx, + runner_id: Id, + ack: protocol::mk2::ToServerAckCommands, +) -> Result<()> { + ctx.udb()? + .run(|tx| { + let ack = ack.clone(); + async move { + let tx = tx.with_subspace(pegboard::keys::subspace()); + + for checkpoint in &ack.last_command_checkpoints { + let start = tx.pack( + &pegboard::keys::runner::ActorCommandKey::subspace_with_actor( + runner_id, + Id::parse(&checkpoint.actor_id)?, + ), + ); + let end = end_of_key_range(&tx.pack( + &pegboard::keys::runner::ActorCommandKey::subspace_with_index( + runner_id, + Id::parse(&checkpoint.actor_id)?, + checkpoint.index, + ), + )); + tx.clear_range(&start, &end); + } - todo!(); + Ok(()) + } + }) + .await } #[tracing::instrument(skip_all)] async fn handle_tunnel_message_mk2( ctx: &StandaloneCtx, - conn: &Conn, msg: protocol::mk2::ToServerTunnelMessage, ) -> Result<()> { // Publish message to UPS diff --git a/engine/packages/pegboard-serverless/src/lib.rs b/engine/packages/pegboard-serverless/src/lib.rs index 720d43461d..c2c1c7bbf9 100644 --- a/engine/packages/pegboard-serverless/src/lib.rs +++ b/engine/packages/pegboard-serverless/src/lib.rs @@ -70,9 +70,7 @@ async fn tick( .run(|tx| async move { let tx = tx.with_subspace(keys::subspace()); - let serverless_desired_subspace = keys::subspace().subspace( - &rivet_types::keys::pegboard::ns::ServerlessDesiredSlotsKey::entire_subspace(), - ); + let serverless_desired_subspace = keys::subspace().subspace(&rivet_types::keys::pegboard::ns::ServerlessDesiredSlotsKey::entire_subspace()); tx.get_ranges_keyvalues( universaldb::RangeOption { @@ -84,8 +82,7 @@ async fn tick( ) .map(|res| match res { Ok(entry) => { - let (key, desired_slots) = - tx.read_entry::(&entry)?; + let (key, desired_slots) = tx.read_entry::(&entry)?; Ok((key.namespace_id, key.runner_name, desired_slots)) } @@ -372,9 +369,7 @@ async fn outbound_handler( if runner_id.is_none() { let data = BASE64.decode(msg.data).context("invalid base64 message")?; - let payload = - protocol::versioned::ToServerlessServer::deserialize_with_embedded_version(&data) - .context("invalid payload")?; + let payload = protocol::versioned::ToServerlessServer::deserialize_with_embedded_version(&data).context("invalid payload")?; match payload { protocol::mk2::ToServerlessServer::ToServerlessServerInit(init) => { @@ -437,11 +432,7 @@ async fn outbound_handler( // send it now if runner_id.is_none() { let data = BASE64.decode(msg.data).context("invalid base64 message")?; - let payload = - protocol::versioned::ToServerlessServer::deserialize_with_embedded_version( - &data, - ) - .context("invalid payload")?; + let payload = protocol::versioned::ToServerlessServer::deserialize_with_embedded_version(&data).context("invalid payload")?; match payload { protocol::mk2::ToServerlessServer::ToServerlessServerInit(init) => { diff --git a/engine/packages/pegboard/src/keys/runner.rs b/engine/packages/pegboard/src/keys/runner.rs index e51555e4c4..771350871c 100644 --- a/engine/packages/pegboard/src/keys/runner.rs +++ b/engine/packages/pegboard/src/keys/runner.rs @@ -860,3 +860,194 @@ impl<'de> TupleUnpack<'de> for MetadataChunkKey { Ok((input, v)) } } + +#[derive(Debug)] +pub struct ActorLastCommandIdxKey { + runner_id: Id, + actor_id: Id, +} + +impl ActorLastCommandIdxKey { + pub fn new(runner_id: Id, actor_id: Id) -> Self { + ActorLastCommandIdxKey { + runner_id, + actor_id, + } + } +} + +impl FormalKey for ActorLastCommandIdxKey { + // Timestamp. + type Value = i64; + + fn deserialize(&self, raw: &[u8]) -> Result { + Ok(i64::from_be_bytes(raw.try_into()?)) + } + + fn serialize(&self, value: Self::Value) -> Result> { + Ok(value.to_be_bytes().to_vec()) + } +} + +impl TuplePack for ActorLastCommandIdxKey { + fn pack( + &self, + w: &mut W, + tuple_depth: TupleDepth, + ) -> std::io::Result { + let t = ( + RUNNER, + DATA, + self.runner_id, + ACTOR, + LAST_COMMAND_IDX, + self.actor_id, + ); + t.pack(w, tuple_depth) + } +} + +impl<'de> TupleUnpack<'de> for ActorLastCommandIdxKey { + fn unpack(input: &[u8], tuple_depth: TupleDepth) -> PackResult<(&[u8], Self)> { + let (input, (_, _, runner_id, _, _, actor_id)) = + <(usize, usize, Id, usize, usize, Id)>::unpack(input, tuple_depth)?; + let v = ActorLastCommandIdxKey { + runner_id, + actor_id, + }; + + Ok((input, v)) + } +} + +#[derive(Debug)] +pub struct ActorCommandKey { + pub runner_id: Id, + pub actor_id: Id, + pub index: i64, +} + +impl ActorCommandKey { + pub fn new(runner_id: Id, actor_id: Id, index: i64) -> Self { + ActorCommandKey { + runner_id, + actor_id, + index, + } + } + + pub fn subspace(runner_id: Id) -> ActorCommandSubspaceKey { + ActorCommandSubspaceKey::new(runner_id) + } + + pub fn subspace_with_actor(runner_id: Id, actor_id: Id) -> ActorCommandSubspaceKey { + ActorCommandSubspaceKey::new_with_actor(runner_id, actor_id) + } + + pub fn subspace_with_index(runner_id: Id, actor_id: Id, index: i64) -> ActorCommandSubspaceKey { + ActorCommandSubspaceKey::new_with_index(runner_id, actor_id, index) + } +} + +impl FormalKey for ActorCommandKey { + type Value = rivet_runner_protocol::mk2::ActorCommandKeyData; + + fn deserialize(&self, raw: &[u8]) -> Result { + rivet_runner_protocol::versioned::ActorCommandKeyData::deserialize_with_embedded_version( + raw, + ) + } + + fn serialize(&self, value: Self::Value) -> Result> { + rivet_runner_protocol::versioned::ActorCommandKeyData::wrap_latest(value) + .serialize_with_embedded_version(rivet_runner_protocol::PROTOCOL_MK2_VERSION) + } +} + +impl TuplePack for ActorCommandKey { + fn pack( + &self, + w: &mut W, + tuple_depth: TupleDepth, + ) -> std::io::Result { + let t = ( + RUNNER, + DATA, + self.runner_id, + ACTOR, + COMMAND, + self.actor_id, + self.index, + ); + t.pack(w, tuple_depth) + } +} + +impl<'de> TupleUnpack<'de> for ActorCommandKey { + fn unpack(input: &[u8], tuple_depth: TupleDepth) -> PackResult<(&[u8], Self)> { + let (input, (_, _, runner_id, _, _, actor_id, index)) = + <(usize, usize, Id, usize, usize, Id, i64)>::unpack(input, tuple_depth)?; + let v = ActorCommandKey { + runner_id, + actor_id, + index, + }; + + Ok((input, v)) + } +} + +pub struct ActorCommandSubspaceKey { + runner_id: Id, + actor_id: Option, + index: Option, +} + +impl ActorCommandSubspaceKey { + pub fn new(runner_id: Id) -> Self { + ActorCommandSubspaceKey { + runner_id, + actor_id: None, + index: None, + } + } + + pub fn new_with_actor(runner_id: Id, actor_id: Id) -> Self { + ActorCommandSubspaceKey { + runner_id, + actor_id: Some(actor_id), + index: None, + } + } + + pub fn new_with_index(runner_id: Id, actor_id: Id, index: i64) -> Self { + ActorCommandSubspaceKey { + runner_id, + actor_id: Some(actor_id), + index: Some(index), + } + } +} + +impl TuplePack for ActorCommandSubspaceKey { + fn pack( + &self, + w: &mut W, + tuple_depth: TupleDepth, + ) -> std::io::Result { + let mut offset = VersionstampOffset::None { size: 0 }; + + let t = (RUNNER, DATA, self.runner_id, ACTOR, COMMAND); + offset += t.pack(w, tuple_depth)?; + + if let Some(actor_id) = &self.actor_id { + offset += actor_id.pack(w, tuple_depth)?; + + if let Some(index) = &self.index { + offset += index.pack(w, tuple_depth)?; + } + } + + Ok(offset) + } +} diff --git a/engine/packages/pegboard/src/ops/runner/update_alloc_idx.rs b/engine/packages/pegboard/src/ops/runner/update_alloc_idx.rs index c720ad831f..ff42c4f9fa 100644 --- a/engine/packages/pegboard/src/ops/runner/update_alloc_idx.rs +++ b/engine/packages/pegboard/src/ops/runner/update_alloc_idx.rs @@ -1,4 +1,5 @@ use gas::prelude::*; +use rivet_runner_protocol::PROTOCOL_MK1_VERSION; use universaldb::options::ConflictRangeType; use universaldb::utils::IsolationLevel::*; @@ -99,7 +100,6 @@ pub async fn pegboard_runner_update_alloc_idx(ctx: &OperationCtx, input: &Input) Some(version), Some(remaining_slots), Some(total_slots), - Some(protocol_version), Some(old_last_ping_ts), ) = ( workflow_id_entry, @@ -108,7 +108,6 @@ pub async fn pegboard_runner_update_alloc_idx(ctx: &OperationCtx, input: &Input) version_entry, remaining_slots_entry, total_slots_entry, - protocol_version_entry, last_ping_ts_entry, ) else { @@ -116,6 +115,8 @@ pub async fn pegboard_runner_update_alloc_idx(ctx: &OperationCtx, input: &Input) continue; }; + let protocol_version = protocol_version_entry.unwrap_or(PROTOCOL_MK1_VERSION); + // Runner is expired, AddIdx is invalid and UpdatePing will do nothing if expired_ts_entry.is_some() { match runner.action { diff --git a/engine/packages/pegboard/src/utils.rs b/engine/packages/pegboard/src/utils.rs index 0bf3e91be7..7897c86478 100644 --- a/engine/packages/pegboard/src/utils.rs +++ b/engine/packages/pegboard/src/utils.rs @@ -30,7 +30,24 @@ pub fn event_actor_id_mk1(event: &protocol::Event) -> &str { } } -pub fn event_generation(event: &protocol::Event) -> u32 { +pub fn event_generation(event: &protocol::mk2::Event) -> u32 { + match event { + protocol::mk2::Event::EventActorIntent(protocol::mk2::EventActorIntent { + generation, + .. + }) => *generation, + protocol::mk2::Event::EventActorStateUpdate(protocol::mk2::EventActorStateUpdate { + generation, + .. + }) => *generation, + protocol::mk2::Event::EventActorSetAlarm(protocol::mk2::EventActorSetAlarm { + generation, + .. + }) => *generation, + } +} + +pub fn event_generation_mk1(event: &protocol::Event) -> u32 { match event { protocol::Event::EventActorIntent(protocol::EventActorIntent { generation, .. }) => { *generation diff --git a/engine/packages/pegboard/src/workflows/actor/destroy.rs b/engine/packages/pegboard/src/workflows/actor/destroy.rs index e7391a6164..74bc8434d6 100644 --- a/engine/packages/pegboard/src/workflows/actor/destroy.rs +++ b/engine/packages/pegboard/src/workflows/actor/destroy.rs @@ -72,30 +72,37 @@ async fn update_state_and_db( ctx.udb()? .run(|tx| { - let state = (*state).clone(); + let runner_id = state.runner_id.clone(); + let namespace_id = state.namespace_id.clone(); + let runner_name_selector = state.runner_name_selector.clone(); + let for_serverless = state.for_serverless.clone(); + let allocated_serverless_slot = state.allocated_serverless_slot.clone(); + let name = state.name.clone(); + let create_ts = state.create_ts.clone(); + let key = state.key.clone(); async move { let tx = tx.with_subspace(keys::subspace()); tx.write(&keys::actor::DestroyTsKey::new(input.actor_id), destroy_ts)?; - if let Some(runner_id) = state.runner_id { + if let Some(runner_id) = runner_id { clear_slot( input.actor_id, - state.namespace_id, - &state.runner_name_selector, + namespace_id, + &runner_name_selector, runner_id, - state.for_serverless, + for_serverless, &tx, ) .await?; - } else if state.allocated_serverless_slot { + } else if allocated_serverless_slot { // Clear the serverless slot even if we do not have a runner id. This happens when the // actor is destroyed while pending allocation tx.atomic_op( &rivet_types::keys::pegboard::ns::ServerlessDesiredSlotsKey::new( - state.namespace_id, - state.runner_name_selector.clone(), + namespace_id, + runner_name_selector.clone(), ), &(-1i64).to_le_bytes(), MutationType::Add, @@ -104,19 +111,19 @@ async fn update_state_and_db( // Update namespace indexes tx.delete(&keys::ns::ActiveActorKey::new( - state.namespace_id, - state.name.clone(), - state.create_ts, + namespace_id, + name.clone(), + create_ts, input.actor_id, )); - if let Some(k) = &state.key { + if let Some(k) = &key { tx.write( &keys::ns::ActorByKeyKey::new( - state.namespace_id, - state.name.clone(), + namespace_id, + name.clone(), k.clone(), - state.create_ts, + create_ts, input.actor_id, ), ActorByKeyKeyData { diff --git a/engine/packages/pegboard/src/workflows/actor/mod.rs b/engine/packages/pegboard/src/workflows/actor/mod.rs index 79df340e42..24eccbc569 100644 --- a/engine/packages/pegboard/src/workflows/actor/mod.rs +++ b/engine/packages/pegboard/src/workflows/actor/mod.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use futures_util::FutureExt; use gas::prelude::*; use rivet_runner_protocol as protocol; @@ -12,6 +14,9 @@ mod setup; pub use runtime::AllocationOverride; +/// Batch size of how many events to ack. +const EVENT_ACK_BATCH_SIZE: i64 = 250; + #[derive(Clone, Debug, Serialize, Deserialize, Hash)] pub struct Input { pub actor_id: Id, @@ -26,7 +31,7 @@ pub struct Input { pub input: Option, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize)] pub struct State { pub name: String, pub key: Option, @@ -57,6 +62,8 @@ pub struct State { // Null if not allocated pub runner_id: Option, pub runner_workflow_id: Option, + #[serde(default)] + pub runner_states: HashMap, } impl State { @@ -92,6 +99,20 @@ impl State { runner_id: None, runner_workflow_id: None, + runner_states: HashMap::new(), + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct RunnerState { + pub last_command_idx: i64, +} + +impl Default for RunnerState { + fn default() -> Self { + RunnerState { + last_command_idx: -1, } } } @@ -250,56 +271,67 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> }; let lifecycle_res = ctx - .loope( - lifecycle_state, - |ctx, state| { - let input = input.clone(); - - async move { - let sig = if let Some(gc_timeout_ts) = state.gc_timeout_ts { - // Listen for signal with gc timeout. if a timeout happens, it means this actor is lost - if let Some(sig) = ctx.listen_until::
(gc_timeout_ts).await? { - sig - } else { - tracing::warn!(actor_id=?input.actor_id, "actor lost"); - - // Fake signal - Main::Lost(Lost { - generation: state.generation, - force_reschedule: false, - reset_rescheduling: false, - }) - } - } else if let Some(alarm_ts) = state.alarm_ts { - // Listen for signal with timeout. if a timeout happens, it means this actor should - // wake up - if let Some(sig) = ctx.listen_until::
(alarm_ts).await? { - sig - } else { - tracing::debug!(actor_id=?input.actor_id, "actor wake"); - - // Fake signal - Main::Wake(Wake { allocation_override: AllocationOverride::DontSleep { pending_timeout: None } }) - } + .loope(lifecycle_state, |ctx, state| { + let input = input.clone(); + + async move { + let signals = if let Some(gc_timeout_ts) = state.gc_timeout_ts { + // Listen for signals with gc timeout. if a timeout happens, it means this actor is lost + let signals = ctx.listen_n_until::
(gc_timeout_ts, 1024).await?; + if signals.is_empty() { + tracing::warn!(actor_id=?input.actor_id, "actor lost"); + + // Fake signal + vec![Main::Lost(Lost { + generation: state.generation, + force_reschedule: false, + reset_rescheduling: false, + })] + } else { + signals + } + } else if let Some(alarm_ts) = state.alarm_ts { + // Listen for signals with timeout. if a timeout happens, it means this actor should + // wake up + let signals = ctx.listen_n_until::
(alarm_ts, 1024).await?; + if signals.is_empty() { + tracing::debug!(actor_id=?input.actor_id, "actor wake"); + + // Fake signal + vec![Main::Wake(Wake { + allocation_override: AllocationOverride::DontSleep { + pending_timeout: None, + }, + })] } else { - // Listen for signal normally - ctx.listen::
().await? - }; + signals + } + } else { + // Listen for signals normally + ctx.listen_n::
(1024).await? + }; + for sig in signals { match sig { + // NOTE: This is only received when allocated to mk1 runner Main::Event(sig) => { - // Ignore state updates for previous generations - if crate::utils::event_generation(&sig.inner) != state.generation { - return Ok(Loop::Continue); - } - - let (Some(runner_id), Some(runner_workflow_id), Some(runner_protocol_version)) = - (state.runner_id, state.runner_workflow_id, state.runner_protocol_version) + let ( + Some(runner_id), + Some(runner_workflow_id), + ) = ( + state.runner_id, + state.runner_workflow_id, + ) else { tracing::warn!("actor not allocated, ignoring event"); - return Ok(Loop::Continue); + continue; }; + // Ignore events for previous generations + if crate::utils::event_generation_mk1(&sig.inner) != state.generation { + continue; + } + match sig.inner { protocol::Event::EventActorIntent(protocol::EventActorIntent { intent, @@ -321,22 +353,18 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> }) .await?; - if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel - } else { - // Send signal to stop actor now that we know it will be sleeping - ctx.signal(crate::workflows::runner::Command { - inner: protocol::Command::CommandStopActor( - protocol::CommandStopActor { - actor_id: input.actor_id.to_string(), - generation: state.generation, - }, - ), - }) - .to_workflow_id(runner_workflow_id) - .send() - .await?; - } + // Send signal to stop actor now that we know it will be sleeping + ctx.signal(crate::workflows::runner::Command { + inner: protocol::Command::CommandStopActor( + protocol::CommandStopActor { + actor_id: input.actor_id.to_string(), + generation: state.generation, + }, + ), + }) + .to_workflow_id(runner_workflow_id) + .send() + .await?; } } protocol::ActorIntent::ActorIntentStop => { @@ -355,21 +383,17 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> }) .await?; - if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel - } else { - ctx.signal(crate::workflows::runner::Command { - inner: protocol::Command::CommandStopActor( - protocol::CommandStopActor { - actor_id: input.actor_id.to_string(), - generation: state.generation, - }, - ), - }) - .to_workflow_id(runner_workflow_id) - .send() - .await?; - } + ctx.signal(crate::workflows::runner::Command { + inner: protocol::Command::CommandStopActor( + protocol::CommandStopActor { + actor_id: input.actor_id.to_string(), + generation: state.generation, + }, + ), + }) + .to_workflow_id(runner_workflow_id) + .send() + .await?; } } }, @@ -398,7 +422,12 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> ctx, &input, state, - StoppedVariant::Normal { code }, + StoppedVariant::Normal { + code: match code { + protocol::StopCode::Ok => protocol::mk2::StopCode::Ok, + protocol::StopCode::Error => protocol::mk2::StopCode::Error, + } + }, ) .await? { @@ -415,6 +444,168 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> } } } + // NOTE: This signal is only received when allocated to a mk2 runner + Main::Events(sig) => { + let Some(runner_id) = state.runner_id else { + tracing::warn!("actor not allocated, ignoring events"); + continue; + }; + + if sig.runner_id != runner_id { + tracing::debug!("events not from current runner, ignoring"); + continue; + } + + // Fetch the last event index for the current runner + let last_event_idx = + state.runner_states.entry(runner_id).or_default().last_event_idx; + + // Filter already received events and events from previous generations + let generation = state.generation; + let new_events = sig.events + .iter() + .filter(|event| { + event.checkpoint.index > last_event_idx && + crate::utils::event_generation(&event.inner) == generation + }); + let new_last_event_idx = + new_events.clone().last().map(|event| event.checkpoint.index); + + for event in new_events { + match &event.inner { + protocol::mk2::Event::EventActorIntent( + protocol::mk2::EventActorIntent { intent, .. }, + ) => match intent { + protocol::mk2::ActorIntent::ActorIntentSleep => { + if !state.sleeping { + state.gc_timeout_ts = Some( + util::timestamp::now() + + ctx + .config() + .pegboard() + .actor_stop_threshold(), + ); + state.sleeping = true; + + ctx.activity(runtime::SetSleepingInput { + actor_id: input.actor_id, + }) + .await?; + + ctx.activity(runtime::InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id, + commands: vec![protocol::mk2::Command::CommandStopActor( + protocol::mk2::CommandStopActor { + generation: state.generation, + }, + )], + }) + .await?; + } + } + protocol::mk2::ActorIntent::ActorIntentStop => { + if !state.stopping { + state.gc_timeout_ts = Some( + util::timestamp::now() + + ctx + .config() + .pegboard() + .actor_stop_threshold(), + ); + state.stopping = true; + + ctx.activity(runtime::SetNotConnectableInput { + actor_id: input.actor_id, + }) + .await?; + + ctx.activity(runtime::InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id, + commands: vec![protocol::mk2::Command::CommandStopActor( + protocol::mk2::CommandStopActor { + generation: state.generation, + }, + )], + }) + .await?; + } + } + }, + protocol::mk2::Event::EventActorStateUpdate( + protocol::mk2::EventActorStateUpdate { + state: actor_state, + .. + }, + ) => match actor_state { + protocol::mk2::ActorState::ActorStateRunning => { + state.gc_timeout_ts = None; + + ctx.activity(runtime::SetStartedInput { + actor_id: input.actor_id, + }) + .await?; + + ctx.msg(Ready { runner_id }) + .tag("actor_id", input.actor_id) + .send() + .await?; + } + protocol::mk2::ActorState::ActorStateStopped( + protocol::mk2::ActorStateStopped { code, .. }, + ) => { + if let StoppedResult::Destroy = handle_stopped( + ctx, + &input, + state, + StoppedVariant::Normal { code: code.clone() }, + ) + .await? + { + return Ok(Loop::Break(runtime::LifecycleResult { + generation: state.generation, + })); + } + } + }, + protocol::mk2::Event::EventActorSetAlarm( + protocol::mk2::EventActorSetAlarm { alarm_ts, .. }, + ) => { + state.alarm_ts = *alarm_ts; + } + } + } + + if let Some(new_last_event_idx) = new_last_event_idx { + // Reborrow for mutability guarantees + let runner_state = state.runner_states.entry(runner_id).or_default(); + + runner_state.last_event_idx = runner_state.last_event_idx.max(new_last_event_idx); + + // Ack events in batch + if runner_state.last_event_idx + > runner_state.last_event_ack_idx.saturating_add(EVENT_ACK_BATCH_SIZE) + { + runner_state.last_event_ack_idx = runner_state.last_event_idx; + + ctx.activity(runtime::SendMessagesToRunnerInput { + runner_id, + messages: vec![protocol::mk2::ToRunner::ToClientAckEvents( + protocol::mk2::ToClientAckEvents { + last_event_checkpoints: vec![ + protocol::mk2::ActorCheckpoint { + actor_id: input.actor_id.to_string(), + index: runner_state.last_event_ack_idx, + }, + ], + }, + )], + }) + .await?; + } + } + } Main::Wake(sig) => { if state.sleeping { if state.runner_id.is_none() { @@ -422,8 +613,13 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> state.sleeping = false; state.will_wake = false; - match runtime::reschedule_actor(ctx, &input, state, sig.allocation_override) - .await? + match runtime::reschedule_actor( + ctx, + &input, + state, + sig.allocation_override, + ) + .await? { runtime::SpawnActorOutput::Allocated { .. } => {} runtime::SpawnActorOutput::Sleep => { @@ -454,7 +650,7 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> Main::Lost(sig) => { // Ignore signals for previous generations if sig.generation != state.generation { - return Ok(Loop::Continue); + continue; } if sig.reset_rescheduling { @@ -479,7 +675,7 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> Main::GoingAway(sig) => { // Ignore signals for previous generations if sig.generation != state.generation { - return Ok(Loop::Continue); + continue; } if sig.reset_rescheduling { @@ -487,8 +683,10 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> } if !state.going_away { - let (Some(runner_workflow_id), Some(runner_protocol_version)) = (state.runner_workflow_id, state.runner_protocol_version) else { - return Ok(Loop::Continue); + let (Some(runner_id), Some(runner_workflow_id), Some(runner_protocol_version)) = + (state.runner_id, state.runner_workflow_id, state.runner_protocol_version) + else { + continue; }; state.gc_timeout_ts = Some( @@ -503,13 +701,24 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> .await?; if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(runtime::InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id, + commands: vec![protocol::mk2::Command::CommandStopActor( + protocol::mk2::CommandStopActor { + generation: state.generation, + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { - inner: protocol::Command::CommandStopActor(protocol::CommandStopActor { - actor_id: input.actor_id.to_string(), - generation: state.generation, - }), + inner: protocol::Command::CommandStopActor( + protocol::CommandStopActor { + actor_id: input.actor_id.to_string(), + generation: state.generation, + }, + ), }) .to_workflow_id(runner_workflow_id) .send() @@ -519,15 +728,28 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> } Main::Destroy(_) => { // If allocated, send stop actor command - if let (Some(runner_workflow_id), Some(runner_protocol_version)) = (state.runner_workflow_id, state.runner_protocol_version) { + if let (Some(runner_id), Some(runner_workflow_id), Some(runner_protocol_version)) = + (state.runner_id, state.runner_workflow_id, state.runner_protocol_version) + { if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(runtime::InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id, + commands: vec![protocol::mk2::Command::CommandStopActor( + protocol::mk2::CommandStopActor { + generation: state.generation, + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { - inner: protocol::Command::CommandStopActor(protocol::CommandStopActor { - actor_id: input.actor_id.to_string(), - generation: state.generation, - }), + inner: protocol::Command::CommandStopActor( + protocol::CommandStopActor { + actor_id: input.actor_id.to_string(), + generation: state.generation, + }, + ), }) .to_workflow_id(runner_workflow_id) .send() @@ -540,12 +762,12 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> })); } } - - Ok(Loop::Continue) } - .boxed() - }, - ) + + Ok(Loop::Continue) + } + .boxed() + }) .await?; // At this point, the actor is not allocated so no cleanup related to alloc idx/desired slots needs to be @@ -566,7 +788,7 @@ pub async fn pegboard_actor(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> #[derive(Debug)] enum StoppedVariant { - Normal { code: protocol::StopCode }, + Normal { code: protocol::mk2::StopCode }, Lost { force_reschedule: bool }, } @@ -586,7 +808,7 @@ async fn handle_stopped( let force_reschedule = match &variant { StoppedVariant::Normal { code } => { // Reset retry count on successful exit - if let protocol::StopCode::Ok = code { + if let protocol::mk2::StopCode::Ok = code { state.reschedule_state = Default::default(); } @@ -598,7 +820,7 @@ async fn handle_stopped( // Clear stop gc timeout to prevent being marked as lost in the lifecycle loop state.gc_timeout_ts = None; state.stopping = false; - state.runner_id = None; + let old_runner_id = state.runner_id.take(); let old_runner_workflow_id = state.runner_workflow_id.take(); let old_runner_protocol_version = state.runner_protocol_version.take(); @@ -641,15 +863,26 @@ async fn handle_stopped( // command in case it ended up allocating if let ( StoppedVariant::Lost { .. }, + Some(old_runner_id), Some(old_runner_workflow_id), Some(old_runner_protocol_version), ) = ( &variant, + old_runner_id, old_runner_workflow_id, old_runner_protocol_version, ) { if protocol::is_mk2(old_runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(runtime::InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id: old_runner_id, + commands: vec![protocol::mk2::Command::CommandStopActor( + protocol::mk2::CommandStopActor { + generation: state.generation, + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { inner: protocol::Command::CommandStopActor(protocol::CommandStopActor { @@ -695,7 +928,7 @@ async fn handle_stopped( && matches!( variant, StoppedVariant::Normal { - code: protocol::StopCode::Ok + code: protocol::mk2::StopCode::Ok } ); @@ -752,6 +985,21 @@ async fn handle_stopped( .send() .await?; + let res = ctx + .activity(runtime::CheckRunnersInput { + seen_runners: state + .runner_states + .iter() + .map(|(runner_id, _)| *runner_id) + .collect(), + }) + .await?; + + // Clean up state + for runner_id in res.remove_runners { + state.runner_states.remove(&runner_id); + } + Ok(StoppedResult::Continue) } @@ -787,7 +1035,8 @@ pub struct Event { #[signal("pegboard_actor_events")] pub struct Events { - pub inner: Vec, + pub runner_id: Id, + pub events: Vec, } #[signal("pegboard_actor_wake")] @@ -832,9 +1081,11 @@ join_signal!(PendingAllocation { }); join_signal!(Main { - Event(Event), + Event, + Events, Wake, Lost, GoingAway, Destroy, + // Comment to prevent invalid formatting }); diff --git a/engine/packages/pegboard/src/workflows/actor/runtime.rs b/engine/packages/pegboard/src/workflows/actor/runtime.rs index 299e8da410..690a3d695d 100644 --- a/engine/packages/pegboard/src/workflows/actor/runtime.rs +++ b/engine/packages/pegboard/src/workflows/actor/runtime.rs @@ -4,19 +4,39 @@ use futures_util::StreamExt; use futures_util::TryStreamExt; use gas::prelude::*; use rivet_metrics::KeyValue; -use rivet_runner_protocol::{self as protocol, PROTOCOL_MK1_VERSION}; +use rivet_runner_protocol::{ + self as protocol, PROTOCOL_MK1_VERSION, PROTOCOL_MK2_VERSION, versioned, +}; use rivet_types::{ actors::CrashPolicy, keys::namespace::runner_config::RunnerConfigVariant, runner_configs::RunnerConfigKind, }; +use std::collections::HashMap; use std::time::Instant; use universaldb::options::{ConflictRangeType, MutationType, StreamingMode}; use universaldb::utils::{FormalKey, IsolationLevel::*}; +use universalpubsub::PublishOpts; +use vbare::OwnedVersionedData; use crate::{keys, metrics}; use super::{Allocate, Destroy, Input, PendingAllocation, State, destroy}; +#[derive(Deserialize, Serialize)] +pub struct LifecycleRunnerState { + pub last_event_idx: i64, + pub last_event_ack_idx: i64, +} + +impl Default for LifecycleRunnerState { + fn default() -> Self { + LifecycleRunnerState { + last_event_idx: -1, + last_event_ack_idx: -1, + } + } +} + // TODO: Rewrite this as a series of nested structs/enums for better transparency of current state (likely // requires actor wf v2) #[derive(Deserialize, Serialize)] @@ -27,6 +47,8 @@ pub struct LifecycleState { pub runner_id: Option, pub runner_workflow_id: Option, pub runner_protocol_version: Option, + #[serde(default)] + pub runner_states: HashMap, pub sleeping: bool, #[serde(default)] @@ -58,6 +80,7 @@ impl LifecycleState { runner_id: Some(runner_id), runner_workflow_id: Some(runner_workflow_id), runner_protocol_version: Some(runner_protocol_version), + runner_states: HashMap::new(), sleeping: false, stopping: false, going_away: false, @@ -74,6 +97,7 @@ impl LifecycleState { runner_id: None, runner_workflow_id: None, runner_protocol_version: None, + runner_states: HashMap::new(), sleeping: true, stopping: false, going_away: false, @@ -537,7 +561,31 @@ pub async fn spawn_actor( .await?; if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id, + commands: vec![protocol::mk2::Command::CommandStartActor( + protocol::mk2::CommandStartActor { + generation, + config: protocol::mk2::ActorConfig { + name: input.name.clone(), + key: input.key.clone(), + // HACK: We should not use dynamic timestamp here, but we don't validate if signal data + // changes (like activity inputs) so this is fine for now. + create_ts: util::timestamp::now(), + input: input + .input + .as_ref() + .map(|x| BASE64_STANDARD.decode(x)) + .transpose()?, + }, + // Empty because request ids are ephemeral. This is intercepted by guard and + // populated before it reaches the runner + hibernating_requests: Vec::new(), + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { inner: protocol::Command::CommandStartActor(protocol::CommandStartActor { @@ -604,7 +652,29 @@ pub async fn spawn_actor( .await?; if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id: sig.runner_id, + commands: vec![protocol::mk2::Command::CommandStartActor( + protocol::mk2::CommandStartActor { + generation, + config: protocol::mk2::ActorConfig { + name: input.name.clone(), + key: input.key.clone(), + create_ts: util::timestamp::now(), + input: input + .input + .as_ref() + .map(|x| BASE64_STANDARD.decode(x)) + .transpose()?, + }, + // Empty because request ids are ephemeral. This is intercepted by guard and + // populated before it reaches the runner + hibernating_requests: Vec::new(), + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { inner: protocol::Command::CommandStartActor( @@ -692,7 +762,29 @@ pub async fn spawn_actor( .await?; if protocol::is_mk2(runner_protocol_version) { - // TODO: Send message to tunnel + ctx.activity(InsertAndSendCommandsInput { + actor_id: input.actor_id, + runner_id: sig.runner_id, + commands: vec![protocol::mk2::Command::CommandStartActor( + protocol::mk2::CommandStartActor { + generation, + config: protocol::mk2::ActorConfig { + name: input.name.clone(), + key: input.key.clone(), + create_ts: util::timestamp::now(), + input: input + .input + .as_ref() + .map(|x| BASE64_STANDARD.decode(x)) + .transpose()?, + }, + // Empty because request ids are ephemeral. This is intercepted by guard and + // populated before it reaches the runner + hibernating_requests: Vec::new(), + }, + )], + }) + .await?; } else { ctx.signal(crate::workflows::runner::Command { inner: protocol::Command::CommandStartActor( @@ -950,3 +1042,148 @@ fn reschedule_backoff( ) -> util::backoff::Backoff { util::backoff::Backoff::new_at(max_exponent, None, base_retry_timeout, 500, retry_count) } + +#[derive(Debug, Serialize, Deserialize, Hash)] +pub struct CheckRunnersInput { + pub seen_runners: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Hash)] +pub struct CheckRunnersOutput { + pub remove_runners: Vec, +} + +#[activity(CheckRunners)] +pub async fn check_runners( + ctx: &ActivityCtx, + input: &CheckRunnersInput, +) -> Result { + ctx.udb()? + .run(|tx| async move { + // Diff the list of seen runners with the list of active runners so we know which we can clean up + // state + let remove_runners = futures_util::stream::iter(input.seen_runners.clone()) + .map(|runner_id| { + let tx = tx.clone(); + async move { + if tx + .exists(&keys::runner::StopTsKey::new(runner_id), Snapshot) + .await? + { + anyhow::Ok(Some(runner_id)) + } else { + Ok(None) + } + } + }) + .buffer_unordered(1024) + .try_filter_map(|x| std::future::ready(Ok(x))) + .try_collect::>() + .await?; + + Ok(CheckRunnersOutput { remove_runners }) + }) + .await +} + +#[derive(Debug, Serialize, Deserialize, Hash)] +pub struct InsertAndSendCommandsInput { + pub actor_id: Id, + pub runner_id: Id, + pub commands: Vec, +} + +#[activity(InsertAndSendCommands)] +pub async fn insert_and_send_commands( + ctx: &ActivityCtx, + input: &InsertAndSendCommandsInput, +) -> Result<()> { + let mut state = ctx.state::()?; + + let runner_state = state.runner_states.entry(input.runner_id).or_default(); + let old_last_command_idx = runner_state.last_command_idx; + runner_state.last_command_idx += input.commands.len() as i64; + + // This does not have to be part of its own activity because the txn is idempotent + let last_command_idx = runner_state.last_command_idx; + ctx.udb()? + .run(|tx| async move { + tx.write( + &keys::runner::ActorLastCommandIdxKey::new(input.runner_id, input.actor_id), + last_command_idx, + )?; + + for (i, command) in input.commands.iter().enumerate() { + tx.write( + &keys::runner::ActorCommandKey::new( + input.runner_id, + input.actor_id, + old_last_command_idx + i as i64 + 1, + ), + match command { + protocol::mk2::Command::CommandStartActor(x) => { + protocol::mk2::ActorCommandKeyData::CommandStartActor(x.clone()) + } + protocol::mk2::Command::CommandStopActor(x) => { + protocol::mk2::ActorCommandKeyData::CommandStopActor(x.clone()) + } + }, + )?; + } + + Ok(()) + }) + .await?; + + let receiver_subject = + crate::pubsub_subjects::RunnerReceiverSubject::new(input.runner_id).to_string(); + + let message_serialized = + versioned::ToRunnerMk2::wrap_latest(protocol::mk2::ToRunner::ToClientCommands( + input + .commands + .iter() + .enumerate() + .map(|(i, command)| protocol::mk2::CommandWrapper { + checkpoint: protocol::mk2::ActorCheckpoint { + actor_id: input.actor_id.to_string(), + index: old_last_command_idx + i as i64 + 1, + }, + inner: command.clone(), + }) + .collect(), + )) + .serialize_with_embedded_version(PROTOCOL_MK2_VERSION)?; + + ctx.ups()? + .publish(&receiver_subject, &message_serialized, PublishOpts::one()) + .await?; + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize, Hash)] +pub struct SendMessagesToRunnerInput { + pub runner_id: Id, + pub messages: Vec, +} + +#[activity(SendMessagesToRunner)] +pub async fn send_messages_to_runner( + ctx: &ActivityCtx, + input: &SendMessagesToRunnerInput, +) -> Result<()> { + let receiver_subject = + crate::pubsub_subjects::RunnerReceiverSubject::new(input.runner_id).to_string(); + + for message in &input.messages { + let message_serialized = versioned::ToRunnerMk2::wrap_latest(message.clone()) + .serialize_with_embedded_version(PROTOCOL_MK2_VERSION)?; + + ctx.ups()? + .publish(&receiver_subject, &message_serialized, PublishOpts::one()) + .await?; + } + + Ok(()) +} diff --git a/engine/packages/pegboard/src/workflows/runner2.rs b/engine/packages/pegboard/src/workflows/runner2.rs index c7ac61e7e7..d0d4f84c29 100644 --- a/engine/packages/pegboard/src/workflows/runner2.rs +++ b/engine/packages/pegboard/src/workflows/runner2.rs @@ -42,13 +42,33 @@ impl State { pub async fn pegboard_runner2(ctx: &mut WorkflowCtx, input: &Input) -> Result<()> { ctx.activity(InitInput { runner_id: input.runner_id, + namespace_id: input.namespace_id, name: input.name.clone(), key: input.key.clone(), - namespace_id: input.namespace_id, + version: input.version, + total_slots: input.total_slots, + protocol_version: input.protocol_version, create_ts: ctx.create_ts(), }) .await?; + // Check for pending actors + let res = ctx + .activity(AllocatePendingActorsInput { + namespace_id: input.namespace_id, + name: input.name.clone(), + }) + .await?; + + // Dispatch pending allocs + for alloc in res.allocations { + ctx.signal(alloc.signal) + .to_workflow::() + .tag("actor_id", alloc.actor_id) + .send() + .await?; + } + ctx.loope(LifecycleState::new(), |ctx, state| { let input = input.clone(); @@ -61,34 +81,10 @@ pub async fn pegboard_runner2(ctx: &mut WorkflowCtx, input: &Input) -> Result<() { Some(Main::Init(_)) => { if !state.draining { - ctx.activity(InsertDbInput { + ctx.activity(MarkEligibleInput { runner_id: input.runner_id, - namespace_id: input.namespace_id, - name: input.name.clone(), - key: input.key.clone(), - version: input.version, - total_slots: input.total_slots, - protocol_version: input.protocol_version, - create_ts: ctx.create_ts(), }) .await?; - - // Check for pending actors - let res = ctx - .activity(AllocatePendingActorsInput { - namespace_id: input.namespace_id, - name: input.name.clone(), - }) - .await?; - - // Dispatch pending allocs - for alloc in res.allocations { - ctx.signal(alloc.signal) - .to_workflow::() - .tag("actor_id", alloc.actor_id) - .send() - .await?; - } } } Some(Main::CheckQueue(_)) => { @@ -119,8 +115,6 @@ pub async fn pegboard_runner2(ctx: &mut WorkflowCtx, input: &Input) -> Result<() }) .await?; - // TODO: Periodically ack events - if state.draining || expired { return Ok(Loop::Break(())); } else { @@ -238,9 +232,12 @@ impl LifecycleState { #[derive(Debug, Serialize, Deserialize, Hash)] struct InitInput { runner_id: Id, + namespace_id: Id, name: String, key: String, - namespace_id: Id, + protocol_version: u16, + version: u32, + total_slots: u32, create_ts: i64, } @@ -250,47 +247,6 @@ async fn init(ctx: &ActivityCtx, input: &InitInput) -> Result<()> { *state = Some(State::new(input.namespace_id, input.create_ts)); - ctx.udb()? - .run(|tx| async move { - let tx = tx.with_subspace(keys::subspace()); - - let runner_by_key_key = keys::ns::RunnerByKeyKey::new( - input.namespace_id, - input.name.clone(), - input.key.clone(), - ); - - // Allocate self - tx.write( - &runner_by_key_key, - RunnerByKeyKeyData { - runner_id: input.runner_id, - workflow_id: ctx.workflow_id(), - }, - )?; - - Ok(()) - }) - .custom_instrument(tracing::info_span!("runner_init_tx")) - .await?; - - Ok(()) -} - -#[derive(Debug, Serialize, Deserialize, Hash)] -struct InsertDbInput { - runner_id: Id, - namespace_id: Id, - name: String, - key: String, - protocol_version: u16, - version: u32, - total_slots: u32, - create_ts: i64, -} - -#[activity(InsertDb)] -async fn insert_db(ctx: &ActivityCtx, input: &InsertDbInput) -> Result<()> { ctx.udb()? .run(|tx| async move { let tx = tx.with_subspace(keys::subspace()); @@ -305,6 +261,7 @@ async fn insert_db(ctx: &ActivityCtx, input: &InsertDbInput) -> Result<()> { )?; let now = util::timestamp::now(); + // TODO: Do we still need to check if it already exists? this txn is only run once // See if key already exists let existing = if let (Some(remaining_slots), Some(last_ping_ts)) = (remaining_slots_entry, last_ping_ts_entry) @@ -356,6 +313,11 @@ async fn insert_db(ctx: &ActivityCtx, input: &InsertDbInput) -> Result<()> { tx.write(&last_ping_ts_key, now)?; + tx.write( + &keys::runner::ProtocolVersionKey::new(input.runner_id), + input.protocol_version, + )?; + // Populate ns indexes tx.write( &keys::ns::ActiveRunnerKey::new( @@ -424,14 +386,48 @@ async fn insert_db(ctx: &ActivityCtx, input: &InsertDbInput) -> Result<()> { }, )?; + let runner_by_key_key = keys::ns::RunnerByKeyKey::new( + input.namespace_id, + input.name.clone(), + input.key.clone(), + ); + + // Allocate self + tx.write( + &runner_by_key_key, + RunnerByKeyKeyData { + runner_id: input.runner_id, + workflow_id: ctx.workflow_id(), + }, + )?; + Ok(()) }) - .custom_instrument(tracing::info_span!("runner_insert_tx")) + .custom_instrument(tracing::info_span!("runner_init_tx")) .await?; Ok(()) } +#[derive(Debug, Serialize, Deserialize, Hash)] +struct MarkEligibleInput { + runner_id: Id, +} + +#[activity(MarkEligible)] +async fn mark_eligible(ctx: &ActivityCtx, input: &MarkEligibleInput) -> Result<()> { + // Mark eligible + ctx.op(crate::ops::runner::update_alloc_idx::Input { + runners: vec![crate::ops::runner::update_alloc_idx::Runner { + runner_id: input.runner_id, + action: crate::ops::runner::update_alloc_idx::Action::AddIdx, + }], + }) + .await?; + + Ok(()) +} + #[derive(Debug, Serialize, Deserialize, Hash)] struct ClearDbInput { runner_id: Id, @@ -810,4 +806,5 @@ join_signal!(Main { Init, CheckQueue, Stop, + // Comment to prevent invalid formatting }); diff --git a/engine/packages/universaldb/Cargo.toml b/engine/packages/universaldb/Cargo.toml index 1aed8b1d5b..ce3da8af03 100644 --- a/engine/packages/universaldb/Cargo.toml +++ b/engine/packages/universaldb/Cargo.toml @@ -11,6 +11,7 @@ async-trait.workspace = true deadpool-postgres.workspace = true foundationdb-tuple.workspace = true futures-util.workspace = true +hex.workspace = true lazy_static.workspace = true rand.workspace = true rivet-metrics.workspace = true diff --git a/engine/packages/universaldb/src/driver/postgres/database.rs b/engine/packages/universaldb/src/driver/postgres/database.rs index 7cae89e26d..1ab832997d 100644 --- a/engine/packages/universaldb/src/driver/postgres/database.rs +++ b/engine/packages/universaldb/src/driver/postgres/database.rs @@ -63,12 +63,9 @@ impl PostgresDatabaseDriver { .await .context("failed to create btree_gist extension")?; - conn.execute( - "CREATE UNLOGGED SEQUENCE IF NOT EXISTS global_version_seq START WITH 1 INCREMENT BY 1 MINVALUE 1", - &[], - ) - .await - .context("failed to create global version sequence")?; + conn.execute("CREATE UNLOGGED SEQUENCE IF NOT EXISTS global_version_seq START WITH 1 INCREMENT BY 1 MINVALUE 1", &[]) + .await + .context("failed to create global version sequence")?; // Create the KV table if it doesn't exist conn.execute( diff --git a/engine/packages/universaldb/src/driver/rocksdb/database.rs b/engine/packages/universaldb/src/driver/rocksdb/database.rs index 01fb740d72..23bac0ec5c 100644 --- a/engine/packages/universaldb/src/driver/rocksdb/database.rs +++ b/engine/packages/universaldb/src/driver/rocksdb/database.rs @@ -42,7 +42,8 @@ impl RocksDbDatabaseDriver { opts.create_if_missing(true); opts.set_max_open_files(10000); opts.set_keep_log_file_num(10); - opts.set_max_total_wal_size(64 * 1024 * 1024); // 64MB + opts.set_max_total_wal_size(64 * 1024 * 1024); // 64MiB + opts.set_write_buffer_size(256 * 1024 * 1024); // 256MiB for conflict detection // Open the OptimisticTransactionDB tracing::debug!(path=%db_path.display(), "opening rocksdb"); diff --git a/engine/packages/universaldb/src/driver/rocksdb/transaction_conflict_tracker.rs b/engine/packages/universaldb/src/driver/rocksdb/transaction_conflict_tracker.rs index 8ef61500d5..370240760a 100644 --- a/engine/packages/universaldb/src/driver/rocksdb/transaction_conflict_tracker.rs +++ b/engine/packages/universaldb/src/driver/rocksdb/transaction_conflict_tracker.rs @@ -65,11 +65,11 @@ impl TransactionConflictTracker { // Check conflict ranges overlap if cr1_start < cr2_end && cr2_start < cr1_end && cr1_type != cr2_type { tracing::debug!( - ?cr1_start, - ?cr1_end, + cr1_start=%hex::encode(cr1_start), + cr1_end=%hex::encode(cr1_end), ?cr1_type, - ?cr2_start, - ?cr2_end, + cr2_start=%hex::encode(cr2_start), + cr2_end=%hex::encode(cr2_end), ?cr2_type, txn1_start_version, txn1_commit_version, diff --git a/engine/packages/universaldb/src/driver/rocksdb/transaction_task.rs b/engine/packages/universaldb/src/driver/rocksdb/transaction_task.rs index 704c4f3823..72ccde4f65 100644 --- a/engine/packages/universaldb/src/driver/rocksdb/transaction_task.rs +++ b/engine/packages/universaldb/src/driver/rocksdb/transaction_task.rs @@ -139,7 +139,8 @@ impl TransactionTask { fn create_transaction(&self) -> RocksDbTransaction<'_, OptimisticTransactionDB> { let write_opts = WriteOptions::default(); - let txn_opts = rocksdb::OptimisticTransactionOptions::default(); + let mut txn_opts = rocksdb::OptimisticTransactionOptions::default(); + txn_opts.set_snapshot(true); self.db.transaction_opt(&write_opts, &txn_opts) } diff --git a/engine/packages/universaldb/src/utils/keys.rs b/engine/packages/universaldb/src/utils/keys.rs index 2f3ceee6ac..475e24b070 100644 --- a/engine/packages/universaldb/src/utils/keys.rs +++ b/engine/packages/universaldb/src/utils/keys.rs @@ -132,4 +132,6 @@ define_keys! { // 104 - RESERVED BY EE // 105 - RESERVED BY EE (106, PROTOCOL_VERSION, "protocol_version"), + (107, LAST_COMMAND_IDX, "last_command_idx"), + (108, COMMAND, "command"), } diff --git a/engine/packages/universaldb/src/utils/mod.rs b/engine/packages/universaldb/src/utils/mod.rs index a81a3ba99b..ff7090d438 100644 --- a/engine/packages/universaldb/src/utils/mod.rs +++ b/engine/packages/universaldb/src/utils/mod.rs @@ -24,6 +24,14 @@ pub enum IsolationLevel { #[derive(Debug, Clone, Copy)] pub struct MaybeCommitted(pub bool); +impl std::ops::Deref for MaybeCommitted { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// Calculate exponential backoff based on attempt. /// /// Ours: diff --git a/engine/sdks/rust/api-full/src/apis/actors_api.rs b/engine/sdks/rust/api-full/src/apis/actors_api.rs index b1906914f9..feb60b24a7 100644 --- a/engine/sdks/rust/api-full/src/apis/actors_api.rs +++ b/engine/sdks/rust/api-full/src/apis/actors_api.rs @@ -51,10 +51,14 @@ pub async fn create_actor( if !status.is_client_error() && !status.is_server_error() { let content = resp.text().await?; match content_type { - ContentType::Json => serde_json::from_str(&content).map_err(Error::from), - ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::ActorsCreateResponse`"))), - ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::ActorsCreateResponse`")))), - } + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::ActorsCreateResponse`"))), + ContentType::Unsupported(unknown_type) => { + return Err(Error::from(serde_json::Error::custom(format!( + "Received `{unknown_type}` content type response that cannot be converted to `models::ActorsCreateResponse`" + )))) + } + } } else { let content = resp.text().await?; let entity: Option = serde_json::from_str(&content).ok(); diff --git a/engine/sdks/rust/api-full/src/apis/ns_api.rs b/engine/sdks/rust/api-full/src/apis/ns_api.rs index 6232e204fe..e44967a724 100644 --- a/engine/sdks/rust/api-full/src/apis/ns_api.rs +++ b/engine/sdks/rust/api-full/src/apis/ns_api.rs @@ -51,10 +51,14 @@ pub async fn create_namespace( if !status.is_client_error() && !status.is_server_error() { let content = resp.text().await?; match content_type { - ContentType::Json => serde_json::from_str(&content).map_err(Error::from), - ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::NamespacesCreateResponse`"))), - ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::NamespacesCreateResponse`")))), - } + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::NamespacesCreateResponse`"))), + ContentType::Unsupported(unknown_type) => { + return Err(Error::from(serde_json::Error::custom(format!( + "Received `{unknown_type}` content type response that cannot be converted to `models::NamespacesCreateResponse`" + )))) + } + } } else { let content = resp.text().await?; let entity: Option = serde_json::from_str(&content).ok(); diff --git a/engine/sdks/rust/runner-protocol/src/versioned.rs b/engine/sdks/rust/runner-protocol/src/versioned.rs index ad1dcc9bbb..8ce2662694 100644 --- a/engine/sdks/rust/runner-protocol/src/versioned.rs +++ b/engine/sdks/rust/runner-protocol/src/versioned.rs @@ -37,6 +37,16 @@ impl OwnedVersionedData for ToClientMk2 { ToClientMk2::V4(data) => serde_bare::to_vec(&data).map_err(Into::into), } } + + fn deserialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } + + fn serialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } } pub enum ToServerMk2 { @@ -71,6 +81,16 @@ impl OwnedVersionedData for ToServerMk2 { ToServerMk2::V4(data) => serde_bare::to_vec(&data).map_err(Into::into), } } + + fn deserialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } + + fn serialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } } pub enum ToRunnerMk2 { @@ -105,6 +125,16 @@ impl OwnedVersionedData for ToRunnerMk2 { ToRunnerMk2::V4(data) => serde_bare::to_vec(&data).map_err(Into::into), } } + + fn deserialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } + + fn serialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } } pub enum ToClient { @@ -1024,6 +1054,51 @@ impl ToServerlessServer { } } +pub enum ActorCommandKeyData { + // Only in v4 + V4(v4::ActorCommandKeyData), +} + +impl OwnedVersionedData for ActorCommandKeyData { + type Latest = v4::ActorCommandKeyData; + + fn wrap_latest(latest: v4::ActorCommandKeyData) -> Self { + ActorCommandKeyData::V4(latest) + } + + fn unwrap_latest(self) -> Result { + #[allow(irrefutable_let_patterns)] + if let ActorCommandKeyData::V4(data) = self { + Ok(data) + } else { + bail!("version not latest"); + } + } + + fn deserialize_version(payload: &[u8], version: u16) -> Result { + match version { + 4 => Ok(ActorCommandKeyData::V4(serde_bare::from_slice(payload)?)), + _ => bail!("invalid version: {version}"), + } + } + + fn serialize_version(self, _version: u16) -> Result> { + match self { + ActorCommandKeyData::V4(data) => serde_bare::to_vec(&data).map_err(Into::into), + } + } + + fn deserialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } + + fn serialize_converters() -> Vec Result> { + // No changes between v1 and v4 + vec![Ok, Ok, Ok] + } +} + // Helper conversion functions fn convert_to_client_tunnel_message_kind_v1_to_v2( kind: v1::ToClientTunnelMessageKind, diff --git a/engine/sdks/schemas/runner-protocol/v4.bare b/engine/sdks/schemas/runner-protocol/v4.bare index ff1a4bba50..a91d1dfd00 100644 --- a/engine/sdks/schemas/runner-protocol/v4.bare +++ b/engine/sdks/schemas/runner-protocol/v4.bare @@ -203,6 +203,12 @@ type CommandWrapper struct { inner: Command } +# We redeclare this so its top level +type ActorCommandKeyData union { + CommandStartActor | + CommandStopActor +} + # MARK: Tunnel # Message ID @@ -330,7 +336,6 @@ type ToServerInit struct { name: str version: u32 totalSlots: u32 - lastCommandCheckpoints: list prepopulateActorNames: optional> metadata: optional } @@ -338,7 +343,7 @@ type ToServerInit struct { type ToServerEvents list type ToServerAckCommands struct { - lastCommandIdx: i64 + lastCommandCheckpoints: list } type ToServerStopping void @@ -370,7 +375,6 @@ type ProtocolMetadata struct { type ToClientInit struct { runnerId: Id - lastEventCheckpoints: list metadata: ProtocolMetadata } diff --git a/engine/sdks/typescript/runner-protocol/src/index.ts b/engine/sdks/typescript/runner-protocol/src/index.ts index 7e619fa402..8a47f762d8 100644 --- a/engine/sdks/typescript/runner-protocol/src/index.ts +++ b/engine/sdks/typescript/runner-protocol/src/index.ts @@ -978,6 +978,62 @@ export function writeCommandWrapper(bc: bare.ByteCursor, x: CommandWrapper): voi writeCommand(bc, x.inner) } +/** + * We redeclare this so its top level + */ +export type ActorCommandKeyData = + | { readonly tag: "CommandStartActor"; readonly val: CommandStartActor } + | { readonly tag: "CommandStopActor"; readonly val: CommandStopActor } + +export function readActorCommandKeyData(bc: bare.ByteCursor): ActorCommandKeyData { + const offset = bc.offset + const tag = bare.readU8(bc) + switch (tag) { + case 0: + return { tag: "CommandStartActor", val: readCommandStartActor(bc) } + case 1: + return { tag: "CommandStopActor", val: readCommandStopActor(bc) } + default: { + bc.offset = offset + throw new bare.BareError(offset, "invalid tag") + } + } +} + +export function writeActorCommandKeyData(bc: bare.ByteCursor, x: ActorCommandKeyData): void { + switch (x.tag) { + case "CommandStartActor": { + bare.writeU8(bc, 0) + writeCommandStartActor(bc, x.val) + break + } + case "CommandStopActor": { + bare.writeU8(bc, 1) + writeCommandStopActor(bc, x.val) + break + } + } +} + +export function encodeActorCommandKeyData(x: ActorCommandKeyData, config?: Partial): Uint8Array { + const fullConfig = config != null ? bare.Config(config) : DEFAULT_CONFIG + const bc = new bare.ByteCursor( + new Uint8Array(fullConfig.initialBufferLength), + fullConfig, + ) + writeActorCommandKeyData(bc, x) + return new Uint8Array(bc.view.buffer, bc.view.byteOffset, bc.offset) +} + +export function decodeActorCommandKeyData(bytes: Uint8Array): ActorCommandKeyData { + const bc = new bare.ByteCursor(bytes, DEFAULT_CONFIG) + const result = readActorCommandKeyData(bc) + if (bc.offset < bc.view.byteLength) { + throw new bare.BareError(bc.offset, "remaining bytes") + } + return result +} + export type MessageId = { /** * Globally unique ID @@ -1460,26 +1516,7 @@ export function writeToClientPing(bc: bare.ByteCursor, x: ToClientPing): void { bare.writeI64(bc, x.ts) } -function read11(bc: bare.ByteCursor): readonly ActorCheckpoint[] { - const len = bare.readUintSafe(bc) - if (len === 0) { - return [] - } - const result = [readActorCheckpoint(bc)] - for (let i = 1; i < len; i++) { - result[i] = readActorCheckpoint(bc) - } - return result -} - -function write11(bc: bare.ByteCursor, x: readonly ActorCheckpoint[]): void { - bare.writeUintSafe(bc, x.length) - for (let i = 0; i < x.length; i++) { - writeActorCheckpoint(bc, x[i]) - } -} - -function read12(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++) { @@ -1494,7 +1531,7 @@ function read12(bc: bare.ByteCursor): ReadonlyMap { return result } -function write12(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]) @@ -1502,22 +1539,22 @@ function write12(bc: bare.ByteCursor, x: ReadonlyMap): void { } } -function read13(bc: bare.ByteCursor): ReadonlyMap | null { - return bare.readBool(bc) ? read12(bc) : null +function read12(bc: bare.ByteCursor): ReadonlyMap | null { + return bare.readBool(bc) ? read11(bc) : null } -function write13(bc: bare.ByteCursor, x: ReadonlyMap | null): void { +function write12(bc: bare.ByteCursor, x: ReadonlyMap | null): void { bare.writeBool(bc, x != null) if (x != null) { - write12(bc, x) + write11(bc, x) } } -function read14(bc: bare.ByteCursor): Json | null { +function read13(bc: bare.ByteCursor): Json | null { return bare.readBool(bc) ? readJson(bc) : null } -function write14(bc: bare.ByteCursor, x: Json | null): void { +function write13(bc: bare.ByteCursor, x: Json | null): void { bare.writeBool(bc, x != null) if (x != null) { writeJson(bc, x) @@ -1531,7 +1568,6 @@ export type ToServerInit = { readonly name: string readonly version: u32 readonly totalSlots: u32 - readonly lastCommandCheckpoints: readonly ActorCheckpoint[] readonly prepopulateActorNames: ReadonlyMap | null readonly metadata: Json | null } @@ -1541,9 +1577,8 @@ export function readToServerInit(bc: bare.ByteCursor): ToServerInit { name: bare.readString(bc), version: bare.readU32(bc), totalSlots: bare.readU32(bc), - lastCommandCheckpoints: read11(bc), - prepopulateActorNames: read13(bc), - metadata: read14(bc), + prepopulateActorNames: read12(bc), + metadata: read13(bc), } } @@ -1551,9 +1586,8 @@ export function writeToServerInit(bc: bare.ByteCursor, x: ToServerInit): void { bare.writeString(bc, x.name) bare.writeU32(bc, x.version) bare.writeU32(bc, x.totalSlots) - write11(bc, x.lastCommandCheckpoints) - write13(bc, x.prepopulateActorNames) - write14(bc, x.metadata) + write12(bc, x.prepopulateActorNames) + write13(bc, x.metadata) } export type ToServerEvents = readonly EventWrapper[] @@ -1577,18 +1611,37 @@ export function writeToServerEvents(bc: bare.ByteCursor, x: ToServerEvents): voi } } +function read14(bc: bare.ByteCursor): readonly ActorCheckpoint[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readActorCheckpoint(bc)] + for (let i = 1; i < len; i++) { + result[i] = readActorCheckpoint(bc) + } + return result +} + +function write14(bc: bare.ByteCursor, x: readonly ActorCheckpoint[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writeActorCheckpoint(bc, x[i]) + } +} + export type ToServerAckCommands = { - readonly lastCommandIdx: i64 + readonly lastCommandCheckpoints: readonly ActorCheckpoint[] } export function readToServerAckCommands(bc: bare.ByteCursor): ToServerAckCommands { return { - lastCommandIdx: bare.readI64(bc), + lastCommandCheckpoints: read14(bc), } } export function writeToServerAckCommands(bc: bare.ByteCursor, x: ToServerAckCommands): void { - bare.writeI64(bc, x.lastCommandIdx) + write14(bc, x.lastCommandCheckpoints) } export type ToServerStopping = null @@ -1738,21 +1791,18 @@ export function writeProtocolMetadata(bc: bare.ByteCursor, x: ProtocolMetadata): export type ToClientInit = { readonly runnerId: Id - readonly lastEventCheckpoints: readonly ActorCheckpoint[] readonly metadata: ProtocolMetadata } export function readToClientInit(bc: bare.ByteCursor): ToClientInit { return { runnerId: readId(bc), - lastEventCheckpoints: read11(bc), metadata: readProtocolMetadata(bc), } } export function writeToClientInit(bc: bare.ByteCursor, x: ToClientInit): void { writeId(bc, x.runnerId) - write11(bc, x.lastEventCheckpoints) writeProtocolMetadata(bc, x.metadata) } @@ -1783,12 +1833,12 @@ export type ToClientAckEvents = { export function readToClientAckEvents(bc: bare.ByteCursor): ToClientAckEvents { return { - lastEventCheckpoints: read11(bc), + lastEventCheckpoints: read14(bc), } } export function writeToClientAckEvents(bc: bare.ByteCursor, x: ToClientAckEvents): void { - write11(bc, x.lastEventCheckpoints) + write14(bc, x.lastEventCheckpoints) } export type ToClientKvResponse = { diff --git a/engine/sdks/typescript/runner/src/actor.ts b/engine/sdks/typescript/runner/src/actor.ts index a2016f61f2..c2cc375bd4 100644 --- a/engine/sdks/typescript/runner/src/actor.ts +++ b/engine/sdks/typescript/runner/src/actor.ts @@ -27,6 +27,10 @@ export class RunnerActor { }> = []; actorStartPromise: ReturnType>; + lastCommandIdx: bigint = -1n; + nextEventIdx: bigint = 0n; + eventHistory: protocol.EventWrapper[] = []; + /** * If restoreHibernatingRequests has been called. This is used to assert * that the caller is implemented correctly. @@ -81,8 +85,8 @@ export class RunnerActor { gatewayId, requestId, request: { - resolve: () => {}, - reject: () => {}, + resolve: () => { }, + reject: () => { }, actorId: this.actorId, gatewayId: gatewayId, requestId: requestId, @@ -118,8 +122,8 @@ export class RunnerActor { gatewayId, requestId, request: { - resolve: () => {}, - reject: () => {}, + resolve: () => { }, + reject: () => { }, actorId: this.actorId, gatewayId: gatewayId, requestId: requestId, @@ -193,4 +197,14 @@ export class RunnerActor { this.webSockets.splice(index, 1); } } + + handleAckEvents(lastEventIdx: bigint) { + this.eventHistory = this.eventHistory.filter( + (event) => event.checkpoint.index > lastEventIdx, + ); + } + + recordEvent(eventWrapper: protocol.EventWrapper) { + this.eventHistory.push(eventWrapper); + } } diff --git a/engine/sdks/typescript/runner/src/mod.ts b/engine/sdks/typescript/runner/src/mod.ts index 926bd93c77..bdda7ee54d 100644 --- a/engine/sdks/typescript/runner/src/mod.ts +++ b/engine/sdks/typescript/runner/src/mod.ts @@ -18,7 +18,6 @@ export { idToStr } from "./utils"; const KV_EXPIRE: number = 30_000; const PROTOCOL_VERSION: number = 4; -const RUNNER_PING_INTERVAL = 3_000; /** Warn once the backlog significantly exceeds the server's ack batch size. */ const EVENT_BACKLOG_WARN_THRESHOLD = 10_000; @@ -197,9 +196,6 @@ export class Runner { // WebSocket __pegboardWebSocket?: WebSocket; runnerId?: string; - #lastCommandIdx: number = -1; - #pingLoop?: NodeJS.Timeout; - #nextEventIdx: bigint = 0n; #started: boolean = false; #shutdown: boolean = false; #shuttingDown: boolean = false; @@ -211,7 +207,6 @@ export class Runner { #runnerLostTimeout?: NodeJS.Timeout; // Event storage for resending - #eventHistory: protocol.EventWrapper[] = []; #eventBacklogWarned: boolean = false; // Command acknowledgment @@ -308,10 +303,6 @@ export class Runner { } #stopAllActors() { - this.log?.info({ - msg: "stopping all actors due to runner lost threshold exceeded", - }); - const actorIds = Array.from(this.#actors.keys()); for (const actorId of actorIds) { this.forceStopActor(actorId); @@ -477,12 +468,6 @@ export class Runner { this.#runnerLostTimeout = undefined; } - // Clear ping loop - if (this.#pingLoop) { - clearInterval(this.#pingLoop); - this.#pingLoop = undefined; - } - // Clear ack interval if (this.#ackInterval) { clearInterval(this.#ackInterval); @@ -738,10 +723,6 @@ export class Runner { name: this.#config.runnerName, version: this.#config.version, totalSlots: this.#config.totalSlots, - lastCommandIdx: - this.#lastCommandIdx >= 0 - ? BigInt(this.#lastCommandIdx) - : null, prepopulateActorNames: new Map( Object.entries(this.#config.prepopulateActorNames).map( ([name, data]) => [ @@ -758,24 +739,6 @@ export class Runner { val: init, }); - // Start ping interval - const pingLoop = setInterval(() => { - if (ws.readyState === 1) { - this.__sendToServer({ - tag: "ToServerPing", - val: { - ts: BigInt(Date.now()), - }, - }); - } else { - clearInterval(pingLoop); - this.log?.info({ - msg: "WebSocket not open, stopping ping loop", - }); - } - }, RUNNER_PING_INTERVAL); - this.#pingLoop = pingLoop; - // Start command acknowledgment interval (5 minutes) const ackInterval = 5 * 60 * 1000; // 5 minutes in milliseconds const ackLoop = setInterval(() => { @@ -815,8 +778,8 @@ export class Runner { if (this.runnerId !== init.runnerId) { this.runnerId = init.runnerId; - // Clear history if runner id changed - this.#eventHistory.length = 0; + // Clear actors if runner id changed + this.#stopAllActors(); } // Store the runner lost threshold from metadata @@ -826,13 +789,12 @@ export class Runner { this.log?.info({ msg: "received init", - lastEventIdx: init.lastEventIdx, runnerLostThreshold: this.#runnerLostThreshold, }); // Resend pending events this.#processUnsentKvRequests(); - this.#resendUnacknowledgedEvents(init.lastEventIdx); + this.#resendUnacknowledgedEvents(); this.#tunnel?.resendBufferedEvents(); this.#config.onConnected(); @@ -846,9 +808,13 @@ export class Runner { this.#handleKvResponse(kvResponse); } else if (message.tag === "ToClientTunnelMessage") { this.#tunnel?.handleTunnelMessage(message.val); - } else if (message.tag === "ToClientClose") { - this.#tunnel?.shutdown(); - ws.close(1000, "manual closure"); + } else if (message.tag === "ToClientPing") { + this.__sendToServer({ + tag: "ToServerPong", + val: { + ts: message.val.ts + }, + }); } else { unreachable(message); } @@ -871,6 +837,10 @@ export class Runner { seconds: this.#runnerLostThreshold / 1000, }); this.#runnerLostTimeout = setTimeout(() => { + this.log?.info({ + msg: "stopping all actors due to runner lost threshold", + }); + this.#stopAllActors(); }, this.#runnerLostThreshold); } @@ -909,12 +879,6 @@ export class Runner { this.#config.onDisconnected(ev.code, ev.reason); } - // Clear ping loop on close - if (this.#pingLoop) { - clearInterval(this.#pingLoop); - this.#pingLoop = undefined; - } - // Clear ack interval on close if (this.#ackInterval) { clearInterval(this.#ackInterval); @@ -960,44 +924,52 @@ export class Runner { unreachable(commandWrapper.inner); } - this.#lastCommandIdx = Number(commandWrapper.index); + const actor = this.getActor(commandWrapper.checkpoint.actorId, commandWrapper.inner.val.generation); + if (actor) actor.lastCommandIdx = commandWrapper.checkpoint.index; } } #handleAckEvents(ack: protocol.ToClientAckEvents) { - const lastAckedIdx = ack.lastEventIdx; + let originalTotalEvents = Array.from(this.#actors).reduce((s, [_, actor]) => s + actor.eventHistory.length, 0); - const originalLength = this.#eventHistory.length; - this.#eventHistory = this.#eventHistory.filter( - (event) => event.index > lastAckedIdx, - ); + for (const [_, actor] of this.#actors) { + let checkpoint = ack.lastEventCheckpoints.find(x => x.actorId == actor.actorId); + + if (checkpoint) actor.handleAckEvents(checkpoint.index); + } + + const totalEvents = Array.from(this.#actors).reduce((s, [_, actor]) => s + actor.eventHistory.length, 0); + const prunedCount = originalTotalEvents - totalEvents; - const prunedCount = originalLength - this.#eventHistory.length; if (prunedCount > 0) { this.log?.info({ msg: "pruned acknowledged events", - lastAckedIdx: lastAckedIdx.toString(), prunedCount, }); } - if (this.#eventHistory.length <= EVENT_BACKLOG_WARN_THRESHOLD) { + if (totalEvents <= EVENT_BACKLOG_WARN_THRESHOLD) { this.#eventBacklogWarned = false; } } /** Track events to send to the server in case we need to resend it on disconnect. */ #recordEvent(eventWrapper: protocol.EventWrapper) { - this.#eventHistory.push(eventWrapper); + const actor = this.getActor(eventWrapper.checkpoint.actorId); + if (!actor) return; + + actor.recordEvent(eventWrapper); + + let totalEvents = Array.from(this.#actors).reduce((s, [_, actor]) => s + actor.eventHistory.length, 0); if ( - this.#eventHistory.length > EVENT_BACKLOG_WARN_THRESHOLD && + totalEvents > EVENT_BACKLOG_WARN_THRESHOLD && !this.#eventBacklogWarned ) { this.#eventBacklogWarned = true; this.log?.warn({ msg: "unacknowledged event backlog exceeds threshold", - backlogSize: this.#eventHistory.length, + backlogSize: totalEvents, threshold: EVENT_BACKLOG_WARN_THRESHOLD, }); } @@ -1013,7 +985,7 @@ export class Runner { const startCommand = commandWrapper.inner .val as protocol.CommandStartActor; - const actorId = startCommand.actorId; + const actorId = commandWrapper.checkpoint.actorId; const generation = startCommand.generation; const config = startCommand.config; @@ -1094,7 +1066,7 @@ export class Runner { const stopCommand = commandWrapper.inner .val as protocol.CommandStopActor; - const actorId = stopCommand.actorId; + const actorId = commandWrapper.checkpoint.actorId; const generation = stopCommand.generation; await this.forceStopActor(actorId, generation); @@ -1105,6 +1077,9 @@ export class Runner { generation: number, intentType: "sleep" | "stop", ) { + const actor = this.getActor(actorId, generation); + if (!actor) return; + let actorIntent: protocol.ActorIntent; if (intentType === "sleep") { @@ -1124,9 +1099,11 @@ export class Runner { intent: actorIntent, }; - const eventIndex = this.#nextEventIdx++; const eventWrapper: protocol.EventWrapper = { - index: eventIndex, + checkpoint: { + actorId, + index: actor.nextEventIdx++, + }, inner: { tag: "EventActorIntent", val: intentEvent, @@ -1146,6 +1123,9 @@ export class Runner { generation: number, stateType: "running" | "stopped", ) { + const actor = this.getActor(actorId, generation); + if (!actor) return; + let actorState: protocol.ActorState; if (stateType === "running") { @@ -1168,9 +1148,11 @@ export class Runner { state: actorState, }; - const eventIndex = this.#nextEventIdx++; const eventWrapper: protocol.EventWrapper = { - index: eventIndex, + checkpoint: { + actorId, + index: actor.nextEventIdx++, + }, inner: { tag: "EventActorStateUpdate", val: stateUpdateEvent, @@ -1186,9 +1168,18 @@ export class Runner { } #sendCommandAcknowledgment() { - if (this.#lastCommandIdx < 0) { - // No commands received yet, nothing to acknowledge - return; + const lastCommandCheckpoints = []; + + for (const [_, actor] of this.#actors) { + if (actor.lastCommandIdx < 0) { + // No commands received yet, nothing to acknowledge + continue; + } + + lastCommandCheckpoints.push({ + actorId: actor.actorId, + index: actor.lastCommandIdx, + }); } //this.#log?.log("Sending command acknowledgment", this.#lastCommandIdx); @@ -1196,7 +1187,7 @@ export class Runner { this.__sendToServer({ tag: "ToServerAckCommands", val: { - lastCommandIdx: BigInt(this.#lastCommandIdx), + lastCommandCheckpoints, }, }); } @@ -1500,9 +1491,11 @@ export class Runner { alarmTs: alarmTs !== null ? BigInt(alarmTs) : null, }; - const eventIndex = this.#nextEventIdx++; const eventWrapper: protocol.EventWrapper = { - index: eventIndex, + checkpoint: { + actorId, + index: actor.nextEventIdx++, + }, inner: { tag: "EventActorSetAlarm", val: alarmEvent, @@ -1669,6 +1662,7 @@ export class Runner { tag: "ToServerlessServerInit", val: { runnerId: this.runnerId, + runnerProtocolVersion: PROTOCOL_VERSION, }, }); @@ -1710,16 +1704,18 @@ export class Runner { }, delay); } - #resendUnacknowledgedEvents(lastEventIdx: bigint) { - const eventsToResend = this.#eventHistory.filter( - (event) => event.index > lastEventIdx, - ); + #resendUnacknowledgedEvents() { + const eventsToResend = []; + + for (const [_, actor] of this.#actors) { + eventsToResend.push(...actor.eventHistory); + } if (eventsToResend.length === 0) return; this.log?.info({ msg: "resending unacknowledged events", - fromIndex: lastEventIdx + 1n, + count: eventsToResend.length, }); // Resend events in batches diff --git a/engine/sdks/typescript/runner/src/stringify.ts b/engine/sdks/typescript/runner/src/stringify.ts index 3001108b77..8aff36fcea 100644 --- a/engine/sdks/typescript/runner/src/stringify.ts +++ b/engine/sdks/typescript/runner/src/stringify.ts @@ -40,8 +40,6 @@ export function stringifyToServerTunnelMessageKind( kind: protocol.ToServerTunnelMessageKind, ): string { switch (kind.tag) { - case "DeprecatedTunnelAck": - return "DeprecatedTunnelAck"; case "ToServerResponseStart": { const { status, headers, body, stream } = kind.val; const bodyStr = body === null ? "null" : stringifyArrayBuffer(body); @@ -82,8 +80,6 @@ export function stringifyToClientTunnelMessageKind( kind: protocol.ToClientTunnelMessageKind, ): string { switch (kind.tag) { - case "DeprecatedTunnelAck": - return "DeprecatedTunnelAck"; case "ToClientRequestStart": { const { actorId, method, path, headers, body, stream } = kind.val; const bodyStr = body === null ? "null" : stringifyArrayBuffer(body); @@ -119,7 +115,7 @@ export function stringifyToClientTunnelMessageKind( export function stringifyCommand(command: protocol.Command): string { switch (command.tag) { case "CommandStartActor": { - const { actorId, generation, config, hibernatingRequests } = + const { generation, config, hibernatingRequests } = command.val; const keyStr = config.key === null ? "null" : `"${config.key}"`; const inputStr = @@ -130,11 +126,11 @@ export function stringifyCommand(command: protocol.Command): string { hibernatingRequests.length > 0 ? `[${hibernatingRequests.map((hr) => `{gatewayId: ${idToStr(hr.gatewayId)}, requestId: ${idToStr(hr.requestId)}}`).join(", ")}]` : "[]"; - return `CommandStartActor{actorId: "${actorId}", generation: ${generation}, config: {name: "${config.name}", key: ${keyStr}, createTs: ${stringifyBigInt(config.createTs)}, input: ${inputStr}}, hibernatingRequests: ${hibernatingRequestsStr}}`; + return `CommandStartActor{generation: ${generation}, config: {name: "${config.name}", key: ${keyStr}, createTs: ${stringifyBigInt(config.createTs)}, input: ${inputStr}}, hibernatingRequests: ${hibernatingRequestsStr}}`; } case "CommandStopActor": { - const { actorId, generation } = command.val; - return `CommandStopActor{actorId: "${actorId}", generation: ${generation}}`; + const { generation } = command.val; + return `CommandStopActor{generation: ${generation}}`; } } } @@ -146,7 +142,7 @@ export function stringifyCommand(command: protocol.Command): string { export function stringifyCommandWrapper( wrapper: protocol.CommandWrapper, ): string { - return `CommandWrapper{index: ${stringifyBigInt(wrapper.index)}, inner: ${stringifyCommand(wrapper.inner)}}`; + return `CommandWrapper{actorId: "${wrapper.checkpoint.actorId}", index: ${stringifyBigInt(wrapper.checkpoint.index)}, inner: ${stringifyCommand(wrapper.inner)}}`; } /** @@ -193,7 +189,7 @@ export function stringifyEvent(event: protocol.Event): string { * Handles ArrayBuffers, BigInts, and Maps that can't be JSON.stringified */ export function stringifyEventWrapper(wrapper: protocol.EventWrapper): string { - return `EventWrapper{index: ${stringifyBigInt(wrapper.index)}, inner: ${stringifyEvent(wrapper.inner)}}`; + return `EventWrapper{actorId: ${wrapper.checkpoint.actorId}, index: ${stringifyBigInt(wrapper.checkpoint.index)}, inner: ${stringifyEvent(wrapper.inner)}}`; } /** @@ -207,34 +203,33 @@ export function stringifyToServer(message: protocol.ToServer): string { name, version, totalSlots, - lastCommandIdx, prepopulateActorNames, metadata, } = message.val; - const lastCommandIdxStr = - lastCommandIdx === null - ? "null" - : stringifyBigInt(lastCommandIdx); const prepopulateActorNamesStr = prepopulateActorNames === null ? "null" : `Map(${prepopulateActorNames.size})`; const metadataStr = metadata === null ? "null" : `"${metadata}"`; - return `ToServerInit{name: "${name}", version: ${version}, totalSlots: ${totalSlots}, lastCommandIdx: ${lastCommandIdxStr}, prepopulateActorNames: ${prepopulateActorNamesStr}, metadata: ${metadataStr}}`; + return `ToServerInit{name: "${name}", version: ${version}, totalSlots: ${totalSlots}, prepopulateActorNames: ${prepopulateActorNamesStr}, metadata: ${metadataStr}}`; } case "ToServerEvents": { const events = message.val; return `ToServerEvents{count: ${events.length}, events: [${events.map((e) => stringifyEventWrapper(e)).join(", ")}]}`; } case "ToServerAckCommands": { - const { lastCommandIdx } = message.val; - return `ToServerAckCommands{lastCommandIdx: ${stringifyBigInt(lastCommandIdx)}}`; + const { lastCommandCheckpoints } = message.val; + const checkpointsStr = + lastCommandCheckpoints.length > 0 + ? `[${lastCommandCheckpoints.map((cp) => `{actorId: "${cp.actorId}", index: ${stringifyBigInt(cp.index)}}`).join(", ")}]` + : "[]"; + return `ToServerAckCommands{lastCommandCheckpoints: ${checkpointsStr}}`; } case "ToServerStopping": return "ToServerStopping"; - case "ToServerPing": { + case "ToServerPong": { const { ts } = message.val; - return `ToServerPing{ts: ${stringifyBigInt(ts)}}`; + return `ToServerPong{ts: ${stringifyBigInt(ts)}}`; } case "ToServerKvRequest": { const { actorId, requestId, data } = message.val; @@ -255,19 +250,24 @@ export function stringifyToServer(message: protocol.ToServer): string { export function stringifyToClient(message: protocol.ToClient): string { switch (message.tag) { case "ToClientInit": { - const { runnerId, lastEventIdx, metadata } = message.val; + const { runnerId, metadata } = message.val; const metadataStr = `{runnerLostThreshold: ${stringifyBigInt(metadata.runnerLostThreshold)}}`; - return `ToClientInit{runnerId: "${runnerId}", lastEventIdx: ${stringifyBigInt(lastEventIdx)}, metadata: ${metadataStr}}`; + return `ToClientInit{runnerId: "${runnerId}", metadata: ${metadataStr}}`; } - case "ToClientClose": - return "ToClientClose"; + case "ToClientPing": + const { ts } = message.val; + return `ToClientPing{ts: ${stringifyBigInt(ts)}}`; case "ToClientCommands": { const commands = message.val; return `ToClientCommands{count: ${commands.length}, commands: [${commands.map((c) => stringifyCommandWrapper(c)).join(", ")}]}`; } case "ToClientAckEvents": { - const { lastEventIdx } = message.val; - return `ToClientAckEvents{lastEventIdx: ${stringifyBigInt(lastEventIdx)}}`; + const { lastEventCheckpoints } = message.val; + const checkpointsStr = + lastEventCheckpoints.length > 0 + ? `[${lastEventCheckpoints.map((cp) => `{actorId: "${cp.actorId}", index: ${stringifyBigInt(cp.index)}}`).join(", ")}]` + : "[]"; + return `ToClientAckEvents{lastEventCheckpoints: ${checkpointsStr}}`; } case "ToClientKvResponse": { const { requestId, data } = message.val; diff --git a/engine/sdks/typescript/runner/src/tunnel.ts b/engine/sdks/typescript/runner/src/tunnel.ts index 7c67dd3c13..6de8c54f1b 100644 --- a/engine/sdks/typescript/runner/src/tunnel.ts +++ b/engine/sdks/typescript/runner/src/tunnel.ts @@ -671,9 +671,6 @@ export class Tunnel { message.messageKind.val, ); break; - case "DeprecatedTunnelAck": - // Ignore deprecated tunnel ack messages - break; default: unreachable(message.messageKind); } diff --git a/engine/sdks/typescript/test-runner/src/index.ts b/engine/sdks/typescript/test-runner/src/index.ts index 4cb91eee77..b679cfd576 100644 --- a/engine/sdks/typescript/test-runner/src/index.ts +++ b/engine/sdks/typescript/test-runner/src/index.ts @@ -18,7 +18,7 @@ const RIVET_RUNNER_VERSION = process.env.RIVET_RUNNER_VERSION : 1; const RIVET_RUNNER_TOTAL_SLOTS = process.env.RIVET_RUNNER_TOTAL_SLOTS ? Number(process.env.RIVET_RUNNER_TOTAL_SLOTS) - : 100; + : 10000; const RIVET_ENDPOINT = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420"; const RIVET_TOKEN = process.env.RIVET_TOKEN ?? "dev"; const AUTOSTART_SERVER = process.env.NO_AUTOSTART_SERVER === undefined; @@ -160,7 +160,7 @@ async function startRunner(): Promise< onConnected: () => { runnerStarted.resolve(undefined); }, - onDisconnected: () => {}, + onDisconnected: () => { }, onShutdown: () => { runnerStopped.resolve(undefined); }, @@ -259,15 +259,11 @@ async function startRunner(): Promise< }); }); }, - // TODO: - hibernatableWebSocket: undefined as any, - // getActorHibernationConfig(actorId, gatewayId, requestId) { - // const websocketId = Buffer.from(requestId).toString("base64"); - // return { - // enabled: true, - // lastMsgIndex: websocketLastMsgIndexes.get(websocketId), - // }; - // }, + hibernatableWebSocket: { + canHibernate() { + return true; + } + }, }; const runner = new Runner(config); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc1ef52211..0c850afa1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,7 +152,7 @@ importers: version: 5.9.2 vitest: specifier: ^1.6.1 - version: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) engine/sdks/typescript/runner-protocol: dependencies: @@ -211,7 +211,7 @@ importers: version: 5.9.2 vitest: specifier: ^1.6.1 - version: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) engine/tests/load: devDependencies: @@ -272,7 +272,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -287,10 +287,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/chat-room: dependencies: @@ -315,7 +315,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -336,10 +336,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/cloudflare-workers: dependencies: @@ -435,7 +435,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/counter-next-js: dependencies: @@ -487,7 +487,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/cursors: dependencies: @@ -512,7 +512,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -533,10 +533,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/cursors-raw-websocket: dependencies: @@ -561,7 +561,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -582,10 +582,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/deno: dependencies: @@ -713,7 +713,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -743,10 +743,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/hono: dependencies: @@ -796,7 +796,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -811,7 +811,7 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/hono-react: dependencies: @@ -842,7 +842,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -857,10 +857,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/kitchen-sink: dependencies: @@ -885,7 +885,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -897,7 +897,7 @@ importers: version: 5.9.2 vite: specifier: ^5.2.0 - version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/next-js: dependencies: @@ -950,7 +950,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -968,10 +968,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/quickstart-cross-actor-actions: dependencies: @@ -993,7 +993,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1011,10 +1011,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/quickstart-multi-region: dependencies: @@ -1036,7 +1036,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1054,10 +1054,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/quickstart-native-websockets: dependencies: @@ -1082,7 +1082,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1100,10 +1100,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) ws: specifier: ^8.16.0 version: 8.18.3 @@ -1128,7 +1128,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1146,10 +1146,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/quickstart-scheduling: dependencies: @@ -1171,7 +1171,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1189,10 +1189,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/quickstart-state: dependencies: @@ -1214,7 +1214,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1232,10 +1232,10 @@ importers: version: 5.9.3 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/raw-fetch-handler: dependencies: @@ -1269,7 +1269,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^9.1.2 version: 9.2.1 @@ -1281,10 +1281,10 @@ importers: version: 5.9.2 vite: specifier: ^5.4.19 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/raw-websocket-handler: dependencies: @@ -1315,7 +1315,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)) concurrently: specifier: ^9.1.0 version: 9.2.1 @@ -1327,10 +1327,10 @@ importers: version: 5.9.2 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1) + version: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/raw-websocket-handler-proxy: dependencies: @@ -1370,7 +1370,7 @@ importers: version: 8.18.1 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)) concurrently: specifier: ^9.1.0 version: 9.2.1 @@ -1382,10 +1382,10 @@ importers: version: 5.9.2 vite: specifier: ^6.0.5 - version: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1) + version: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/react: dependencies: @@ -1410,7 +1410,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -1425,10 +1425,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/starter: dependencies: @@ -1472,7 +1472,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.0.0 - version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) concurrently: specifier: ^8.2.0 version: 8.2.2 @@ -1484,10 +1484,10 @@ importers: version: 5.9.2 vite: specifier: ^5.0.0 - version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) examples/trpc: dependencies: @@ -1695,7 +1695,7 @@ importers: version: 1.131.36(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.36)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3) '@tanstack/router-plugin': specifier: ^1.131.36 - version: 1.131.36(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))(webpack@5.101.3(esbuild@0.25.12)) + version: 1.131.36(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))(webpack@5.101.3(esbuild@0.25.12)) '@tanstack/store': specifier: ^0.7.5 version: 0.7.5 @@ -1740,7 +1740,7 @@ importers: version: 4.25.1(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.18.7)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.2)(codemirror@6.0.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) actor-core: specifier: ^0.6.3 version: 0.6.3(ws@8.18.3) @@ -1782,7 +1782,7 @@ importers: version: 3.1.1 favigo: specifier: ^1.1.0 - version: 1.1.0(esbuild@0.25.12)(rollup@4.53.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))(webpack@5.101.3(esbuild@0.25.12)) + version: 1.1.0(esbuild@0.25.12)(rollup@4.53.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))(webpack@5.101.3(esbuild@0.25.12)) file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -1857,13 +1857,13 @@ importers: version: 3.1.1(react@19.1.1) vite: specifier: ^5.4.20 - version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vite-plugin-favicons-inject: specifier: ^2.2.0 version: 2.2.0 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) zod: specifier: ^3.25.76 version: 3.25.76 @@ -2068,7 +2068,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -2080,10 +2080,10 @@ importers: version: 3.4.17 vite: specifier: ^5.4.20 - version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) vite-plugin-dts: specifier: ^3.9.1 - version: 3.9.1(@types/node@20.19.13)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + version: 3.9.1(@types/node@20.19.13)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) frontend/packages/icons: dependencies: @@ -2113,7 +2113,7 @@ importers: version: 19.1.1(react@19.1.1) vite: specifier: ^5.4.20 - version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) devDependencies: esbuild: specifier: ^0.25.9 @@ -2151,7 +2151,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) wrangler: specifier: ^4.22.0 version: 4.44.0(@cloudflare/workers-types@4.20251014.0) @@ -2185,7 +2185,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) rivetkit-typescript/packages/framework-base: dependencies: @@ -2201,13 +2201,13 @@ importers: version: 5.9.2 vite: specifier: ^6.3.5 - version: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + version: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.2)(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.5.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.2)(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) rivetkit-typescript/packages/next-js: dependencies: @@ -2357,10 +2357,10 @@ importers: version: 5.9.2 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) ws: specifier: ^8.18.1 version: 8.18.3 @@ -2438,8 +2438,8 @@ importers: specifier: ^3.1.1 version: 3.1.1(@types/react@19.2.2)(react@19.2.0) '@next/mdx': - specifier: ^15.5.2 - version: 15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1)) + specifier: ^15.5.6 + version: 15.5.6(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)) '@next/third-parties': specifier: latest version: 16.0.3(next@15.5.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2))(react@19.2.0) @@ -2650,7 +2650,7 @@ importers: version: 5.9.3 vite-node: specifier: ^5.2.0 - version: 5.2.0(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(ms@2.1.3)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + version: 5.2.0(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(ms@2.1.3)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) yaml: specifier: ^2.8.1 version: 2.8.1 @@ -13070,8 +13070,8 @@ packages: uglify-js: optional: true - terser@5.44.1: - resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} hasBin: true @@ -14153,9 +14153,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} - zustand@5.0.3: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} engines: {node: '>=12.20.0'} @@ -17163,12 +17160,12 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/mdx@15.5.2(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.1.1))': + '@next/mdx@15.5.6(@mdx-js/loader@3.1.1(webpack@5.101.3(esbuild@0.25.9)))(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0))': dependencies: source-map: 0.7.6 optionalDependencies: '@mdx-js/loader': 3.1.1(webpack@5.101.3(esbuild@0.25.9)) - '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.1.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0) '@next/swc-darwin-arm64@15.4.5': optional: true @@ -18995,7 +18992,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.131.36(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))(webpack@5.101.3(esbuild@0.25.12))': + '@tanstack/router-plugin@1.131.36(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))(webpack@5.101.3(esbuild@0.25.12))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) @@ -19013,7 +19010,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) webpack: 5.101.3(esbuild@0.25.12) transitivePeerDependencies: - supports-color @@ -19601,7 +19598,7 @@ snapshots: transitivePeerDependencies: - '@bare-ts/lib' - '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -19609,11 +19606,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -19621,11 +19618,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -19633,11 +19630,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitejs/plugin-react@4.7.0(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -19645,11 +19642,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -19657,7 +19654,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1) + vite: 6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -19675,45 +19672,45 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))': + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) '@vitest/pretty-format@3.1.1': dependencies: @@ -19764,7 +19761,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) '@vitest/utils@1.6.1': dependencies: @@ -21562,11 +21559,13 @@ snapshots: '@next/eslint-plugin-next': 16.0.3 eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.26.0) - eslint-plugin-react: 7.37.5(eslint@8.26.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.26.0) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@1.21.7)) + globals: 16.4.0 + typescript-eslint: 8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -21585,13 +21584,16 @@ snapshots: eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: - debug: 4.4.1 - eslint: 8.26.0 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0) - glob: 7.2.3 - is-glob: 4.0.3 - resolve: 1.22.10 - tsconfig-paths: 3.15.0 + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.1(jiti@1.21.7) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -21606,7 +21608,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.26.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.7.1(eslint-plugin-import@2.32.0(eslint@8.26.0))(eslint@8.26.0))(eslint@8.26.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -21660,8 +21662,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.39.1(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -22095,7 +22097,7 @@ snapshots: sharp: 0.33.5 xml2js: 0.6.2 - favigo@1.1.0(esbuild@0.25.12)(rollup@4.53.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1))(webpack@5.101.3(esbuild@0.25.12)): + favigo@1.1.0(esbuild@0.25.12)(rollup@4.53.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))(webpack@5.101.3(esbuild@0.25.12)): dependencies: favicons: 7.2.0 sharp: 0.33.5 @@ -22103,7 +22105,7 @@ snapshots: optionalDependencies: esbuild: 0.25.12 rollup: 4.53.3 - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) webpack: 5.101.3(esbuild@0.25.12) fb-dotslash@0.5.8: {} @@ -23790,12 +23792,12 @@ snapshots: metro-minify-terser@0.83.2: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.44.1 + terser: 5.44.0 metro-minify-terser@0.83.3: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.44.1 + terser: 5.44.0 metro-resolver@0.83.2: dependencies: @@ -26668,7 +26670,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.1 + terser: 5.44.0 webpack: 5.101.3(esbuild@0.25.12) optionalDependencies: esbuild: 0.25.12 @@ -26680,7 +26682,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.44.1 + terser: 5.44.0 webpack: 5.101.3(esbuild@0.25.9) optionalDependencies: esbuild: 0.25.9 @@ -27465,13 +27467,13 @@ snapshots: - utf-8-validate - zod - vite-node@1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27483,13 +27485,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@3.2.4(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27501,13 +27503,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@3.2.4(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27519,13 +27521,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@3.2.4(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27537,13 +27539,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@3.2.4(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27555,13 +27557,13 @@ snapshots: - supports-color - terser - vite-node@3.2.4(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite-node@3.2.4(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - less @@ -27573,13 +27575,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(ms@2.1.3)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + vite-node@5.2.0(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(ms@2.1.3)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -27594,7 +27596,7 @@ snapshots: - tsx - yaml - vite-plugin-dts@3.9.1(@types/node@20.19.13)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)): + vite-plugin-dts@3.9.1(@types/node@20.19.13)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)): dependencies: '@microsoft/api-extractor': 7.43.0(@types/node@20.19.13) '@rollup/pluginutils': 5.3.0(rollup@4.53.3) @@ -27605,13 +27607,13 @@ snapshots: typescript: 5.9.3 vue-tsc: 1.8.27(typescript@5.9.3) optionalDependencies: - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.2)(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.53.3)(typescript@5.9.2)(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.53.2(@types/node@24.10.1) '@rollup/pluginutils': 5.3.0(rollup@4.53.3) @@ -27624,7 +27626,7 @@ snapshots: magic-string: 0.30.19 typescript: 5.9.2 optionalDependencies: - vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - rollup @@ -27634,29 +27636,29 @@ snapshots: dependencies: favicons: 7.2.0 - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1) + vite: 7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -27668,9 +27670,9 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 - vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -27682,9 +27684,9 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 - vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -27696,9 +27698,9 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 - vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -27710,9 +27712,9 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 - vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 @@ -27724,9 +27726,9 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 - vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1): + vite@6.4.1(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -27742,11 +27744,11 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 tsx: 4.20.5 yaml: 2.8.1 - vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -27762,11 +27764,11 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 tsx: 4.20.6 yaml: 2.8.1 - vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1): + vite@7.2.2(@types/node@22.18.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -27782,12 +27784,12 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 tsx: 4.20.5 yaml: 2.8.1 optional: true - vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -27803,11 +27805,11 @@ snapshots: lightningcss: 1.30.2 sass: 1.93.2 stylus: 0.62.0 - terser: 5.44.1 + terser: 5.44.0 tsx: 4.20.6 yaml: 2.8.1 - vitest@1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@vitest/expect': 1.6.1 '@vitest/runner': 1.6.1 @@ -27826,8 +27828,8 @@ snapshots: strip-literal: 2.1.1 tinybench: 2.9.0 tinypool: 0.8.4 - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 1.6.1(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.18.1 @@ -27841,11 +27843,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -27863,8 +27865,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 3.2.4(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 3.2.4(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -27880,11 +27882,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -27902,8 +27904,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 3.2.4(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 3.2.4(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -27920,11 +27922,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -27942,8 +27944,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 3.2.4(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 3.2.4(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -27959,11 +27961,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -27981,8 +27983,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 3.2.4(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 3.2.4(@types/node@24.10.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -27998,11 +28000,11 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1)) + '@vitest/mocker': 3.2.4(vite@5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -28020,8 +28022,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) - vite-node: 3.2.4(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.1) + vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 3.2.4(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -28088,6 +28090,39 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webpack@5.101.3(esbuild@0.25.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.25.12)(webpack@5.101.3(esbuild@0.25.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + webpack@5.101.3(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 @@ -28347,16 +28382,14 @@ snapshots: dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.1.12): + zod-validation-error@4.0.2(zod@3.25.76): dependencies: - zod: 4.1.12 + zod: 3.25.76 zod@3.22.3: {} zod@3.25.76: {} - zod@4.1.12: {} - zustand@5.0.3(@types/react@19.2.2)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)): optionalDependencies: '@types/react': 19.2.2