diff --git a/engine/artifacts/errors/actor.shutdown_timeout.json b/engine/artifacts/errors/actor.shutdown_timeout.json deleted file mode 100644 index 5405ac64e7..0000000000 --- a/engine/artifacts/errors/actor.shutdown_timeout.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "shutdown_timeout", - "group": "actor", - "message": "Actor shutdown timed out." -} \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/config.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/config.rs index 0abbe759c6..0b9c854f63 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/config.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/config.rs @@ -43,11 +43,6 @@ impl Default for CanHibernateWebSocket { } } -#[derive(Clone, Debug, Default)] -pub struct ActorConfigOverrides { - pub sleep_grace_period: Option, -} - #[derive(Clone, Debug)] pub struct ActionDefinition { pub name: String, @@ -84,7 +79,6 @@ pub struct ActorConfig { pub max_outgoing_message_size: u32, pub preload_max_workflow_bytes: Option, pub preload_max_connections_bytes: Option, - pub overrides: Option, pub actions: Vec, /// Author-declared inspector tab entries (custom tabs + built-in /// hides). Validated upstream (Zod / builder). @@ -201,12 +195,7 @@ impl ActorConfig { } pub fn effective_sleep_grace_period(&self) -> Duration { - cap_duration( - self.sleep_grace_period, - self.overrides - .as_ref() - .and_then(|overrides| overrides.sleep_grace_period), - ) + self.sleep_grace_period } /// Runtime authority for rejecting malformed config that bypassed the @@ -247,21 +236,12 @@ impl Default for ActorConfig { max_outgoing_message_size: DEFAULT_MAX_OUTGOING_MESSAGE_SIZE, preload_max_workflow_bytes: None, preload_max_connections_bytes: None, - overrides: None, actions: Vec::new(), inspector_tabs: Vec::new(), } } } -fn cap_duration(duration: Duration, override_duration: Option) -> Duration { - if let Some(override_duration) = override_duration { - duration.min(override_duration) - } else { - duration - } -} - fn duration_ms(value: u32) -> Duration { Duration::from_millis(u64::from(value)) } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/keys.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/keys.rs index 41b57c6ce3..dcf1975f4b 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/keys.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/keys.rs @@ -16,8 +16,6 @@ pub const PERSIST_DATA_KEY: &[u8] = &[1]; pub const CONN_PREFIX: [u8; 1] = [2]; // The inspector auth token lives at [3]. pub const INSPECTOR_TOKEN_KEY: [u8; 1] = [3]; -// User KV entries live under [4, ...user_key]. -pub const KV_PREFIX: [u8; 1] = [4]; // Queue storage lives under [5, ...]. pub const QUEUE_PREFIX: [u8; 1] = [5]; // Workflow storage lives under [6, ...]. @@ -72,22 +70,6 @@ struct QueueInvalidMessageKey { reason: String, } -pub fn make_prefixed_key(key: &[u8]) -> Vec { - concat_prefix(&KV_PREFIX, key) -} - -pub fn remove_prefix_from_key(prefixed_key: &[u8]) -> &[u8] { - &prefixed_key[KV_PREFIX.len()..] -} - -pub fn make_workflow_key(key: &[u8]) -> Vec { - concat_prefix(&WORKFLOW_STORAGE_PREFIX, key) -} - -pub fn make_traces_key(key: &[u8]) -> Vec { - concat_prefix(&TRACES_STORAGE_PREFIX, key) -} - pub fn make_connection_key(conn_id: &str) -> Vec { concat_prefix(&CONN_PREFIX, conn_id.as_bytes()) } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs index 2e2954658d..a719bbfd43 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/messages.rs @@ -66,10 +66,6 @@ impl Request { ) } - pub fn into_inner(self) -> http::Request> { - self.0 - } - pub fn into_body(self) -> Vec { self.0.into_body() } @@ -154,10 +150,6 @@ impl Response { ) } - pub fn into_inner(self) -> http::Response> { - self.0 - } - pub fn into_body(self) -> Vec { self.0.into_body() } diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/mod.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/mod.rs index 04855d127a..d282470bf0 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/mod.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/mod.rs @@ -21,7 +21,7 @@ pub mod task_types; pub(crate) mod work_registry; pub use action::ActionDispatchError; -pub use config::{ActionDefinition, ActorConfig, ActorConfigOverrides, CanHibernateWebSocket}; +pub use config::{ActionDefinition, ActorConfig, CanHibernateWebSocket}; pub use connection::ConnHandle; pub use context::{ActorContext, ActorWorkRegion, KeepAwakeRegion, WebSocketCallbackRegion}; pub use factory::{ActorEntryFn, ActorFactory}; @@ -40,5 +40,5 @@ pub use task::{ ActionDispatchResult, ActorTask, DispatchCommand, HttpDispatchResult, LifecycleCommand, LifecycleEvent, LifecycleState, }; -pub use task_types::{ActorChildOutcome, ShutdownKind, StateMutationReason, UserTaskKind}; +pub use task_types::{ShutdownKind, StateMutationReason, UserTaskKind}; pub use work_registry::{ActorWorkKind, ActorWorkPolicy}; diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs index 67a692506e..7f34e55aa7 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/sqlite.rs @@ -141,20 +141,6 @@ impl SqliteDb { self.backend } - pub async fn get_pages( - &self, - request: protocol::SqliteGetPagesRequest, - ) -> Result { - self.handle()?.sqlite_get_pages(request).await - } - - pub async fn commit( - &self, - request: protocol::SqliteCommitRequest, - ) -> Result { - self.handle()?.sqlite_commit(request).await - } - pub async fn open(&self) -> Result<()> { match self.backend { SqliteBackend::LocalNative => { diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/state.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/state.rs index 55a347db3e..9e3ea58560 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/state.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/state.rs @@ -511,15 +511,6 @@ impl ActorContext { self.0.persisted.read().scheduled_events.clone() } - pub fn set_scheduled_events(&self, scheduled_events: Vec) { - self.0.persisted.write().scheduled_events = scheduled_events; - self.0 - .metrics - .inc_state_mutation(StateMutationReason::ScheduledEventsUpdate); - self.mark_dirty(); - self.schedule_save(None); - } - pub(crate) fn update_scheduled_events( &self, update: impl FnOnce(&mut Vec) -> R, @@ -537,19 +528,6 @@ impl ActorContext { result } - pub fn set_input(&self, input: Option>) { - self.0.persisted.write().input = input; - self.0 - .metrics - .inc_state_mutation(StateMutationReason::InputSet); - self.mark_dirty(); - self.schedule_save(None); - } - - pub fn input(&self) -> Option> { - self.0.persisted.read().input.clone() - } - pub fn set_has_initialized(&self, has_initialized: bool) { { let mut persisted = self.0.persisted.write(); diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/task_types.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/task_types.rs index 0a29ecabe4..27fa16757a 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/task_types.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/actor/task_types.rs @@ -1,7 +1,3 @@ -use std::{any::Any, fmt}; - -use anyhow::Result; - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum LifecycleState { #[default] @@ -66,7 +62,6 @@ impl UserTaskKind { pub enum StateMutationReason { InternalReplace, ScheduledEventsUpdate, - InputSet, HasInitialized, } @@ -75,36 +70,7 @@ impl StateMutationReason { match self { Self::InternalReplace => "internal_replace", Self::ScheduledEventsUpdate => "scheduled_events_update", - Self::InputSet => "input_set", Self::HasInitialized => "has_initialized", } } } - -pub enum ActorChildOutcome { - UserTaskFinished { - kind: UserTaskKind, - result: Result<()>, - }, - UserTaskPanicked { - kind: UserTaskKind, - payload: Box, - }, -} - -impl fmt::Debug for ActorChildOutcome { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ActorChildOutcome::UserTaskFinished { kind, result } => f - .debug_struct("UserTaskFinished") - .field("kind", kind) - .field("result", result) - .finish(), - ActorChildOutcome::UserTaskPanicked { kind, .. } => f - .debug_struct("UserTaskPanicked") - .field("kind", kind) - .field("payload", &"") - .finish(), - } - } -} diff --git a/rivetkit-rust/packages/rivetkit-core/src/actor/vars.rs b/rivetkit-rust/packages/rivetkit-core/src/actor/vars.rs deleted file mode 100644 index 0d19263d9e..0000000000 --- a/rivetkit-rust/packages/rivetkit-core/src/actor/vars.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::sync::Arc; -use std::sync::RwLock; - -#[derive(Clone, Default)] -pub struct ActorVars(Arc>>); - -impl ActorVars { - pub fn vars(&self) -> Vec { - self.0.read().expect("actor vars lock poisoned").clone() - } - - pub fn set_vars(&self, vars: Vec) { - *self.0.write().expect("actor vars lock poisoned") = vars; - } -} - -impl std::fmt::Debug for ActorVars { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ActorVars") - .field("len", &self.vars().len()) - .finish() - } -} diff --git a/rivetkit-rust/packages/rivetkit-core/src/error.rs b/rivetkit-rust/packages/rivetkit-core/src/error.rs index 4241551135..e83f14694d 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/error.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/error.rs @@ -73,9 +73,6 @@ pub enum ActorLifecycle { #[error("destroying", "Actor is destroying.")] Destroying, - #[error("shutdown_timeout", "Actor shutdown timed out.")] - ShutdownTimeout, - #[error("dropped_reply", "Actor reply channel was dropped without a response.")] DroppedReply, diff --git a/rivetkit-rust/packages/rivetkit-core/src/lib.rs b/rivetkit-rust/packages/rivetkit-core/src/lib.rs index 6e8b6a8d24..5f3ad667da 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/lib.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/lib.rs @@ -112,9 +112,7 @@ pub mod websocket; pub use actor::{kv, sqlite}; pub use actor::action::ActionDispatchError; -pub use actor::config::{ - ActionDefinition, ActorConfig, ActorConfigInput, ActorConfigOverrides, CanHibernateWebSocket, -}; +pub use actor::config::{ActionDefinition, ActorConfig, ActorConfigInput, CanHibernateWebSocket}; pub use actor::connection::ConnHandle; pub use actor::context::{ActorContext, ActorWorkRegion, KeepAwakeRegion, WebSocketCallbackRegion}; pub use actor::factory::{ActorEntryFn, ActorFactory}; @@ -141,7 +139,7 @@ pub use actor::work_registry::{ActorWorkKind, ActorWorkPolicy}; pub use error::ActorLifecycle; pub use inspector::{Inspector, InspectorSnapshot}; pub use registry::{CoreRegistry, EngineSpawnMode, ServeConfig}; -pub use runtime::{RuntimeBoxFuture, RuntimeSpawner, boxed_runtime_future}; +pub use runtime::{RuntimeBoxFuture, RuntimeSpawner}; pub use serverless::{CoreServerlessRuntime, ServerlessRequest, ServerlessResponse}; pub use types::{ ActorKey, ActorKeySegment, ConnId, ListOpts, SaveStateOpts, WsMessage, format_actor_key, diff --git a/rivetkit-rust/packages/rivetkit-core/src/metrics_endpoint.rs b/rivetkit-rust/packages/rivetkit-core/src/metrics_endpoint.rs index 97881e2765..2f9a51deed 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/metrics_endpoint.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/metrics_endpoint.rs @@ -125,13 +125,6 @@ pub fn render_prometheus_metrics() -> Result { }) } -pub fn authorization_bearer_token(headers: &http::HeaderMap) -> Option<&str> { - headers - .get(http::header::AUTHORIZATION) - .and_then(|value| value.to_str().ok()) - .and_then(bearer_token_from_authorization) -} - pub fn authorization_bearer_token_map(headers: &HashMap) -> Option<&str> { headers .iter() diff --git a/rivetkit-rust/packages/rivetkit-core/src/runtime.rs b/rivetkit-rust/packages/rivetkit-core/src/runtime.rs index b73a5f0d05..bd6a42cc55 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/runtime.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/runtime.rs @@ -79,57 +79,3 @@ impl RuntimeSpawner { tokio::task::spawn_local(future) } } - -#[cfg(feature = "native-runtime")] -pub fn boxed_runtime_future(future: F) -> RuntimeBoxFuture -where - F: Future + Send + 'static, -{ - Box::pin(future) -} - -#[cfg(not(any(feature = "native-runtime", feature = "wasm-runtime")))] -pub fn boxed_runtime_future(future: F) -> RuntimeBoxFuture -where - F: Future + Send + 'static, -{ - Box::pin(future) -} - -#[cfg(feature = "wasm-runtime")] -pub fn boxed_runtime_future(future: F) -> RuntimeBoxFuture -where - F: Future + 'static, -{ - Box::pin(future) -} - -#[cfg(all(test, feature = "wasm-runtime"))] -mod tests { - use std::cell::RefCell; - use std::rc::Rc; - - use super::{RuntimeBoxFuture, boxed_runtime_future}; - - fn accepts_wasm_local_callback( - callback: impl Fn() -> RuntimeBoxFuture<()> + 'static, - ) -> impl Fn() -> RuntimeBoxFuture<()> { - callback - } - - #[test] - fn wasm_runtime_box_future_accepts_local_callbacks() { - let state = Rc::new(RefCell::new(0)); - let callback = accepts_wasm_local_callback({ - let state = state.clone(); - move || { - let state = state.clone(); - boxed_runtime_future(async move { - *state.borrow_mut() += 1; - }) - } - }); - - let _future = callback(); - } -} diff --git a/rivetkit-rust/packages/rivetkit-core/src/serverless.rs b/rivetkit-rust/packages/rivetkit-core/src/serverless.rs index 8b90c7f300..25665bef57 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/serverless.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/serverless.rs @@ -233,12 +233,6 @@ impl CoreServerlessRuntime { } } - pub async fn active_envoy_actor_count(&self) -> Option { - self.active_envoy_status() - .await - .map(|status| status.active_actor_count) - } - pub async fn active_envoy_status(&self) -> Option { self.envoy .lock() diff --git a/rivetkit-rust/packages/rivetkit-core/src/websocket.rs b/rivetkit-rust/packages/rivetkit-core/src/websocket.rs index 9346583222..2a7196ea5b 100644 --- a/rivetkit-rust/packages/rivetkit-core/src/websocket.rs +++ b/rivetkit-rust/packages/rivetkit-core/src/websocket.rs @@ -48,12 +48,6 @@ impl WebSocket { })) } - pub fn from_sender(sender: WebSocketSender) -> Self { - let websocket = Self::new(); - websocket.configure_sender(sender); - websocket - } - pub fn send(&self, msg: WsMessage) { if let Err(error) = self.try_send(msg) { tracing::error!(?error, "failed to send websocket message"); diff --git a/rivetkit-rust/packages/rivetkit-core/tests/config.rs b/rivetkit-rust/packages/rivetkit-core/tests/config.rs index b550599730..89c98ccf60 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/config.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/config.rs @@ -84,7 +84,6 @@ mod moved_tests { config.can_hibernate_websocket, super::CanHibernateWebSocket::Bool(false), )); - assert!(config.overrides.is_none()); } #[test] diff --git a/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs b/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs index bb7456578c..538ececbc7 100644 --- a/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs +++ b/rivetkit-rust/packages/rivetkit-core/tests/schedule.rs @@ -179,12 +179,14 @@ mod moved_tests { schedule.configure_schedule_envoy(handle, Some(8)); let future_ts = now_timestamp_ms() + 60_000; - schedule.set_scheduled_events(vec![PersistedScheduleEvent { - event_id: "event-1".to_owned(), - timestamp: future_ts, - action: "tick".to_owned(), - args: Some(vec![1, 2, 3]), - }]); + schedule.update_scheduled_events(|events| { + *events = vec![PersistedScheduleEvent { + event_id: "event-1".to_owned(), + timestamp: future_ts, + action: "tick".to_owned(), + args: Some(vec![1, 2, 3]), + }]; + }); schedule.sync_future_alarm_logged(); assert_eq!( diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs b/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs index b3d8291da6..ff170c4383 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs @@ -209,11 +209,6 @@ impl ActorContext { conn.set_state_initial(bytes); Ok(()) } - - #[allow(dead_code)] - pub(crate) fn has_conn_changes(&self) -> bool { - self.inner.conns().any(|conn| conn.is_hibernatable()) - } } #[napi] diff --git a/rivetkit-typescript/packages/rivetkit-wasm/index.d.ts b/rivetkit-typescript/packages/rivetkit-wasm/index.d.ts index b9f1184b07..581bef9c66 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/index.d.ts +++ b/rivetkit-typescript/packages/rivetkit-wasm/index.d.ts @@ -1,8 +1,4 @@ export function start(): void; -export function awaitPromise(promise: Promise): Promise; -export function uint8ArrayFromBytes(bytes: Uint8Array): Uint8Array; -export function bridgeRivetErrorPrefix(): string; -export function roundTripBytes(bytes: Uint8Array): Uint8Array; export class ActorContext { free(): void; diff --git a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs index 187ce756d8..79c2c6d905 100644 --- a/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-wasm/src/lib.rs @@ -1992,26 +1992,6 @@ impl WasmSqliteDb { } } -#[wasm_bindgen(js_name = bridgeRivetErrorPrefix)] -pub fn bridge_rivet_error_prefix() -> String { - BRIDGE_RIVET_ERROR_PREFIX.to_string() -} - -#[wasm_bindgen(js_name = roundTripBytes)] -pub fn round_trip_bytes(bytes: Vec) -> Vec { - bytes -} - -#[wasm_bindgen(js_name = uint8ArrayFromBytes)] -pub fn uint8_array_from_bytes(bytes: Vec) -> Uint8Array { - Uint8Array::from(bytes.as_slice()) -} - -#[wasm_bindgen(js_name = awaitPromise)] -pub async fn await_promise(promise: Promise) -> Result { - JsFuture::from(promise).await -} - #[derive(Default, serde::Deserialize)] #[serde(default, rename_all = "camelCase")] struct WasmRequestSaveOpts { diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts index ea94448566..9f9f05bfd1 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/config.ts @@ -21,7 +21,6 @@ export const ACTOR_CONTEXT_INTERNAL_SYMBOL = Symbol( "rivetkit.actor_context_internal", ); export const RAW_STATE_SYMBOL = Symbol("rivetkit.raw_state"); -export const CONN_DRIVER_SYMBOL = Symbol("rivetkit.conn_driver"); export const CONN_STATE_MANAGER_SYMBOL = Symbol("rivetkit.conn_state_manager"); export interface ActorLogger { diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts deleted file mode 100644 index 95e1665b74..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ /dev/null @@ -1,2484 +0,0 @@ -// TODO(sleep-cleanup): investigate whether ActorInstance / drivers/engine -// path is still needed after native path maturation. preventSleep tracking -// below is retained for legacy driver compatibility but no longer drives core. -import type { OtlpExportTraceServiceRequestJson } from "@rivetkit/traces"; -import { - createNoopTraces, - createTraces, - type SpanHandle, - type SpanStatusInput, - type Traces, -} from "@rivetkit/traces"; -import invariant from "invariant"; -import { ActorMetrics, type StartupTimingKey } from "@/actor/metrics"; -import type { Client } from "@/client/client"; -import type { ActorKey } from "@/client/query"; -import { getBaseLogger, getIncludeTarget, type Logger } from "@/common/log"; -import { stringifyError } from "@/common/utils"; -import type { UniversalWebSocket } from "@/common/websocket-interface"; -import { ActorInspector } from "@/inspector/actor-inspector"; -import type { Registry } from "@/mod"; -import { - ACTOR_VERSIONED, - CONN_VERSIONED, -} from "@/schemas/actor-persist/versioned"; -import { EXTRA_ERROR_LOG, promiseWithResolvers } from "@/utils"; -import { getRivetExperimentalOtel } from "@/utils/env-vars"; -import { - type Actions, - type ActorConfig, - type ActorConfigInput, - ActorConfigSchema, - getRunFunction, -} from "../config"; -import { - handleInboundHibernatableWebSocketMessage as applyInboundHibernatableWebSocketMessage, - HibernatableWebSocketAckState, -} from "../conn/hibernatable-websocket-ack-state"; -import { - type AnyConn, - CONN_STATE_MANAGER_SYMBOL, - type Conn, - type ConnId, -} from "../conn/mod"; -import { - convertConnFromBarePersistedConn, - type PersistedConn, -} from "../conn/persisted"; -import { - type ActionContext, - ActorContext, - RequestContext, - WebSocketContext, -} from "../contexts"; - -import type { AnyDatabaseProvider, InferDatabaseClient } from "../database"; -import { ActorDefinition } from "../definition"; -import type { ActorDriver } from "../driver"; -import * as errors from "../errors"; -import { serializeActorKey } from "../keys"; -import { getValueLength, processMessage } from "../protocol/old"; -import type { InputData } from "../protocol/serde"; -import { Schedule } from "../schedule"; -import { - type EventSchemaConfig, - getEventCanSubscribe, - getQueueCanPublish, - type QueueSchemaConfig, -} from "../schema"; -import { - assertUnreachable, - DeadlineError, - deadline, - generateSecureToken, -} from "../utils"; -import { ConnectionManager } from "./connection-manager"; -import { EventManager } from "./event-manager"; -import { KEYS, workflowStoragePrefix } from "./keys"; -import { - convertActorFromBarePersisted, - type PersistedActor, -} from "./persisted"; -import type { PreloadedEntries, PreloadMap } from "./preload-map"; -import { QueueManager } from "./queue-manager"; -import { ScheduleManager } from "./schedule-manager"; -import { type SaveStateOptions, StateManager } from "./state-manager"; -import { ActorTracesDriver } from "./traces-driver"; -import { TrackedWebSocket } from "./tracked-websocket"; -import { WriteCollector } from "./write-collector"; - -export type { SaveStateOptions }; - -/** - * Symbol used by subsystems (e.g., queue-manager) to access the - * unexpected KV round-trip warning without exposing it as a public method. - */ -export const WARN_UNEXPECTED_KV_ROUND_TRIP = Symbol( - "warnUnexpectedKvRoundTrip", -); - -export function actor< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase extends AnyDatabaseProvider, - TEvents extends EventSchemaConfig = Record, - TQueues extends QueueSchemaConfig = Record, - TActions extends Actions< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase, - TEvents, - TQueues - > = Actions< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase, - TEvents, - TQueues - >, ->( - input: ActorConfigInput< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase, - TEvents, - TQueues, - TActions - >, -): ActorDefinition< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase, - TEvents, - TQueues, - TActions -> { - const config = ActorConfigSchema.parse(input) as ActorConfig< - TState, - TConnParams, - TConnState, - TVars, - TInput, - TDatabase, - TEvents, - TQueues - >; - return new ActorDefinition(config); -} - -enum CanSleep { - Yes, - NotReady, - NotStarted, - PreventSleep, - ActiveConns, - ActiveDisconnectCallbacks, - ActiveHonoHttpRequests, - ActiveKeepAwake, - ActiveInternalKeepAwake, - ActiveRun, - ActiveWebSocketCallbacks, -} - -/** - * Names of actor-managed async regions that should keep the actor awake while - * work is still running. - */ -interface ActiveAsyncRegionCounts { - keepAwake: number; - internalKeepAwake: number; - websocketCallbacks: number; -} - -/** - * Error messages for the async-region counters. These are used when a counter - * underflows, which indicates mismatched begin/end bookkeeping. - */ -const ACTIVE_ASYNC_REGION_ERROR_MESSAGES: Record< - keyof ActiveAsyncRegionCounts, - string -> = { - keepAwake: "active keep awake count went below 0, this is a RivetKit bug", - internalKeepAwake: - "active internal keep awake count went below 0, this is a RivetKit bug", - websocketCallbacks: - "active websocket callback count went below 0, this is a RivetKit bug", -}; - -/** - * Minimal lifecycle contract shared by static and dynamic actor instances. - * - * Runtime internals (connections, inspector, queue manager, etc) are exposed - * only on `ActorInstance`. - */ -export interface BaseActorInstance< - _S = any, - _CP = any, - _CS = any, - _V = any, - _I = any, - _DB extends AnyDatabaseProvider = AnyDatabaseProvider, - _E extends EventSchemaConfig = Record, - _Q extends QueueSchemaConfig = Record, -> { - readonly id: string; - readonly isStopping: boolean; - onStop(mode: "sleep" | "destroy"): Promise; - onAlarm(): Promise; - cleanupPersistedConnections?(reason?: string): Promise; - getHibernatingWebSocketMetadata?(): Array<{ - gatewayId: ArrayBuffer; - requestId: ArrayBuffer; - serverMessageIndex: number; - clientMessageIndex: number; - path: string; - headers: Record; - }>; -} - -/** Actor type alias with all `any` types. */ -export type AnyActorInstance = BaseActorInstance< - any, - any, - any, - any, - any, - any, - any, - any ->; - -/** Static actor type alias with all `any` types. */ -export type AnyStaticActorInstance = ActorInstance< - any, - any, - any, - any, - any, - any, - any, - any ->; - -export function isStaticActorInstance( - actor: AnyActorInstance, -): actor is AnyStaticActorInstance { - if (actor instanceof ActorInstance) { - return true; - } - - if (!actor || typeof actor !== "object") { - return false; - } - - const candidate = actor as Partial; - return ( - typeof candidate.executeAction === "function" && - typeof candidate.beginHonoHttpRequest === "function" && - typeof candidate.endHonoHttpRequest === "function" && - typeof candidate.connectionManager === "object" && - candidate.connectionManager !== null - ); -} - -export type ExtractActorState = - A extends ActorInstance - ? State - : never; - -export type ExtractActorConnParams = - A extends ActorInstance - ? ConnParams - : never; - -export type ExtractActorConnState = - A extends ActorInstance - ? ConnState - : never; - -// MARK: - Main ActorInstance Class -export class ActorInstance< - S, - CP, - CS, - V, - I, - DB extends AnyDatabaseProvider, - E extends EventSchemaConfig = Record, - Q extends QueueSchemaConfig = Record, -> implements BaseActorInstance -{ - // MARK: - Core Properties - actorContext: ActorContext; - #config: ActorConfig; - driver!: ActorDriver; - #inlineClient!: Client>; - #actorId!: string; - #name!: string; - #key!: ActorKey; - #actorKeyString!: string; - #region!: string; - - // MARK: - Managers - connectionManager!: ConnectionManager; - - stateManager!: StateManager; - - eventManager!: EventManager; - - #scheduleManager!: ScheduleManager; - - queueManager!: QueueManager; - - // MARK: - Logging - #log!: Logger; - #rLog!: Logger; - - // MARK: - Lifecycle State - /** - * If the core actor initiation has set up. - * - * Almost all actions on this actor will throw an error if false. - **/ - #ready = false; - /** - * If the actor has fully started. - * - * The only purpose of this is to prevent sleeping until started. - */ - #started = false; - #sleepCalled = false; - #destroyCalled = false; - #stopCalled = false; - #shutdownComplete = false; - #sleepTimeout?: NodeJS.Timeout; - #abortController = new AbortController(); - - // MARK: - Variables & Database - #vars?: V; - #db?: InferDatabaseClient; - #metrics = new ActorMetrics(); - - // MARK: - Preload - #workflowPreloadEntries?: PreloadedEntries; - #expectNoKvRoundTrips = false; - - // MARK: - Background Tasks - #backgroundPromises: Promise[] = []; - #websocketCallbackPromises: Promise[] = []; - #preventSleepClearedPromise?: ReturnType>; - #runPromise?: Promise; - #runHandlerActive = false; - #activeQueueWaitCount = 0; - - // MARK: - HTTP/WebSocket Tracking - #activeHonoHttpRequests = 0; - #activeAsyncRegionCounts: ActiveAsyncRegionCounts = { - keepAwake: 0, - internalKeepAwake: 0, - websocketCallbacks: 0, - }; - #preventSleep = false; - - // MARK: - Deprecated (kept for compatibility) - #schedule!: Schedule; - - // MARK: - Hibernatable WebSocket State - #hibernatableWebSocketAckState = new HibernatableWebSocketAckState(); - - // MARK: - Inspector - #inspectorToken?: string; - #inspector: ActorInspector; - - // MARK: - Tracing - #traces!: Traces; - - // MARK: - Driver Overrides - /** - * Per-instance config option overrides applied by the driver after creation. - * When set, the effective option value is the minimum of the base config - * value and the override value. - */ - overrides: { - sleepGracePeriod?: number; - } = {}; - - // MARK: - Constructor - constructor(config: ActorConfig) { - this.#config = config; - this.actorContext = new ActorContext(this); - this.#inspector = new ActorInspector(this); - } - - // MARK: - Public Getters - get log(): Logger { - invariant(this.#log, "log not configured"); - return this.#log; - } - - get rLog(): Logger { - invariant(this.#rLog, "log not configured"); - return this.#rLog; - } - - get isStopping(): boolean { - return this.#stopCalled; - } - - get id(): string { - return this.#actorId; - } - - get name(): string { - return this.#name; - } - - get key(): ActorKey { - return this.#key; - } - - get region(): string { - return this.#region; - } - - get inlineClient(): Client> { - return this.#inlineClient; - } - - get inspector(): ActorInspector { - return this.#inspector; - } - - get traces(): Traces { - return this.#traces; - } - - get inspectorToken(): string | undefined { - return this.#inspectorToken; - } - - get metrics(): ActorMetrics { - return this.#metrics; - } - - // MARK: - Tracing - getCurrentTraceSpan(): SpanHandle | null { - return this.#traces.getCurrentSpan(); - } - - startTraceSpan( - name: string, - attributes?: Record, - ): SpanHandle { - return this.#traces.startSpan(name, { - parent: this.#traces.getCurrentSpan() ?? undefined, - attributes: this.#traceAttributes(attributes), - }); - } - - endTraceSpan(handle: SpanHandle, status?: SpanStatusInput): void { - this.#traces.endSpan(handle, status ? { status } : undefined); - } - - async runInTraceSpan( - name: string, - attributes: Record | undefined, - fn: () => T | Promise, - ): Promise { - const span = this.startTraceSpan(name, attributes); - try { - const result = this.#traces.withSpan(span, fn); - const resolved = result instanceof Promise ? await result : result; - this.#traces.endSpan(span, { - status: { code: "OK" }, - }); - return resolved; - } catch (error) { - this.#traces.endSpan(span, { - status: { - code: "ERROR", - message: stringifyError(error), - }, - }); - throw error; - } - } - - emitTraceEvent( - name: string, - attributes?: Record, - handle?: SpanHandle, - ): void { - const span = handle ?? this.#traces.getCurrentSpan(); - if (!span) { - return; - } - this.#traces.emitEvent(span, name, { - attributes: this.#traceAttributes(attributes), - timeUnixMs: Date.now(), - }); - } - - get workflowPreloadEntries(): PreloadedEntries | undefined { - return this.#workflowPreloadEntries; - } - - [WARN_UNEXPECTED_KV_ROUND_TRIP](method: string): void { - if (this.#expectNoKvRoundTrips) { - this.#rLog.warn({ - msg: "unexpected KV round-trip during startup", - method, - }); - this.#expectNoKvRoundTrips = false; - } - } - - static #userStartupKeys: Set = new Set([ - "createStateMs", - "onCreateMs", - "onWakeMs", - "createVarsMs", - "dbMigrateMs", - ]); - - /** - * Measure the duration of an async startup step. Logs at debug level - * and records the duration on the startup metrics object. - * - * When `pauseKvGuard` is true, the unexpected KV round-trip guard is - * suspended for the duration of the callback (used for user code - * callbacks that may legitimately issue KV reads). - */ - async #measureStartup( - name: StartupTimingKey, - fn: () => Promise | T, - opts?: { pauseKvGuard?: boolean }, - ): Promise { - const savedGuard = this.#expectNoKvRoundTrips; - if (opts?.pauseKvGuard) { - this.#expectNoKvRoundTrips = false; - } - const start = performance.now(); - try { - const result = await fn(); - return result; - } finally { - const durationMs = performance.now() - start; - this.#metrics.startup[name] = durationMs; - const prefix = ActorInstance.#userStartupKeys.has(name) - ? "perf user" - : "perf internal"; - this.#rLog.debug({ msg: `${prefix}: ${name}`, durationMs }); - if (opts?.pauseKvGuard) { - this.#expectNoKvRoundTrips = savedGuard; - } - } - } - - get conns(): Map> { - return this.connectionManager.connections; - } - - /** - * Records delivery of an inbound indexed hibernatable websocket message and - * schedules persistence so the index is only acked after a durable write. - */ - handleInboundHibernatableWebSocketMessage( - conn: AnyConn | undefined, - payload: InputData, - rivetMessageIndex: number | undefined, - ): void { - if (!conn?.isHibernatable) { - return; - } - - const connStateManager = conn[CONN_STATE_MANAGER_SYMBOL]; - const hibernatable = connStateManager.hibernatableData; - if (!hibernatable) { - return; - } - - invariant( - typeof rivetMessageIndex === "number", - "missing rivetMessageIndex for hibernatable websocket message", - ); - - applyInboundHibernatableWebSocketMessage({ - connId: conn.id, - hibernatable, - messageLength: getValueLength(payload), - rivetMessageIndex, - ackState: this.#hibernatableWebSocketAckState, - saveState: (opts) => { - void this.stateManager.saveState(opts).catch((error) => { - this.#rLog.error({ - msg: "failed to schedule hibernatable websocket persistence", - connId: conn.id, - error: stringifyError(error), - }); - }); - }, - }); - } - - onCreateHibernatableConn(conn: AnyConn): void { - const hibernatable = conn[CONN_STATE_MANAGER_SYMBOL].hibernatableData; - if (!hibernatable) { - return; - } - - this.#hibernatableWebSocketAckState.createConnEntry( - conn.id, - hibernatable.serverMessageIndex, - ); - } - - onDestroyHibernatableConn(conn: AnyConn): void { - this.#hibernatableWebSocketAckState.deleteConnEntry(conn.id); - } - - onBeforePersistHibernatableConn(conn: AnyConn): void { - const hibernatable = - conn[CONN_STATE_MANAGER_SYMBOL].hibernatableDataOrError(); - this.#hibernatableWebSocketAckState.onBeforePersist( - conn.id, - hibernatable.serverMessageIndex, - ); - } - - onAfterPersistHibernatableConn(conn: AnyConn): void { - const hibernatable = - conn[CONN_STATE_MANAGER_SYMBOL].hibernatableDataOrError(); - const ackServerMessageIndex = - this.#hibernatableWebSocketAckState.consumeAck(conn.id); - if (ackServerMessageIndex === undefined) { - return; - } - - this.driver.ackHibernatableWebSocketMessage?.( - hibernatable.gatewayId, - hibernatable.requestId, - ackServerMessageIndex, - ); - } - - getHibernatingWebSocketMetadata(): Array<{ - gatewayId: ArrayBuffer; - requestId: ArrayBuffer; - serverMessageIndex: number; - clientMessageIndex: number; - path: string; - headers: Record; - }> { - return Array.from(this.conns.values(), (conn) => { - const hibernatable = - conn[CONN_STATE_MANAGER_SYMBOL].hibernatableData; - if (!hibernatable) { - return undefined; - } - return { - gatewayId: hibernatable.gatewayId.slice(0), - requestId: hibernatable.requestId.slice(0), - serverMessageIndex: hibernatable.serverMessageIndex, - clientMessageIndex: hibernatable.clientMessageIndex, - path: hibernatable.requestPath, - headers: { ...hibernatable.requestHeaders }, - }; - }).filter((entry) => entry !== undefined); - } - - get schedule(): Schedule { - return this.#schedule; - } - - get abortSignal(): AbortSignal { - return this.#abortController.signal; - } - - get preventSleep(): boolean { - return this.#preventSleep; - } - - get actions(): string[] { - return Object.keys(this.#config.actions ?? {}); - } - - get config(): ActorConfig { - return this.#config; - } - - // MARK: - State Access - get persist(): PersistedActor { - return this.stateManager.persist; - } - - get state(): S { - return this.stateManager.state; - } - - set state(value: S) { - this.stateManager.state = value; - } - - get stateEnabled(): boolean { - return this.stateManager.stateEnabled; - } - - get connStateEnabled(): boolean { - return "createConnState" in this.#config || "connState" in this.#config; - } - - // MARK: - Variables & Database - get vars(): V { - this.#validateVarsEnabled(); - invariant(this.#vars !== undefined, "vars not enabled"); - return this.#vars; - } - - get db(): InferDatabaseClient { - if (!this.#db) { - if (this.#shutdownComplete && "db" in this.#config) { - throw new errors.ActorStopping( - "database accessed after actor stopped. If you are using setInterval or other background timers, clean them up with c.abortSignal.", - ); - } - throw new errors.DatabaseNotEnabled(); - } - return this.#db; - } - - // MARK: - Initialization - async start( - actorDriver: ActorDriver, - inlineClient: Client>, - actorId: string, - name: string, - key: ActorKey, - region: string, - preload?: PreloadMap, - ) { - const startupStart = performance.now(); - - // Initialize properties - this.driver = actorDriver; - this.#inlineClient = inlineClient; - this.#actorId = actorId; - this.#name = name; - this.#key = key; - this.#actorKeyString = serializeActorKey(this.#key); - this.#region = region; - - // Initialize tracing - this.#initializeTraces(); - - // Initialize logging - this.#initializeLogging(); - - // Initialize managers - this.connectionManager = new ConnectionManager(this); - this.stateManager = new StateManager(this, actorDriver, this.#config); - this.eventManager = new EventManager(this); - this.queueManager = new QueueManager(this, actorDriver); - this.#scheduleManager = new ScheduleManager( - this, - actorDriver, - this.#config, - ); - - // Legacy schedule object (for compatibility) - this.#schedule = new Schedule(this); - - // Enable unexpected KV round-trip detection when preload data was - // provided. - if (preload) { - this.#expectNoKvRoundTrips = true; - } - - // Extract workflow preload data for lazy consumption by workflow engine. - if (preload) { - const workflowEntries = preload.listPrefix(workflowStoragePrefix()); - if (workflowEntries !== undefined) { - this.#workflowPreloadEntries = workflowEntries; - } - } - - // Setup database before lifecycle hooks so c.db is available in - // createState, onCreate, createVars, and onWake. - await this.#setupDatabase(preload); - - // Create a write collector to batch new-actor init writes into a - // single kvBatchPut. - const writeCollector = new WriteCollector(actorDriver, actorId); - - // Load state - await this.#measureStartup("loadStateMs", () => - this.#loadState(preload, writeCollector), - ); - - await this.#measureStartup("initQueueMs", () => - this.queueManager.initialize(preload, writeCollector), - ); - - await this.#measureStartup("initInspectorTokenMs", () => - this.#initializeInspectorToken(preload, writeCollector), - ); - - // Flush any batched writes from new actor initialization. - await this.#measureStartup("flushWritesMs", async () => { - this.#metrics.startup.flushWritesEntries = writeCollector.size; - await writeCollector.flush(); - }); - - // Initialize variables. - await this.#measureStartup( - "createVarsMs", - async () => { - if (this.#varsEnabled) { - await this.#initializeVars(); - } - }, - { pauseKvGuard: true }, - ); - - // Call onStart lifecycle. - await this.#measureStartup("onWakeMs", () => this.#callOnStart(), { - pauseKvGuard: true, - }); - // Initialize alarms - await this.#measureStartup("initAlarmsMs", () => - this.#scheduleManager.initializeAlarms(), - ); - - // Mark as ready - this.#ready = true; - - // Finish up any remaining initiation - // - // Do this after #ready = true since this can call any actor callbacks - // (which require #assertReady) - await this.#measureStartup("onBeforeActorStartMs", async () => { - await this.driver.onBeforeActorStart?.(this); - }); - - // Mark as started - // - // We do this after onBeforeActorStart to prevent the actor from going - // to sleep before finishing setup - this.#started = true; - - // Clear KV round-trip detection after startup completes. - this.#expectNoKvRoundTrips = false; - - // Release workflow preload data after startup completes. - this.#workflowPreloadEntries = undefined; - - // Record total startup time. - this.#metrics.startup.totalMs = performance.now() - startupStart; - this.#rLog.info({ - msg: "actor started", - startupMs: this.#metrics.startup.totalMs, - kvRoundTrips: this.#metrics.startup.kvRoundTrips, - }); - - // Start sleep timer after setting #started since this affects the - // timer - this.resetSleepTimer(); - - // Start run handler in background (does not block startup) - this.#startRunHandler(); - - // Trigger any pending alarms - await this.onAlarm(); - } - - // MARK: - Ready Check - isReady(): boolean { - return this.#ready; - } - - assertReady() { - if (!this.#ready) throw new errors.InternalError("Actor not ready"); - this.assertNotShutdown(); - } - - assertNotShutdown() { - if (this.#shutdownComplete) - throw new errors.ActorStopping("Actor has shut down"); - } - - async cleanupPersistedConnections(reason?: string): Promise { - this.assertReady(); - return await this.connectionManager.cleanupPersistedHibernatableConnections( - reason, - ); - } - - async restartRunHandler(): Promise { - this.assertReady(); - if (this.#stopCalled) - throw errors.actorRestarting({ phase: "stopping" }); - if (this.#runHandlerActive && this.#runPromise) { - await this.#runPromise; - } - if (this.#runHandlerActive) { - return; - } - - this.#startRunHandler(); - } - - isRunHandlerActive(): boolean { - return this.#runHandlerActive; - } - - // MARK: - Stop - async onStop(mode: "sleep" | "destroy") { - if (this.#stopCalled) { - this.#rLog.warn({ msg: "already stopping actor" }); - return; - } - this.#stopCalled = true; - this.#rLog.info({ - msg: "setting stopCalled=true", - mode, - }); - - try { - // Clear sleep timeout - if (this.#sleepTimeout) { - clearTimeout(this.#sleepTimeout); - this.#sleepTimeout = undefined; - } - - // Cancel alarm timeouts so they cannot fire during shutdown. - // Scheduled events are persisted and will be re-initialized - // on wake via initializeAlarms(). - this.driver.cancelAlarm?.(this.#actorId); - this.#abortActorSignal(); - - // The run-handler join, lifecycle hooks, and remaining shutdown - // tasks all share the single sleepGracePeriod budget. - const shutdownTaskDeadlineTs = - Date.now() + this.#getEffectiveSleepGracePeriod(); - - await this.#waitForRunHandler(shutdownTaskDeadlineTs - Date.now()); - - // Call onStop lifecycle - if (mode === "sleep") { - await this.#waitForIdleSleepWindow(shutdownTaskDeadlineTs); - await this.#callOnSleep(shutdownTaskDeadlineTs); - } else if (mode === "destroy") { - await this.#callOnDestroy(shutdownTaskDeadlineTs); - } else { - assertUnreachable(mode); - } - - // Wait for shutdown tasks that were already in flight before - // connection teardown starts. - await this.#waitShutdownTasks(shutdownTaskDeadlineTs); - - // Disconnect non-hibernatable connections - await this.#disconnectConnections(); - - // Drain async WebSocket close handlers and any waitUntil work they - // enqueue before persisting final state. - await this.#waitShutdownTasks(shutdownTaskDeadlineTs); - - // Clear timeouts and save state - this.#rLog.info({ msg: "clearing pending save timeouts" }); - this.stateManager.clearPendingSaveTimeout(); - this.#rLog.info({ msg: "saving state immediately" }); - await this.stateManager.saveState({ - immediate: true, - }); - - // Wait for write queues - await this.stateManager.waitForPendingWrites(); - await this.#scheduleManager.waitForPendingAlarmWrites(); - } finally { - this.#shutdownComplete = true; - await this.#cleanupDatabase(); - } - } - - async debugForceCrash() { - if (this.#shutdownComplete) { - return; - } - if (this.#stopCalled) { - this.#rLog.warn({ - msg: "already stopping actor during hard crash", - }); - return; - } - this.#stopCalled = true; - - try { - if (this.#sleepTimeout) { - clearTimeout(this.#sleepTimeout); - this.#sleepTimeout = undefined; - } - - this.driver.cancelAlarm?.(this.#actorId); - this.#abortActorSignal(); - this.stateManager.clearPendingSaveTimeout(); - } finally { - this.#shutdownComplete = true; - await this.#cleanupDatabase(); - } - } - - // MARK: - Sleep - startSleep() { - if (this.#stopCalled || this.#destroyCalled) { - this.#rLog.debug({ - msg: "cannot call startSleep if actor already stopping", - }); - return; - } - - if (this.#sleepCalled) { - this.#rLog.warn({ - msg: "cannot call startSleep twice, actor already sleeping", - }); - return; - } - this.#sleepCalled = true; - - const sleep = this.driver.startSleep?.bind(this.driver, this.#actorId); - invariant(this.#sleepingSupported, "sleeping not supported"); - invariant(sleep, "no sleep on driver"); - - this.#rLog.info({ msg: "actor sleeping" }); - - // Start sleep on next tick so call site of startSleep can exit - setImmediate(() => { - sleep(); - }); - } - - // MARK: - Destroy - startDestroy() { - if (this.#stopCalled || this.#sleepCalled) { - this.#rLog.debug({ - msg: "cannot call startDestroy if actor already stopping or sleeping", - }); - return; - } - - if (this.#destroyCalled) { - this.#rLog.warn({ - msg: "cannot call startDestroy twice, actor already destroying", - }); - return; - } - this.#destroyCalled = true; - - const destroy = this.driver.startDestroy.bind( - this.driver, - this.#actorId, - ); - - this.#rLog.info({ msg: "actor destroying" }); - - // Start destroy on next tick so call site of startDestroy can exit - setImmediate(() => { - destroy(); - }); - } - - #abortActorSignal() { - if (this.#abortController.signal.aborted) { - return; - } - try { - this.#abortController.abort(); - } catch {} - } - - // MARK: - HTTP Request Tracking - beginHonoHttpRequest() { - this.#activeHonoHttpRequests++; - this.resetSleepTimer(); - } - - endHonoHttpRequest() { - this.#activeHonoHttpRequests--; - if (this.#activeHonoHttpRequests < 0) { - this.#activeHonoHttpRequests = 0; - this.#rLog.warn({ - msg: "active hono requests went below 0, this is a RivetKit bug", - ...EXTRA_ERROR_LOG, - }); - } - this.resetSleepTimer(); - } - - // MARK: - Message Processing - async processMessage( - message: { - body: - | { - tag: "ActionRequest"; - val: { id: bigint; name: string; args: unknown }; - } - | { - tag: "SubscriptionRequest"; - val: { eventName: string; subscribe: boolean }; - }; - }, - conn: Conn, - ) { - // Hibernating WebSocket connections intentionally do not keep the - // actor alive so the actor can sleep while connections are idle. - // Reset the sleep timer on each message so the actor stays awake - // while clients are actively communicating. - this.resetSleepTimer(); - - await processMessage(message, this, conn, { - onExecuteAction: async (ctx, name, args) => { - return await this.executeAction(ctx, name, args); - }, - onSubscribe: async (eventName, conn) => { - this.eventManager.addSubscription(eventName, conn, false); - }, - onUnsubscribe: async (eventName, conn) => { - this.eventManager.removeSubscription(eventName, conn, false); - }, - }); - } - - async assertCanSubscribe( - ctx: ActionContext, - eventName: string, - ): Promise { - const canSubscribe = getEventCanSubscribe( - this.#config.events, - eventName, - ); - if (!canSubscribe) { - return; - } - - const result = await canSubscribe(ctx); - if (typeof result !== "boolean") { - throw new errors.InvalidCanSubscribeResponse(); - } - if (!result) { - throw new errors.Forbidden(); - } - } - - async assertCanPublish( - ctx: ActionContext, - queueName: string, - ): Promise { - const canPublish = getQueueCanPublish< - ActionContext - >(this.#config.queues, queueName); - if (!canPublish) { - return; - } - - const result = await canPublish(ctx); - if (typeof result !== "boolean") { - throw new errors.InvalidCanPublishResponse(); - } - if (!result) { - throw new errors.Forbidden(); - } - } - - // MARK: - Action Execution - async invokeActionByName( - ctx: ActorContext, - actionName: string, - args: unknown[], - timeoutMs?: number, - ): Promise { - this.assertReady(); - - const actions = this.#config.actions ?? {}; - if (!(actionName in actions)) { - this.#rLog.warn({ msg: "action does not exist", actionName }); - throw new errors.ActionNotFound(actionName); - } - - const actionFunction = actions[actionName]; - if (typeof actionFunction !== "function") { - this.#rLog.warn({ - msg: "action is not a function", - actionName, - type: typeof actionFunction, - }); - throw new errors.ActionNotFound(actionName); - } - - const outputOrPromise = actionFunction.call( - undefined, - // TODO: Replace this cast after scheduled actions and direct actions - // share a properly typed internal action invocation context. - ctx as any, - ...args, - ); - const maybeThenable = outputOrPromise as { - then?: (onfulfilled?: unknown, onrejected?: unknown) => unknown; - }; - if (maybeThenable && typeof maybeThenable.then === "function") { - const promise = Promise.resolve(outputOrPromise); - return await (timeoutMs === undefined - ? promise - : deadline(promise, timeoutMs)); - } - - return outputOrPromise; - } - - async executeAction( - ctx: ActionContext, - actionName: string, - args: unknown[], - ): Promise { - this.assertReady(); - - this.#beginActiveAsyncRegion("internalKeepAwake"); - this.#metrics.actionCalls++; - const actionStart = performance.now(); - const actionSpan = this.startTraceSpan(`actor.action.${actionName}`, { - "rivet.action.name": actionName, - }); - let spanEnded = false; - - try { - const output = await this.#traces.withSpan(actionSpan, async () => { - this.#rLog.debug({ - msg: "executing action", - actionName, - args, - }); - - let output = await this.invokeActionByName( - ctx, - actionName, - args, - this.#config.options.actionTimeout, - ); - - // Process through onBeforeActionResponse if configured - if (this.#config.onBeforeActionResponse) { - try { - output = await this.runInTraceSpan( - "actor.onBeforeActionResponse", - { "rivet.action.name": actionName }, - () => - this.#config.onBeforeActionResponse?.( - this.actorContext, - actionName, - args, - output, - ), - ); - } catch (error) { - this.#rLog.error({ - msg: "error in `onBeforeActionResponse`", - error: stringifyError(error), - }); - } - } - - return output; - }); - - return output; - } catch (error) { - this.#metrics.actionErrors++; - const isTimeout = error instanceof DeadlineError; - const message = isTimeout - ? "ActionTimedOut" - : stringifyError(error); - this.#traces.setAttributes(actionSpan, { - "error.message": message, - "error.type": - error instanceof Error ? error.name : typeof error, - }); - this.#traces.endSpan(actionSpan, { - status: { code: "ERROR", message }, - }); - spanEnded = true; - if (isTimeout) { - throw new errors.ActionTimedOut(); - } - this.#rLog.error({ - msg: "action error", - actionName, - error: stringifyError(error), - }); - throw error; - } finally { - this.#metrics.actionTotalMs += performance.now() - actionStart; - if (!spanEnded && actionSpan.isActive()) { - this.#traces.endSpan(actionSpan, { - status: { code: "OK" }, - }); - } - this.#endActiveAsyncRegion("internalKeepAwake"); - this.stateManager.savePersistThrottled(); - } - } - - // MARK: - HTTP/WebSocket Handlers - // - // handleRawRequest intentionally has no isStopping guard (unlike - // handleRawWebSocket). In-flight HTTP requests from pre-existing - // connections are allowed during the graceful shutdown window. - // New external requests cannot reach a stopping actor because the - // driver layer blocks them. - async handleRawRequest( - conn: Conn, - request: Request, - ): Promise { - this.assertReady(); - - if (!this.#config.onRequest) { - throw new errors.RequestHandlerNotDefined(); - } - const onRequest = this.#config.onRequest; - - return await this.runInTraceSpan( - "actor.onRequest", - { - "http.method": request.method, - "http.url": request.url, - "rivet.conn.id": conn.id, - }, - async () => { - const ctx = new RequestContext(this, conn, request); - try { - const response = await onRequest(ctx, request); - if (!response) { - throw new errors.InvalidRequestHandlerResponse(); - } - return response; - } catch (error) { - this.#rLog.error({ - msg: "onRequest error", - error: stringifyError(error), - }); - throw error; - } finally { - this.stateManager.savePersistThrottled(); - } - }, - ); - } - - handleRawWebSocket( - conn: Conn, - websocket: UniversalWebSocket, - request?: Request, - ) { - // NOTE: All code before `onWebSocket` must be synchronous in order to ensure the order of `open` events happen in the correct order. - - this.assertReady(); - if (this.#stopCalled) - throw new errors.InternalError("Actor is stopping"); - - if (!this.#config.onWebSocket) { - throw new errors.InternalError("onWebSocket handler not defined"); - } - - const span = this.startTraceSpan("actor.onWebSocket", { - "http.url": request?.url, - "rivet.conn.id": conn.id, - }); - let spanEnded = false; - - try { - // Reset sleep timer when handling WebSocket - this.resetSleepTimer(); - - // Handle WebSocket - const ctx = new WebSocketContext(this, conn, request); - const trackedWebSocket = this.#createTrackedWebSocket(websocket); - - // NOTE: This is async and will run in the background - const voidOrPromise = this.#traces.withSpan(span, () => - this.#config.onWebSocket?.(ctx, trackedWebSocket), - ); - - // Save changes from the WebSocket open - if (voidOrPromise instanceof Promise) { - voidOrPromise - .then(() => { - if (!spanEnded) { - this.endTraceSpan(span, { code: "OK" }); - spanEnded = true; - } - }) - .catch((error) => { - if (!spanEnded) { - this.endTraceSpan(span, { - code: "ERROR", - message: stringifyError(error), - }); - spanEnded = true; - } - this.#rLog.error({ - msg: "onWebSocket error", - error: stringifyError(error), - }); - }) - .finally(() => { - this.stateManager.savePersistThrottled(); - }); - } else { - if (!spanEnded) { - this.endTraceSpan(span, { code: "OK" }); - spanEnded = true; - } - this.stateManager.savePersistThrottled(); - } - } catch (error) { - if (!spanEnded) { - this.endTraceSpan(span, { - code: "ERROR", - message: stringifyError(error), - }); - spanEnded = true; - } - this.#rLog.error({ - msg: "onWebSocket error", - error: stringifyError(error), - }); - throw error; - } - } - - // MARK: - Scheduling - async scheduleEvent( - timestamp: number, - action: string, - args: unknown[], - ): Promise { - await this.#scheduleManager.scheduleEvent(timestamp, action, args); - } - - async onAlarm() { - if (this.#stopCalled) return; - this.resetSleepTimer(); - await this.#scheduleManager.onAlarm(); - } - - // MARK: - Background Tasks - waitUntil(promise: Promise) { - this.assertNotShutdown(); - - const nonfailablePromise = promise - .then(() => { - this.#rLog.debug({ msg: "wait until promise complete" }); - }) - .catch((error) => { - this.#rLog.error({ - msg: "wait until promise failed", - error: stringifyError(error), - }); - }); - this.#backgroundPromises.push(nonfailablePromise); - } - - #getEffectiveSleepGracePeriod(): number { - if (this.overrides.sleepGracePeriod !== undefined) { - return Math.min( - this.#config.options.sleepGracePeriod, - this.overrides.sleepGracePeriod, - ); - } - return this.#config.options.sleepGracePeriod; - } - - #beginActiveAsyncRegion(region: keyof ActiveAsyncRegionCounts) { - this.#activeAsyncRegionCounts[region]++; - this.resetSleepTimer(); - } - - #endActiveAsyncRegion(region: keyof ActiveAsyncRegionCounts) { - this.#activeAsyncRegionCounts[region]--; - if (this.#activeAsyncRegionCounts[region] < 0) { - this.#activeAsyncRegionCounts[region] = 0; - this.#rLog.warn({ - msg: ACTIVE_ASYNC_REGION_ERROR_MESSAGES[region], - ...EXTRA_ERROR_LOG, - }); - } - - this.resetSleepTimer(); - } - - #trackWebSocketCallback(eventType: string, promise: Promise) { - this.#beginActiveAsyncRegion("websocketCallbacks"); - - const trackedPromise = promise - .then(() => { - this.#rLog.debug({ - msg: "websocket callback complete", - eventType, - }); - }) - .catch((error) => { - this.#rLog.error({ - msg: "websocket callback failed", - eventType, - error: stringifyError(error), - }); - }) - .finally(() => { - this.#endActiveAsyncRegion("websocketCallbacks"); - }); - - this.#websocketCallbackPromises.push(trackedPromise); - } - - /** - * Prevents the actor from sleeping while the given promise is running. - * - * Use this when performing async operations in the `run` handler or other - * background contexts where you need to ensure the actor stays awake. - * - * Returns the resolved value and resets the sleep timer on completion. - * Errors are propagated to the caller. - * - * @deprecated Use `setPreventSleep(true)` while work is active, or move - * shutdown and flush work to `onSleep` if it can wait until the actor is - * sleeping. - */ - async keepAwake(promise: Promise): Promise { - this.assertNotShutdown(); - - this.#beginActiveAsyncRegion("keepAwake"); - - try { - return await promise; - } finally { - this.#endActiveAsyncRegion("keepAwake"); - } - } - - /** - * Internal sleep blocker used by runtime subsystems. - * - * Accepts either a promise or a thunk. The thunk form exists so the actor - * can enter the sleep-blocking region before user code starts running. This - * avoids a race where work begins, but the actor is not yet marked active, - * which can allow the sleep timer to fire underneath that work. - */ - internalKeepAwake(promise: Promise): Promise; - internalKeepAwake(run: () => T | Promise): Promise; - async internalKeepAwake( - promiseOrRun: Promise | (() => T | Promise), - ): Promise { - this.assertNotShutdown(); - - this.#beginActiveAsyncRegion("internalKeepAwake"); - - try { - if (typeof promiseOrRun === "function") { - return await promiseOrRun(); - } - return await promiseOrRun; - } finally { - this.#endActiveAsyncRegion("internalKeepAwake"); - } - } - - setPreventSleep(prevent: boolean) { - if (this.#preventSleep === prevent) return; - - this.#preventSleep = prevent; - if (!prevent) { - this.#preventSleepClearedPromise?.resolve(); - this.#preventSleepClearedPromise = undefined; - } - this.#rLog.debug({ - msg: "updated prevent sleep state", - prevent, - }); - this.resetSleepTimer(); - } - - beginQueueWait() { - this.assertReady(); - this.#activeQueueWaitCount++; - this.resetSleepTimer(); - } - - endQueueWait() { - this.#activeQueueWaitCount--; - if (this.#activeQueueWaitCount < 0) { - this.#activeQueueWaitCount = 0; - this.#rLog.warn({ - msg: "active queue wait count went below 0, this is a RivetKit bug", - ...EXTRA_ERROR_LOG, - }); - } - this.resetSleepTimer(); - } - - // MARK: - Private Helper Methods - #initializeTraces() { - if (getRivetExperimentalOtel()) { - // Experimental mode persists trace data to actor storage so inspector - // queries can return OTel payloads. - this.#traces = createTraces({ - driver: new ActorTracesDriver(this.driver, this.#actorId), - }); - } else { - // Keep the tracing API calls active while disabling trace persistence - // until the experimental flag is enabled. - this.#traces = createNoopTraces(); - } - } - - #traceAttributes( - attributes?: Record, - ): Record { - return { - "rivet.actor.id": this.#actorId, - "rivet.actor.name": this.#name, - "rivet.actor.key": this.#actorKeyString, - "rivet.actor.region": this.#region, - ...(attributes ?? {}), - }; - } - - #patchLoggerForTraces(logger: Logger) { - const levels: Array< - "trace" | "debug" | "info" | "warn" | "error" | "fatal" - > = ["trace", "debug", "info", "warn", "error", "fatal"]; - for (const level of levels) { - const original = logger[level].bind(logger) as ( - ...args: any[] - ) => unknown; - logger[level] = ((...args: unknown[]) => { - this.#emitLogEvent(level, args); - return original(...(args as any[])); - }) as Logger[typeof level]; - } - } - - #emitLogEvent(level: string, args: unknown[]) { - const span = this.#traces.getCurrentSpan(); - if (!span || !span.isActive()) { - return; - } - - let message: string | undefined; - if (args.length >= 2) { - message = String(args[1]); - } else if (args.length === 1) { - const [value] = args; - if (typeof value === "string") { - message = value; - } else if ( - typeof value === "number" || - typeof value === "boolean" - ) { - message = String(value); - } else if (value && typeof value === "object") { - const maybeMsg = (value as { msg?: unknown }).msg; - if (maybeMsg !== undefined) { - message = String(maybeMsg); - } - } - } - - this.#traces.emitEvent(span, "log", { - attributes: this.#traceAttributes({ - "log.level": level, - ...(message ? { "log.message": message } : {}), - }), - timeUnixMs: Date.now(), - }); - } - - #initializeLogging() { - const logParams = { - actor: this.#name, - key: this.#actorKeyString, - actorId: this.#actorId, - }; - - const extraLogParams = this.driver.getExtraActorLogParams?.(); - if (extraLogParams) Object.assign(logParams, extraLogParams); - - this.#log = getBaseLogger().child( - Object.assign( - getIncludeTarget() ? { target: "actor" } : {}, - logParams, - ), - ); - this.#rLog = getBaseLogger().child( - Object.assign( - getIncludeTarget() ? { target: "actor-runtime" } : {}, - logParams, - ), - ); - - this.#patchLoggerForTraces(this.#log); - this.#patchLoggerForTraces(this.#rLog); - } - - async #loadState(preload?: PreloadMap, writeCollector?: WriteCollector) { - let persistDataBuffer: Uint8Array | null; - const preloaded = preload?.get(KEYS.PERSIST_DATA); - if (preloaded) { - persistDataBuffer = preloaded.value; - } else { - this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvBatchGet"); - this.#metrics.startup.kvRoundTrips++; - const [buf] = await this.driver.kvBatchGet(this.#actorId, [ - KEYS.PERSIST_DATA, - ]); - persistDataBuffer = buf; - } - - invariant( - persistDataBuffer !== null, - "persist data has not been set, it should be set when initialized", - ); - - const bareData = - ACTOR_VERSIONED.deserializeWithEmbeddedVersion(persistDataBuffer); - const persistData = convertActorFromBarePersisted(bareData); - - if (persistData.hasInitialized) { - await this.#measureStartup("restoreConnectionsMs", () => - this.#restoreExistingActor(persistData, preload), - ); - } else { - this.#metrics.startup.isNew = true; - await this.#createNewActor(persistData, writeCollector); - } - - // Pass persist reference to schedule manager - this.#scheduleManager.setPersist(this.stateManager.persist); - } - - async #createNewActor( - persistData: PersistedActor, - writeCollector?: WriteCollector, - ) { - this.#rLog.info({ msg: "actor creating" }); - - // Initialize state - await this.#measureStartup("createStateMs", () => - this.stateManager.initializeState(persistData, writeCollector), - ); - - // Call onCreate lifecycle - if (this.#config.onCreate) { - const onCreate = this.#config.onCreate; - await this.#measureStartup( - "onCreateMs", - () => - this.runInTraceSpan("actor.onCreate", undefined, () => - onCreate(this.actorContext as any, persistData.input!), - ), - { pauseKvGuard: true }, - ); - } - } - - async #restoreExistingActor( - persistData: PersistedActor, - preload?: PreloadMap, - ) { - let connEntries: [Uint8Array, Uint8Array][]; - const preloadedConns = preload?.listPrefix(KEYS.CONN_PREFIX); - if (preloadedConns !== undefined) { - connEntries = preloadedConns; - } else { - this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvListPrefix"); - this.#metrics.startup.kvRoundTrips++; - connEntries = await this.driver.kvListPrefix( - this.#actorId, - KEYS.CONN_PREFIX, - ); - } - - // Decode connections - const connections: PersistedConn[] = []; - for (const [_key, value] of connEntries) { - try { - const bareData = CONN_VERSIONED.deserializeWithEmbeddedVersion( - new Uint8Array(value), - ); - const conn = convertConnFromBarePersistedConn(bareData); - connections.push(conn); - } catch (error) { - this.#rLog.error({ - msg: "failed to decode connection", - error: stringifyError(error), - }); - } - } - - this.#metrics.startup.restoreConnectionsCount = connections.length; - this.#rLog.info({ - msg: "actor restoring", - connections: connections.length, - }); - - // Initialize state - this.stateManager.initPersistProxy(persistData); - - // Restore connections - this.connectionManager.restoreConnections(connections); - } - - async #initializeInspectorToken( - preload?: PreloadMap, - writeCollector?: WriteCollector, - ) { - let tokenBuffer: Uint8Array | null; - const preloaded = preload?.get(KEYS.INSPECTOR_TOKEN); - if (preloaded) { - tokenBuffer = preloaded.value; - } else { - this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvBatchGet"); - this.#metrics.startup.kvRoundTrips++; - const [buf] = await this.driver.kvBatchGet(this.#actorId, [ - KEYS.INSPECTOR_TOKEN, - ]); - tokenBuffer = buf; - } - - if (tokenBuffer !== null) { - const decoder = new TextDecoder(); - this.#inspectorToken = decoder.decode(tokenBuffer); - this.#rLog.debug({ msg: "loaded existing inspector token" }); - } else { - this.#inspectorToken = generateSecureToken(); - const tokenBytes = new TextEncoder().encode(this.#inspectorToken); - if (writeCollector) { - writeCollector.add(KEYS.INSPECTOR_TOKEN, tokenBytes); - } else { - this.#metrics.startup.kvRoundTrips++; - await this.driver.kvBatchPut(this.#actorId, [ - [KEYS.INSPECTOR_TOKEN, tokenBytes], - ]); - } - this.#rLog.debug({ msg: "generated new inspector token" }); - } - } - - async #initializeVars() { - let vars: V | undefined; - if ("createVars" in this.#config) { - const createVars = this.#config.createVars; - vars = await this.runInTraceSpan( - "actor.createVars", - undefined, - () => { - const dataOrPromise = createVars?.( - this.actorContext as any, - this.driver.getContext(this.#actorId), - ); - if (dataOrPromise instanceof Promise) { - return deadline( - dataOrPromise, - this.#config.options.createVarsTimeout, - ); - } - return dataOrPromise; - }, - ); - } else if ("vars" in this.#config) { - vars = structuredClone(this.#config.vars); - } else { - throw new Error( - "Could not create variables from 'createVars' or 'vars'", - ); - } - this.#vars = vars; - } - - async #callOnStart() { - this.#rLog.info({ msg: "actor starting" }); - if (this.#config.onWake) { - const onWake = this.#config.onWake; - await this.runInTraceSpan("actor.onWake", undefined, () => - onWake(this.actorContext), - ); - } - } - - async #callOnSleep(deadlineTs: number) { - if (this.#config.onSleep) { - const onSleep = this.#config.onSleep; - try { - this.#rLog.debug({ msg: "calling onSleep" }); - await this.runInTraceSpan( - "actor.onSleep", - undefined, - async () => { - const result = onSleep(this.actorContext); - if (result instanceof Promise) { - const remaining = deadlineTs - Date.now(); - if (remaining <= 0) { - throw new DeadlineError(); - } - await deadline(result, remaining); - } - }, - ); - this.#rLog.debug({ msg: "onSleep completed" }); - } catch (error) { - if (error instanceof DeadlineError) { - this.#rLog.error({ msg: "onSleep timed out" }); - } else { - this.#rLog.error({ - msg: "error in onSleep", - error: stringifyError(error), - }); - } - } - } - } - - async #callOnDestroy(deadlineTs: number) { - if (this.#config.onDestroy) { - const onDestroy = this.#config.onDestroy; - try { - this.#rLog.debug({ msg: "calling onDestroy" }); - await this.runInTraceSpan( - "actor.onDestroy", - undefined, - async () => { - const result = onDestroy(this.actorContext); - if (result instanceof Promise) { - const remaining = deadlineTs - Date.now(); - if (remaining <= 0) { - throw new DeadlineError(); - } - await deadline(result, remaining); - } - }, - ); - this.#rLog.debug({ msg: "onDestroy completed" }); - } catch (error) { - if (error instanceof DeadlineError) { - this.#rLog.error({ msg: "onDestroy timed out" }); - } else { - this.#rLog.error({ - msg: "error in onDestroy", - error: stringifyError(error), - }); - } - } - } - } - - #startRunHandler() { - const runFn = getRunFunction(this.#config.run); - if (!runFn) return; - - this.#rLog.debug({ msg: "starting run handler" }); - this.#runHandlerActive = true; - this.resetSleepTimer(); - - const runSpan = this.startTraceSpan("actor.run"); - const runResult = this.#traces.withSpan(runSpan, () => - runFn(this.actorContext), - ); - - // Do not destroy or immediately sleep the actor when run exits. Finished - // workflows must stay inspectable when something goes wrong, and callers - // may still need to invoke actions after the run handler has completed. - if (runResult instanceof Promise) { - this.#runPromise = runResult - .then(() => { - if (this.#stopCalled) { - if (runSpan.isActive()) { - this.endTraceSpan(runSpan, { code: "OK" }); - } - this.#rLog.debug({ - msg: "run handler exited during actor stop", - }); - return; - } - - if (runSpan.isActive()) { - this.endTraceSpan(runSpan, { code: "OK" }); - } - this.#rLog.info({ - msg: "run handler exited", - }); - }) - .catch((error) => { - if (this.#stopCalled) { - if (runSpan.isActive()) { - this.endTraceSpan(runSpan, { code: "OK" }); - } - this.#rLog.debug({ - msg: "run handler threw during actor stop", - error: stringifyError(error), - }); - return; - } - - this.endTraceSpan(runSpan, { - code: "ERROR", - message: stringifyError(error), - }); - this.#rLog.error({ - msg: "run handler threw error", - error: stringifyError(error), - }); - }) - .finally(() => { - this.#runHandlerActive = false; - this.resetSleepTimer(); - }); - } else if (runSpan.isActive()) { - this.endTraceSpan(runSpan, { code: "OK" }); - this.#rLog.info({ - msg: "run handler exited", - }); - this.#runHandlerActive = false; - this.resetSleepTimer(); - } - } - - async #waitForRunHandler(timeoutMs: number) { - if (!this.#runPromise) { - return; - } - - this.#rLog.debug({ msg: "waiting for run handler to complete" }); - - if (timeoutMs <= 0) { - this.#rLog.warn({ - msg: "run handler did not complete in time, it may have leaked - ensure long-running work settles before shutdown finalizes", - timeoutMs, - }); - return; - } - - const timedOut = await Promise.race([ - this.#runPromise.then(() => false).catch(() => false), - new Promise((resolve) => - setTimeout(() => resolve(true), timeoutMs), - ), - ]); - - if (timedOut) { - this.#rLog.warn({ - msg: "run handler did not complete in time, it may have leaked - ensure long-running work settles before shutdown finalizes", - timeoutMs, - }); - } else { - this.#rLog.debug({ msg: "run handler completed" }); - } - } - - async #setupDatabase(_preload?: PreloadMap) { - if (!("db" in this.#config) || !this.#config.db) { - return; - } - - const dbProvider = this.#config.db; - - let client: InferDatabaseClient | undefined; - try { - client = await this.#measureStartup("setupDatabaseClientMs", () => - dbProvider.createClient({ - actorId: this.#actorId, - overrideRawDatabaseClient: this.driver - .overrideRawDatabaseClient - ? () => - this.driver.overrideRawDatabaseClient?.( - this.#actorId, - ) - : undefined, - overrideDrizzleDatabaseClient: this.driver - .overrideDrizzleDatabaseClient - ? () => - this.driver.overrideDrizzleDatabaseClient?.( - this.#actorId, - ) - : undefined, - kv: { - batchPut: (entries: [Uint8Array, Uint8Array][]) => - this.driver.kvBatchPut(this.#actorId, entries), - batchGet: (keys: Uint8Array[]) => - this.driver.kvBatchGet(this.#actorId, keys), - batchDelete: (keys: Uint8Array[]) => - this.driver.kvBatchDelete(this.#actorId, keys), - deleteRange: (start: Uint8Array, end: Uint8Array) => - this.driver.kvDeleteRange( - this.#actorId, - start, - end, - ), - }, - metrics: this.#metrics, - log: this.#rLog, - nativeDatabaseProvider: - this.driver.getNativeDatabaseProvider?.(), - }), - ); - this.#rLog.info({ msg: "database migration starting" }); - await this.#measureStartup("dbMigrateMs", async () => { - await dbProvider.onMigrate?.(client!); - }); - this.#rLog.info({ msg: "database migration complete" }); - this.#db = client; - } catch (error) { - if (client) { - try { - await client.close(); - } catch (cleanupError) { - this.#rLog.error({ - msg: "database setup cleanup failed", - error: stringifyError(cleanupError), - }); - } - } - if (error instanceof Error) { - this.#rLog.error({ - msg: "database setup failed", - error: stringifyError(error), - }); - throw error; - } - const wrappedError = new Error( - `Database setup failed: ${String(error)}`, - ); - this.#rLog.error({ - msg: "database setup failed with non-Error object", - error: String(error), - errorType: typeof error, - }); - throw wrappedError; - } - } - - async #cleanupDatabase() { - const client = this.#db; - const dbConfig = "db" in this.#config ? this.#config.db : undefined; - this.#db = undefined; - - if (client && dbConfig) { - try { - await client.close(); - } catch (error) { - this.#rLog.error({ - msg: "database cleanup failed", - error: stringifyError(error), - }); - } - } - } - - async #disconnectConnections() { - const promises: Promise[] = []; - this.#rLog.debug({ - msg: "disconnecting connections on actor stop", - totalConns: this.connectionManager.connections.size, - }); - for (const connection of this.connectionManager.connections.values()) { - this.#rLog.debug({ - msg: "checking connection for disconnect", - connId: connection.id, - isHibernatable: connection.isHibernatable, - }); - if (!connection.isHibernatable) { - this.#rLog.debug({ - msg: "disconnecting non-hibernatable connection on actor stop", - connId: connection.id, - }); - promises.push(connection.disconnect()); - } else { - this.#rLog.debug({ - msg: "preserving hibernatable connection on actor stop", - connId: connection.id, - }); - } - } - - // Wait with timeout - let timeoutHandle: ReturnType | undefined; - const res = await Promise.race([ - Promise.all(promises).then(() => { - if (timeoutHandle !== undefined) clearTimeout(timeoutHandle); - return false; - }), - new Promise((res) => { - timeoutHandle = globalThis.setTimeout(() => res(true), 1500); - }), - ]); - - if (res) { - this.#rLog.warn({ - msg: "timed out waiting for connections to close, shutting down anyway", - }); - } - } - - /** - * Drain shutdown blockers within the shared shutdown deadline. - * - * This method is intentionally called multiple times during shutdown so - * work created by earlier shutdown phases, such as async WebSocket close - * handlers or waitUntil calls they enqueue, is also drained before final - * persistence. - */ - async #waitShutdownTasks(deadlineTs: number) { - while ( - this.#backgroundPromises.length > 0 || - this.#websocketCallbackPromises.length > 0 || - this.#preventSleep - ) { - await this.#drainPromiseQueue( - this.#backgroundPromises, - "background tasks", - deadlineTs, - ); - await this.#drainPromiseQueue( - this.#websocketCallbackPromises, - "websocket callbacks", - deadlineTs, - ); - await this.#waitForPreventSleepClear(deadlineTs); - - if (deadlineTs - Date.now() <= 0) { - break; - } - } - } - - #sleepWindowBlocker(): - | "activeHonoHttpRequests" - | "keepAwake" - | "internalKeepAwake" - | "pendingDisconnectCallbacks" - | null { - if (this.#activeHonoHttpRequests > 0) { - return "activeHonoHttpRequests"; - } - if (this.#activeAsyncRegionCounts.keepAwake > 0) { - return "keepAwake"; - } - if (this.#activeAsyncRegionCounts.internalKeepAwake > 0) { - return "internalKeepAwake"; - } - if (this.connectionManager.pendingDisconnectCount > 0) { - return "pendingDisconnectCallbacks"; - } - return null; - } - - async #waitForIdleSleepWindow(deadlineTs: number) { - while (true) { - const blocker = this.#sleepWindowBlocker(); - if (!blocker) { - return; - } - - const remaining = deadlineTs - Date.now(); - if (remaining <= 0) { - this.#rLog.warn({ - msg: "timed out waiting for actor to become idle before onSleep", - blocker, - }); - return; - } - - await new Promise((resolve) => - setTimeout(resolve, Math.min(25, remaining)), - ); - } - } - - async #drainPromiseQueue( - promises: Promise[], - label: string, - deadlineTs: number, - ) { - // Drain in a loop so that work scheduled from earlier callbacks is also - // awaited within the same deadline. - while (promises.length > 0) { - const remaining = deadlineTs - Date.now(); - if (remaining <= 0) { - this.#rLog.error({ - msg: `timed out waiting for ${label}`, - count: promises.length, - }); - break; - } - - const batch = promises.length; - - let timeoutHandle: ReturnType | undefined; - const timedOut = await Promise.race([ - Promise.allSettled(promises.slice(0, batch)).then(() => { - if (timeoutHandle !== undefined) - clearTimeout(timeoutHandle); - return false; - }), - new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve(true), remaining); - }), - ]); - - if (timedOut) { - this.#rLog.error({ - msg: `timed out waiting for ${label}`, - count: promises.length, - }); - break; - } - - promises.splice(0, batch); - } - - if (promises.length === 0) { - this.#rLog.debug({ msg: `${label} finished` }); - } - } - - async #waitForPreventSleepClear(deadlineTs: number) { - while (this.#preventSleep) { - const remaining = deadlineTs - Date.now(); - if (remaining <= 0) { - this.#rLog.error({ - msg: "timed out waiting for preventSleep to clear during shutdown", - }); - break; - } - - if (!this.#preventSleepClearedPromise) { - this.#preventSleepClearedPromise = promiseWithResolvers( - (reason: unknown) => - this.#rLog.warn({ - msg: "preventSleep clear waiter rejected unexpectedly", - reason: stringifyError(reason), - ...EXTRA_ERROR_LOG, - }), - ); - } - - let timeoutHandle: ReturnType | undefined; - const timedOut = await Promise.race([ - this.#preventSleepClearedPromise.promise.then(() => { - if (timeoutHandle !== undefined) - clearTimeout(timeoutHandle); - return false; - }), - new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve(true), remaining); - }), - ]); - - if (timedOut) { - this.#rLog.error({ - msg: "timed out waiting for preventSleep to clear during shutdown", - }); - break; - } - } - } - - #createTrackedWebSocket(websocket: UniversalWebSocket): TrackedWebSocket { - return new TrackedWebSocket(websocket, { - onPromise: (eventType, promise) => { - this.#trackWebSocketCallback(eventType, promise); - }, - onError: (eventType, error) => { - this.#rLog.error({ - msg: "error in websocket event handler", - eventType, - error: stringifyError(error), - }); - }, - }); - } - - resetSleepTimer() { - if (this.#config.options.noSleep || !this.#sleepingSupported) return; - if (this.#stopCalled) return; - - const canSleep = this.#canSleep(); - let timeoutMs: number | undefined; - - if (canSleep === CanSleep.Yes) { - timeoutMs = this.#config.options.sleepTimeout; - } - - this.#rLog.debug({ - msg: "resetting sleep timer", - canSleep: CanSleep[canSleep], - existingTimeout: !!this.#sleepTimeout, - timeout: timeoutMs, - }); - - if (this.#sleepTimeout) { - clearTimeout(this.#sleepTimeout); - this.#sleepTimeout = undefined; - } - - if (this.#sleepCalled) return; - - if (timeoutMs !== undefined) { - this.#sleepTimeout = setTimeout(() => { - if (this.#canSleep() !== CanSleep.Yes) { - this.resetSleepTimer(); - return; - } - this.startSleep(); - }, timeoutMs); - } - } - - #canSleep(): CanSleep { - if (!this.#ready) return CanSleep.NotReady; - if (!this.#started) return CanSleep.NotReady; - if (this.#preventSleep) return CanSleep.PreventSleep; - if (this.#activeHonoHttpRequests > 0) - return CanSleep.ActiveHonoHttpRequests; - if (this.#activeAsyncRegionCounts.keepAwake > 0) { - return CanSleep.ActiveKeepAwake; - } - if (this.#activeAsyncRegionCounts.internalKeepAwake > 0) { - return CanSleep.ActiveInternalKeepAwake; - } - if (this.#runHandlerActive && this.#activeQueueWaitCount === 0) { - return CanSleep.ActiveRun; - } - - for (const _conn of this.connectionManager.connections.values()) { - if (!_conn.isHibernatable) { - return CanSleep.ActiveConns; - } - } - - if (this.connectionManager.pendingDisconnectCount > 0) { - return CanSleep.ActiveDisconnectCallbacks; - } - - if (this.#activeAsyncRegionCounts.websocketCallbacks > 0) { - return CanSleep.ActiveWebSocketCallbacks; - } - - return CanSleep.Yes; - } - - get #sleepingSupported(): boolean { - return this.driver.startSleep !== undefined; - } - - get #varsEnabled(): boolean { - return "createVars" in this.#config || "vars" in this.#config; - } - - #validateVarsEnabled() { - if (!this.#varsEnabled) { - throw new errors.VarsNotEnabled(); - } - } -} diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/keys.ts b/rivetkit-typescript/packages/rivetkit/src/actor/keys.ts index 3c7ad0e03b..3c5282a152 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/keys.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/keys.ts @@ -13,7 +13,7 @@ export const KEYS = { TRACES_PREFIX: Uint8Array.from([7]), }; -export const STORAGE_VERSION = { +const STORAGE_VERSION = { QUEUE: 1, WORKFLOW: 1, TRACES: 1, @@ -30,8 +30,6 @@ const QUEUE_NAMESPACE = { MESSAGES: Uint8Array.from([2]), } as const; -const QUEUE_ID_BYTES = 8; - function concatPrefix(prefix: Uint8Array, suffix: Uint8Array): Uint8Array { const merged = new Uint8Array(prefix.length + suffix.length); merged.set(prefix, 0); @@ -55,10 +53,6 @@ const WORKFLOW_STORAGE_PREFIX = concatPrefix( KEYS.WORKFLOW_PREFIX, STORAGE_VERSION_BYTES.WORKFLOW, ); -const TRACES_STORAGE_PREFIX = concatPrefix( - KEYS.TRACES_PREFIX, - STORAGE_VERSION_BYTES.TRACES, -); export function serializeActorKey(key: ActorKey): string { // Use a special marker for empty key arrays @@ -160,22 +154,10 @@ export function makeWorkflowKey(key: Uint8Array): Uint8Array { return concatPrefix(WORKFLOW_STORAGE_PREFIX, key); } -export function makeTracesKey(key: Uint8Array): Uint8Array { - return concatPrefix(TRACES_STORAGE_PREFIX, key); -} - export function workflowStoragePrefix(): Uint8Array { return Uint8Array.from(WORKFLOW_STORAGE_PREFIX); } -export function tracesStoragePrefix(): Uint8Array { - return Uint8Array.from(TRACES_STORAGE_PREFIX); -} - -export function queueStoragePrefix(): Uint8Array { - return Uint8Array.from(QUEUE_STORAGE_PREFIX); -} - export function queueMetadataKey(): Uint8Array { return Uint8Array.from(QUEUE_METADATA_KEY); } @@ -183,38 +165,3 @@ export function queueMetadataKey(): Uint8Array { export function queueMessagesPrefix(): Uint8Array { return Uint8Array.from(QUEUE_MESSAGES_PREFIX); } - -export function makeConnKey(connId: string): Uint8Array { - const encoder = new TextEncoder(); - const connIdBytes = encoder.encode(connId); - const key = new Uint8Array(KEYS.CONN_PREFIX.length + connIdBytes.length); - key.set(KEYS.CONN_PREFIX, 0); - key.set(connIdBytes, KEYS.CONN_PREFIX.length); - return key; -} - -export function makeQueueMessageKey(id: bigint): Uint8Array { - const key = new Uint8Array(QUEUE_MESSAGES_PREFIX.length + QUEUE_ID_BYTES); - key.set(QUEUE_MESSAGES_PREFIX, 0); - const view = new DataView(key.buffer, key.byteOffset, key.byteLength); - view.setBigUint64(QUEUE_MESSAGES_PREFIX.length, id, false); - return key; -} - -export function decodeQueueMessageKey(key: Uint8Array): bigint { - const offset = QUEUE_MESSAGES_PREFIX.length; - if (key.length < offset + QUEUE_ID_BYTES) { - throw new Error("Queue key is too short"); - } - for (let i = 0; i < QUEUE_MESSAGES_PREFIX.length; i++) { - if (key[i] !== QUEUE_MESSAGES_PREFIX[i]) { - throw new Error("Queue key has invalid prefix"); - } - } - const view = new DataView( - key.buffer, - key.byteOffset + offset, - QUEUE_ID_BYTES, - ); - return view.getBigUint64(0, false); -} diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/schema.ts b/rivetkit-typescript/packages/rivetkit/src/actor/schema.ts index 63990b15a1..5d7645e070 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/schema.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/schema.ts @@ -229,31 +229,6 @@ function isPromiseLike(value: unknown): value is PromiseLike { ); } -export async function validateSchema( - schemas: T | undefined, - key: keyof T & string, - data: unknown, -): Promise[typeof key]>> { - const schema = getValidationSchema(schemas?.[key]); - - if (!schema) { - return { success: true, data: data as InferSchemaMap[typeof key] }; - } - - if (isStandardSchema(schema)) { - const result = await schema["~standard"].validate(data); - if (result.issues) { - return { success: false, issues: [...result.issues] }; - } - return { - success: true, - data: result.value as InferSchemaMap[typeof key], - }; - } - - return { success: true, data: data as InferSchemaMap[typeof key] }; -} - export function validateSchemaSync( schemas: T | undefined, key: keyof T & string, diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/utils.ts b/rivetkit-typescript/packages/rivetkit/src/actor/utils.ts index 9e3f9f091e..2c81e9e52c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/utils.ts @@ -1,139 +1,3 @@ -import * as errors from "./errors"; -import { loggerWithoutContext } from "./log"; - -export function assertUnreachable(x: never): never { - loggerWithoutContext().error({ - msg: "unreachable", - value: `${x}`, - stack: new Error().stack, - }); - throw errors.internalError(`Unreachable case: ${x}`); -} - -export const throttle = < - // biome-ignore lint/suspicious/noExplicitAny: we want to allow any function - Fn extends (...args: any) => any, ->( - fn: Fn, - delay: number, -) => { - let lastRan = false; - let lastArgs: Parameters | null = null; - - return (...args: Parameters) => { - if (!lastRan) { - fn.apply(this, args); - lastRan = true; - const timer = () => - setTimeout(() => { - lastRan = false; - if (lastArgs) { - fn.apply(this, lastArgs); - lastRan = true; - lastArgs = null; - timer(); - } - }, delay); - timer(); - } else lastArgs = args; - }; -}; - -export class DeadlineError extends Error { - constructor() { - super("Promise did not complete before deadline."); - } -} - -export function deadline(promise: Promise, timeout: number): Promise { - const controller = new AbortController(); - const signal = controller.signal; - - // Set a timeout to abort the operation - const timeoutId = setTimeout(() => controller.abort(), timeout); - - return Promise.race([ - promise, - new Promise((_, reject) => { - signal.addEventListener("abort", () => reject(new DeadlineError())); - }), - ]).finally(() => { - clearTimeout(timeoutId); - }); -} - -export class Lock { - private _locked = false; - private _waiting: Array<() => void> = []; - - constructor(private _value: T) {} - - async lock(fn: (value: T) => Promise): Promise { - if (this._locked) { - await new Promise((resolve) => this._waiting.push(resolve)); - } - this._locked = true; - - try { - await fn(this._value); - } finally { - this._locked = false; - const next = this._waiting.shift(); - if (next) next(); - } - } -} - -export interface JoinedAbortSignal { - signal?: AbortSignal; - cleanup: () => void; -} - -export function joinAbortSignals( - ...signals: Array -): JoinedAbortSignal { - const activeSignals = signals.filter( - (signal): signal is AbortSignal => signal !== undefined, - ); - if (activeSignals.length === 0) { - return { signal: undefined, cleanup: () => {} }; - } - if (activeSignals.length === 1) { - return { signal: activeSignals[0], cleanup: () => {} }; - } - - const controller = new AbortController(); - if (activeSignals.some((signal) => signal.aborted)) { - controller.abort(); - return { signal: controller.signal, cleanup: () => {} }; - } - - const cleanup = () => { - for (const signal of activeSignals) { - signal.removeEventListener("abort", onAbort); - } - }; - const onAbort = () => { - controller.abort(); - cleanup(); - }; - for (const signal of activeSignals) { - signal.addEventListener("abort", onAbort, { once: true }); - } - - return { signal: controller.signal, cleanup }; -} - -export function generateSecureToken(length = 32) { - const array = new Uint8Array(length); - crypto.getRandomValues(array); - // Replace base64 chars that are not URL safe with URL-safe chars and strip padding - return btoa(String.fromCharCode(...array)) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); -} - /** * Checks if a path is an actor state path within the persisted actor data. */ diff --git a/rivetkit-typescript/packages/rivetkit/src/client/query.ts b/rivetkit-typescript/packages/rivetkit/src/client/query.ts index 02e23dced4..57ec361055 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/query.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/query.ts @@ -1,11 +1,4 @@ import { z } from "zod/v4"; -import { - HEADER_ACTOR_ID, - HEADER_ACTOR_QUERY, - HEADER_CONN_PARAMS, - HEADER_ENCODING, -} from "@/common/actor-router-consts"; -import { EncodingSchema } from "@/common/encoding"; // Maximum size of a key component in bytes // Set to 128 bytes to allow for separators and escape characters in the full key @@ -63,36 +56,11 @@ export const ActorQuerySchema = z.union([ }), ]); -export const ConnectRequestSchema = z.object({ - query: ActorQuerySchema.describe(HEADER_ACTOR_QUERY), - encoding: EncodingSchema.describe(HEADER_ENCODING), - connParams: z.string().optional().describe(HEADER_CONN_PARAMS), -}); - -export const ConnectWebSocketRequestSchema = z.object({ - query: ActorQuerySchema.describe("query"), - encoding: EncodingSchema.describe("encoding"), - connParams: z.unknown().optional().describe("conn_params"), -}); - -export const ConnMessageRequestSchema = z.object({ - actorId: z.string().describe(HEADER_ACTOR_ID), - encoding: EncodingSchema.describe(HEADER_ENCODING), -}); - -export const ResolveRequestSchema = z.object({ - query: ActorQuerySchema.describe(HEADER_ACTOR_QUERY), - connParams: z.string().optional().describe(HEADER_CONN_PARAMS), -}); - export type ActorQuery = z.infer; export type ActorGatewayQuery = Extract< ActorQuery, { getForKey: unknown } | { getOrCreateForKey: unknown } >; -export type GetForKeyRequest = z.infer; -export type GetOrCreateRequest = z.infer; -export type ConnectQuery = z.infer; /** * Interface representing a request to create a actor. */ diff --git a/rivetkit-typescript/packages/rivetkit/src/client/test.ts b/rivetkit-typescript/packages/rivetkit/src/client/test.ts deleted file mode 100644 index d4a4d115f0..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/client/test.ts +++ /dev/null @@ -1,44 +0,0 @@ -//import { exec as execCallback } from "node:child_process"; -//import { setupLogging } from "@/common//log"; -//import type { ClientOptions } from "./client"; -//import { InternalError } from "./errors"; -//import { Client } from "./mod.ts"; -// -///** -// * Uses the Rivet CLI to read the manager endpoint to connect to. This allows -// * for writing tests that run locally without hardcoding the manager endpoint. -// */ -//export async function readEndpointFromCli(): Promise { -// // Read endpoint -// const cliPath = process.env.RIVET_CLI_PATH ?? "rivet"; -// -// try { -// const { stdout, stderr } = await new Promise<{ -// stdout: string; -// stderr: string; -// }>((resolve, reject) => { -// execCallback(`${cliPath} manager endpoint`, (error, stdout, stderr) => { -// if (error) reject(error); -// else resolve({ stdout, stderr }); -// }); -// }); -// -// if (stderr) { -// throw new Error(stderr); -// } -// -// // Decode output -// return stdout.trim(); -// } catch (error) { -// throw new InternalError(`Read endpoint failed: ${error}`); -// } -//} -// -//export class TestClient extends Client { -// public constructor(opts?: ClientOptions) { -// // Setup logging automatically -// setupLogging(); -// -// super(readEndpointFromCli(), opts); -// } -//} diff --git a/rivetkit-typescript/packages/rivetkit/src/common/database/shared.ts b/rivetkit-typescript/packages/rivetkit/src/common/database/shared.ts index 6750245218..576760f489 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/database/shared.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/database/shared.ts @@ -37,13 +37,6 @@ export function isSqliteBindingObject( return Object.values(value).every((entry) => isSqliteBindingValue(entry)); } -export function isSqliteBindingArray(value: unknown): value is unknown[] { - return ( - Array.isArray(value) && - value.every((entry) => isSqliteBindingValue(entry)) - ); -} - export function toSqliteBindings( input: unknown[] | SqliteBindingObject, ): SqliteBindings { diff --git a/rivetkit-typescript/packages/rivetkit/src/common/encoding.ts b/rivetkit-typescript/packages/rivetkit/src/common/encoding.ts index c29e56cf9e..d3ccf220cc 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/encoding.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/encoding.ts @@ -1,7 +1,6 @@ import type { VersionedDataHandler } from "vbare"; import { z } from "zod/v4"; import { serializeWithEncoding } from "@/serde"; -import { assertUnreachable } from "./utils"; /** Data that can be deserialized. */ export type InputData = string | Buffer | Blob | ArrayBufferLike | Uint8Array; @@ -444,17 +443,6 @@ export function reviveJsonCompatValue( return input; } -/** Converts data that was encoded to a string. Some formats do not support raw binary data. */ -export function encodeDataToString(message: OutputData): string { - if (typeof message === "string") { - return message; - } - if (message instanceof Uint8Array) { - return base64EncodeUint8Array(message); - } - assertUnreachable(message); -} - function base64DecodeToUint8Array(base64: string): Uint8Array { if (typeof Buffer !== "undefined") { return new Uint8Array(Buffer.from(base64, "base64")); diff --git a/rivetkit-typescript/packages/rivetkit/src/common/eventsource.ts b/rivetkit-typescript/packages/rivetkit/src/common/eventsource.ts deleted file mode 100644 index 87ad83f7be..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/common/eventsource.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { EventSource } from "eventsource"; -import { logger } from "@/client/log"; - -// Global singleton promise that will be reused for subsequent calls -let eventSourcePromise: Promise | null = null; - -export async function importEventSource(): Promise { - // IMPORTANT: Import `eventsource` from the custom `eventsource` library. We need a custom implementation - // since we need to attach our own custom headers to the request. - // - // We can't use the browser-provided EventSource since it does not allow providing custom headers. - - // Return existing promise if we already started loading - if (eventSourcePromise !== null) { - return eventSourcePromise; - } - - // Create and store the promise - eventSourcePromise = (async () => { - let _EventSource: typeof EventSource; - - // Node.js environment - try { - const moduleName = "eventsource"; - const es = await import(/* webpackIgnore: true */ moduleName); - _EventSource = es.EventSource; - logger().debug("using eventsource from npm"); - } catch (_err) { - // EventSource not available - _EventSource = class MockEventSource { - constructor() { - throw new Error( - 'EventSource support requires installing the "eventsource" peer dependency.', - ); - } - } as unknown as typeof EventSource; - logger().debug("using mock eventsource"); - } - - return _EventSource; - })(); - - return eventSourcePromise; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/common/inline-websocket-adapter.ts b/rivetkit-typescript/packages/rivetkit/src/common/inline-websocket-adapter.ts index 748e6d7652..9f1d8dd5d5 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/inline-websocket-adapter.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/inline-websocket-adapter.ts @@ -211,14 +211,3 @@ export class InlineWebSocketAdapter { } } } - -/** - * Creates an InlineWebSocketAdapter and returns the client-side WebSocket. - * This is the main entry point for creating inline WebSocket connections. - */ -export function createInlineWebSocket( - handler: UpgradeWebSocketArgs, -): UniversalWebSocket { - const adapter = new InlineWebSocketAdapter(handler); - return adapter.clientWebSocket; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/common/log-levels.ts b/rivetkit-typescript/packages/rivetkit/src/common/log-levels.ts deleted file mode 100644 index cc58976904..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/common/log-levels.ts +++ /dev/null @@ -1,27 +0,0 @@ -export type LogLevel = - | "TRACE" - | "DEBUG" - | "INFO" - | "WARN" - | "ERROR" - | "CRITICAL"; - -export const LogLevels: Record = { - TRACE: 0, - DEBUG: 1, - INFO: 2, - WARN: 3, - ERROR: 4, - CRITICAL: 5, -} as const; - -export const LevelNameMap: Record = { - 0: "TRACE", - 1: "DEBUG", - 2: "INFO", - 3: "WARN", - 4: "ERROR", - 5: "CRITICAL", -}; - -export type LevelIndex = number; diff --git a/rivetkit-typescript/packages/rivetkit/src/common/network.ts b/rivetkit-typescript/packages/rivetkit/src/common/network.ts deleted file mode 100644 index 29119187b2..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/common/network.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Only enforced client-side in order to prevent building malformed URLs. */ -export const MAX_CONN_PARAMS_SIZE = 4096; diff --git a/rivetkit-typescript/packages/rivetkit/src/common/utils.ts b/rivetkit-typescript/packages/rivetkit/src/common/utils.ts index edb2494af5..a5f4ee172f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/utils.ts @@ -7,35 +7,6 @@ export function assertUnreachable(x: never): never { throw new Error(`Unreachable case: ${x}`); } -/** - * Safely stringifies an object, ensuring that the stringified object is under a certain size. - * @param obj any object to stringify - * @param maxSize maximum size of the stringified object in bytes - * @returns stringified object - */ -export function safeStringify(obj: unknown, maxSize: number) { - let size = 0; - - function replacer(key: string, value: unknown) { - if (value === null || value === undefined) return value; - const valueSize = - typeof value === "string" - ? value.length - : JSON.stringify(value).length; - size += key.length + valueSize; - - if (size > maxSize) { - throw new Error( - `JSON object exceeds size limit of ${maxSize} bytes.`, - ); - } - - return value; - } - - return JSON.stringify(obj, replacer); -} - export interface DeconstructedError { __type: "ActorError"; statusCode: ContentfulStatusCode; diff --git a/rivetkit-typescript/packages/rivetkit/src/common/websocket-test-hooks.ts b/rivetkit-typescript/packages/rivetkit/src/common/websocket-test-hooks.ts index 4c8f76cd83..55e39d09d0 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/websocket-test-hooks.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/websocket-test-hooks.ts @@ -8,13 +8,7 @@ export type IndexedWebSocketPayload = | Blob | ArrayBufferView; -export type IndexedWebSocketSender = ( - data: IndexedWebSocketPayload, - rivetMessageIndex?: number, -) => void | Promise; - export interface IndexedWebSocketTestHook { - __rivetSendWithMessageIndex?: IndexedWebSocketSender; __rivetGetHibernatableAckState?: () => HibernatableWebSocketAckStateSnapshot; __rivetWaitForHibernatableAck?: ( serverMessageIndex: number, @@ -41,25 +35,6 @@ export interface HibernatableWebSocketAckStateTestRequest { __rivetkitTestHibernatableAckStateV1: true; } -export function setIndexedWebSocketTestSender( - websocket: UniversalWebSocket, - sender: IndexedWebSocketSender, - enabled: boolean, -): void { - if (!enabled) { - return; - } - - (websocket as IndexedWebSocketTestHook).__rivetSendWithMessageIndex = - sender; -} - -export function getIndexedWebSocketTestSender( - websocket: UniversalWebSocket, -): IndexedWebSocketSender | undefined { - return (websocket as IndexedWebSocketTestHook).__rivetSendWithMessageIndex; -} - export function setHibernatableWebSocketAckTestHooks( websocket: UniversalWebSocket, hooks: { @@ -145,21 +120,6 @@ export function setRemoteHibernatableWebSocketAckTestHooks( ); } -export async function waitForHibernatableWebSocketAck( - websocket: UniversalWebSocket, - serverMessageIndex: number, -): Promise { - const waitForAck = (websocket as IndexedWebSocketTestHook) - .__rivetWaitForHibernatableAck; - if (!waitForAck) { - throw new Error( - "hibernatable websocket ack test hook is unavailable on this transport", - ); - } - - await waitForAck(serverMessageIndex); -} - export function parseHibernatableWebSocketAckStateTestRequest( data: IndexedWebSocketPayload, enabled: boolean, diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts deleted file mode 100644 index 298358ea73..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts +++ /dev/null @@ -1,2473 +0,0 @@ -import type { EnvoyConfig } from "@rivetkit/rivetkit-native/wrapper"; -import { - type EnvoyHandle, - type HibernatingWebSocketMetadata, - openDatabaseFromEnvoy, - type protocol, - startEnvoySync, -} from "@rivetkit/rivetkit-native/wrapper"; -import * as cbor from "cbor-x"; -import type { Context as HonoContext } from "hono"; -import { streamSSE } from "hono/streaming"; -import { WSContext, type WSContextInit } from "hono/ws"; -import invariant from "invariant"; -import { CONN_STATE_MANAGER_SYMBOL } from "@/actor/conn/mod"; -import { isStaticActorDefinition, lookupInRegistry } from "@/actor/definition"; -import type { ActorDriver } from "@/actor/driver"; -import { KEYS } from "@/actor/instance/keys"; -import type { AnyActorInstance } from "@/actor/instance/mod"; -import { - type AnyStaticActorInstance, - isStaticActorInstance, -} from "@/actor/instance/mod"; -import { - compareBytes, - createPreloadMap, - type PreloadMap, -} from "@/actor/instance/preload-map"; -import { deserializeActorKey } from "@/actor/keys"; -import type { Encoding } from "@/actor/protocol/serde"; -import { type ActorRouter, createActorRouter } from "@/actor/router"; -import { - parseWebSocketProtocols, - routeWebSocket, - truncateRawWebSocketPathPrefix, - type UpgradeWebSocketArgs, -} from "@/actor/router-websocket-endpoints"; -import type { Client } from "@/client/client"; -import { - PATH_CONNECT, - PATH_INSPECTOR_CONNECT, - PATH_WEBSOCKET_BASE, - PATH_WEBSOCKET_PREFIX, -} from "@/common/actor-router-consts"; -import { getLogger } from "@/common/log"; -import { deconstructError } from "@/common/utils"; -import type { - RivetMessageEvent, - UniversalWebSocket, -} from "@/common/websocket-interface"; -import { - buildHibernatableWebSocketAckStateTestResponse, - type IndexedWebSocketPayload, - parseHibernatableWebSocketAckStateTestRequest, - registerRemoteHibernatableWebSocketAckHooks, - setHibernatableWebSocketAckTestHooks, - unregisterRemoteHibernatableWebSocketAckHooks, -} from "@/common/websocket-test-hooks"; -import { - type JsNativeDatabaseLike, - wrapJsNativeDatabase, -} from "@/db/native-database"; -import { - type EngineControlClient, - getInitialActorKvState, -} from "@/driver-helpers/mod"; -import { DynamicActorInstance } from "@/dynamic/instance"; -import { isDynamicActorDefinition } from "@/dynamic/internal"; -import { DynamicActorIsolateRuntime } from "@/dynamic/isolate-runtime"; -import { getEndpoint } from "@/engine-client/api-utils"; -import { buildActorNames, type RegistryConfig } from "@/registry/config"; -import { - type LongTimeoutHandle, - promiseWithResolvers, - setLongTimeout, - stringifyError, - VERSION, -} from "@/utils"; -import { logger } from "./log"; - -const ENVOY_SSE_PING_INTERVAL = 1000; -const FALLBACK_ENVOY_STOP_WAIT_MS = 15_000; -const INITIAL_SLEEP_TIMEOUT_MS = 250; -const REMOTE_ACK_HOOK_QUERY_PARAM = "__rivetkitAckHook"; - -// Message ack deadline is 30s on the gateway, but we will ack more frequently -// in order to minimize the message buffer size on the gateway and to give -// generous breathing room for the timeout. -// -// See engine/packages/pegboard-gateway/src/shared_state.rs -// (HWS_MESSAGE_ACK_TIMEOUT) -const _CONN_MESSAGE_ACK_DEADLINE = 5_000; - -// Force saveState when cumulative message size reaches this threshold (0.5 MB) -// -// See engine/packages/pegboard-gateway/src/shared_state.rs -// (HWS_MAX_PENDING_MSGS_SIZE_PER_REQ) -const _CONN_BUFFERED_MESSAGE_SIZE_THRESHOLD = 500_000; - -interface ActorHandler { - actor?: AnyActorInstance; - actorName?: string; - actorStartPromise?: ReturnType>; - actorStartError?: Error; - alarmTimeout?: LongTimeoutHandle; - alarmTimestamp?: number; -} - -interface HibernatableWebSocketAckState { - lastSentIndex: number; - lastAckedIndex: number; - pendingIndexes: number[]; - ackWaiters: Map void>>; -} - -interface HibernatableConnectBinding { - actorId: string; - websocket: UniversalWebSocket; - request: Request; - requestPath: string; - requestHeaders: Record; - encoding: Encoding; - connParams: unknown; - gatewayId: ArrayBuffer; - requestId: ArrayBuffer; - remoteAckHookToken?: string; - detach?: () => void; -} - -interface HibernatableRunnerWebSocketBinding { - actorId: string; - websocket: UniversalWebSocket; - requestPath: string; - requestHeaders: Record; - encoding: Encoding; - connParams: unknown; - gatewayId: ArrayBuffer; - requestId: ArrayBuffer; - remoteAckHookToken?: string; - proxyToActorWs?: UniversalWebSocket; - detach?: () => void; -} - -export type DriverContext = {}; - -export class EngineActorDriver implements ActorDriver { - #config: RegistryConfig; - #inlineClient: Client; - #envoy: EnvoyHandle; - #actors: Map = new Map(); - #dynamicRuntimes = new Map(); - #hibernatableWebSocketAcks = new Map< - string, - HibernatableWebSocketAckState - >(); - #hibernatableConnectBindings = new Map< - string, - HibernatableConnectBinding - >(); - #hibernatableRunnerWebSocketBindings = new Map< - string, - HibernatableRunnerWebSocketBinding - >(); - #actorRouter: ActorRouter; - - #envoyStarted: PromiseWithResolvers = promiseWithResolvers((reason) => - logger().warn({ - msg: "unhandled envoy started promise rejection", - reason, - }), - ); - #envoyStopped: PromiseWithResolvers = promiseWithResolvers((reason) => - logger().warn({ - msg: "unhandled envoy stopped promise rejection", - reason, - }), - ); - #isEnvoyStopped: boolean = false; - #isShuttingDown: boolean = false; - - // HACK: Track actor stop intent locally since the envoy protocol doesn't - // pass the stop reason to onActorStop. This will be fixed when the envoy - // protocol is updated to send the intent directly (see RVT-5284) - #actorStopIntent: Map = new Map(); - - constructor( - config: RegistryConfig, - engineClient: EngineControlClient, - inlineClient: Client, - ) { - this.#config = config; - this.#engineClient = engineClient; - this.#inlineClient = inlineClient; - - // HACK: Override inspector token (which are likely to be - // removed later on) with token from x-rivet-token header - // TODO: - // if (token && runConfig.inspector && runConfig.inspector.enabled) { - // runConfig.inspector.token = () => token; - // } - - this.#actorRouter = createActorRouter( - config, - this, - undefined, - config.test.enabled, - ); - - // Create configuration - const envoyConfig: EnvoyConfig = { - version: config.envoy.version, - endpoint: getEndpoint(config), - token: config.token, - namespace: config.namespace, - poolName: config.envoy.poolName, - notGlobal: true, - metadata: { - rivetkit: { version: VERSION }, - }, - prepopulateActorNames: buildActorNames(config), - onShutdown: () => { - this.#envoyStopped.resolve(); - this.#isEnvoyStopped = true; - }, - fetch: this.#envoyFetch.bind(this), - websocket: this.#envoyWebSocket.bind(this), - hibernatableWebSocket: { - canHibernate: this.#hwsCanHibernate.bind(this), - }, - onActorStart: this.#envoyOnActorStart.bind(this), - onActorStop: this.#envoyOnActorStop.bind(this), - logger: getLogger("envoy-client"), - debugLatencyMs: process.env._RIVET_DEBUG_LATENCY_MS - ? Number.parseInt(process.env._RIVET_DEBUG_LATENCY_MS, 10) - : undefined, - }; - - // Create and start envoy - const envoy = startEnvoySync(envoyConfig); - - this.#envoy = envoy; - - envoy.started().then( - () => { - this.#envoyStarted.resolve(); - }, - (error) => { - this.#envoyStarted.reject(error); - }, - ); - - logger().debug({ - msg: "envoy client started", - endpoint: config.endpoint, - namespace: config.namespace, - poolName: config.envoy.poolName, - }); - } - - async #discardCrashedActorState(actorId: string) { - const handler = this.#actors.get(actorId); - if (!handler) { - return; - } - - if (handler.alarmTimeout) { - handler.alarmTimeout.abort(); - handler.alarmTimeout = undefined; - } - - if (handler.actor && isStaticActorInstance(handler.actor)) { - try { - await handler.actor.debugForceCrash(); - } catch (err) { - logger().debug({ - msg: "actor crash cleanup errored", - actorId, - error: stringifyError(err), - }); - } - } - - this.#actors.delete(actorId); - this.#actorStopIntent.delete(actorId); - } - - getExtraActorLogParams(): Record { - return { envoyKey: this.#envoy.getEnvoyKey() ?? "-" }; - } - - #hibernatableWebSocketAckKey( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): string { - return `${Buffer.from(gatewayId).toString("hex")}:${Buffer.from(requestId).toString("hex")}`; - } - - #ensureHibernatableWebSocketAckState( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): HibernatableWebSocketAckState { - const key = this.#hibernatableWebSocketAckKey(gatewayId, requestId); - let state = this.#hibernatableWebSocketAcks.get(key); - if (!state) { - state = { - lastSentIndex: 0, - lastAckedIndex: 0, - pendingIndexes: [], - ackWaiters: new Map(), - }; - this.#hibernatableWebSocketAcks.set(key, state); - } - return state; - } - - #deleteHibernatableWebSocketAckState( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): void { - this.#hibernatableWebSocketAcks.delete( - this.#hibernatableWebSocketAckKey(gatewayId, requestId), - ); - } - - #detachHibernatableConnectBinding( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): void { - const key = this.#hibernatableWebSocketAckKey(gatewayId, requestId); - const binding = this.#hibernatableConnectBindings.get(key); - if (!binding) { - return; - } - binding.detach?.(); - binding.detach = undefined; - } - - #deleteHibernatableConnectBinding( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): void { - const key = this.#hibernatableWebSocketAckKey(gatewayId, requestId); - const binding = this.#hibernatableConnectBindings.get(key); - binding?.detach?.(); - this.#hibernatableConnectBindings.delete(key); - } - - #cleanupFailedHibernatableConnectBinding( - binding: HibernatableConnectBinding, - ): void { - this.#deleteHibernatableWebSocketAckState( - binding.gatewayId, - binding.requestId, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - binding.remoteAckHookToken, - this.#config.test.enabled, - ); - this.#deleteHibernatableConnectBinding( - binding.gatewayId, - binding.requestId, - ); - } - - #detachHibernatableRunnerWebSocketBinding( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): void { - const key = this.#hibernatableWebSocketAckKey(gatewayId, requestId); - const binding = this.#hibernatableRunnerWebSocketBindings.get(key); - if (!binding) { - return; - } - binding.detach?.(); - binding.detach = undefined; - // Close the runtime-side ws so it does not outlive the binding when - // rebinding overwrites `binding.proxyToActorWs` with a fresh socket. - const previous = binding.proxyToActorWs; - if (previous && previous.readyState !== previous.CLOSED) { - previous.close(1011, "dynamic.rebind"); - } - binding.proxyToActorWs = undefined; - } - - #deleteHibernatableRunnerWebSocketBinding( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): void { - const key = this.#hibernatableWebSocketAckKey(gatewayId, requestId); - const binding = this.#hibernatableRunnerWebSocketBindings.get(key); - binding?.detach?.(); - this.#hibernatableRunnerWebSocketBindings.delete(key); - } - - #cleanupFailedHibernatableRunnerWebSocketBinding( - binding: HibernatableRunnerWebSocketBinding, - ): void { - const proxyToActorWs = binding.proxyToActorWs; - if ( - proxyToActorWs && - proxyToActorWs.readyState !== proxyToActorWs.CLOSED - ) { - proxyToActorWs.close(1011, "dynamic.bind_failed"); - } - this.#deleteHibernatableWebSocketAckState( - binding.gatewayId, - binding.requestId, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - binding.remoteAckHookToken, - this.#config.test.enabled, - ); - this.#deleteHibernatableRunnerWebSocketBinding( - binding.gatewayId, - binding.requestId, - ); - } - - #recordInboundHibernatableWebSocketMessage( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - rivetMessageIndex: number, - ): void { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - state.lastSentIndex = Math.max(state.lastSentIndex, rivetMessageIndex); - if (!state.pendingIndexes.includes(rivetMessageIndex)) { - state.pendingIndexes.push(rivetMessageIndex); - state.pendingIndexes.sort((a, b) => a - b); - } - } - - #recordAckedHibernatableWebSocketMessage( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - serverMessageIndex: number, - ): void { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - state.lastAckedIndex = Math.max( - state.lastAckedIndex, - serverMessageIndex, - ); - state.pendingIndexes = state.pendingIndexes.filter( - (index) => index > serverMessageIndex, - ); - for (const [index, waiters] of state.ackWaiters) { - if (index > serverMessageIndex) { - continue; - } - state.ackWaiters.delete(index); - for (const resolve of waiters) { - resolve(); - } - } - } - - #registerHibernatableWebSocketAckTestHooks( - websocket: UniversalWebSocket, - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - remoteHookToken?: string, - ): void { - setHibernatableWebSocketAckTestHooks( - websocket, - { - getState: () => { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - return { - lastSentIndex: state.lastSentIndex, - lastAckedIndex: state.lastAckedIndex, - pendingIndexes: [...state.pendingIndexes], - }; - }, - waitForAck: async (serverMessageIndex) => { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - if (state.lastAckedIndex >= serverMessageIndex) { - return; - } - await new Promise((resolve) => { - const waiters = - state.ackWaiters.get(serverMessageIndex) ?? []; - waiters.push(resolve); - state.ackWaiters.set(serverMessageIndex, waiters); - }); - }, - }, - this.#config.test.enabled, - ); - registerRemoteHibernatableWebSocketAckHooks( - remoteHookToken ?? "", - { - getState: () => { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - return { - lastSentIndex: state.lastSentIndex, - lastAckedIndex: state.lastAckedIndex, - pendingIndexes: [...state.pendingIndexes], - }; - }, - waitForAck: async (serverMessageIndex) => { - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - if (state.lastAckedIndex >= serverMessageIndex) { - return; - } - await new Promise((resolve) => { - const waiters = - state.ackWaiters.get(serverMessageIndex) ?? []; - waiters.push(resolve); - state.ackWaiters.set(serverMessageIndex, waiters); - }); - }, - }, - this.#config.test.enabled && Boolean(remoteHookToken), - ); - } - - #maybeRespondToHibernatableAckStateProbe( - websocket: UniversalWebSocket, - data: IndexedWebSocketPayload, - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - ): boolean { - if ( - !parseHibernatableWebSocketAckStateTestRequest( - data, - this.#config.test.enabled, - ) - ) { - return false; - } - - const state = this.#ensureHibernatableWebSocketAckState( - gatewayId, - requestId, - ); - const response = buildHibernatableWebSocketAckStateTestResponse( - { - lastSentIndex: state.lastSentIndex, - lastAckedIndex: state.lastAckedIndex, - pendingIndexes: [...state.pendingIndexes], - }, - this.#config.test.enabled, - ); - invariant(response, "missing hibernatable websocket ack test response"); - websocket.send(response); - return true; - } - - async #loadActorHandler(actorId: string): Promise { - // Check if actor is already loaded - const handler = this.#actors.get(actorId); - if (!handler) - throw new Error(`Actor handler does not exist ${actorId}`); - if (handler.actorStartPromise) await handler.actorStartPromise.promise; - if (handler.actorStartError) throw handler.actorStartError; - if (!handler.actor) throw new Error("Actor should be loaded"); - return handler; - } - - getContext(_actorId: string): DriverContext { - return {}; - } - - cancelAlarm(actorId: string): void { - const handler = this.#actors.get(actorId); - if (handler?.alarmTimeout) { - handler.alarmTimeout.abort(); - handler.alarmTimeout = undefined; - } - } - - #isDynamicActor(actorId: string): boolean { - return this.#dynamicRuntimes.has(actorId); - } - - #requireDynamicRuntime(actorId: string): DynamicActorIsolateRuntime { - const runtime = this.#dynamicRuntimes.get(actorId); - if (!runtime) { - throw new Error( - `dynamic runtime is not loaded for actor ${actorId}`, - ); - } - return runtime; - } - - async setAlarm(actor: AnyActorInstance, timestamp: number): Promise { - const handler = this.#actors.get(actor.id); - if (!handler) { - logger().warn({ - msg: "no handler for actor to set alarm", - }); - - return; - } - - // Clear prev timeout - if (handler.alarmTimeout && handler.alarmTimestamp === timestamp) { - return; - } - - if (handler.alarmTimeout) { - handler.alarmTimeout.abort(); - handler.alarmTimeout = undefined; - } - - // Set alarm - const delay = Math.max(0, timestamp - Date.now()); - handler.alarmTimestamp = timestamp; - handler.alarmTimeout = setLongTimeout(() => { - void (async () => { - const currentHandler = this.#actors.get(actor.id); - if (!currentHandler) { - logger().debug({ - msg: "alarm fired without loaded actor", - actorId: actor.id, - }); - return; - } - - if (currentHandler.actorStartPromise) { - try { - await currentHandler.actorStartPromise.promise; - } catch (error) { - logger().debug({ - msg: "alarm skipped after actor failed to start", - actorId: actor.id, - error: stringifyError(error), - }); - return; - } - } - - const alarmActor = this.#actors.get(actor.id)?.actor; - if (!alarmActor || alarmActor.isStopping) { - logger().debug({ - msg: "alarm fired without ready actor", - actorId: actor.id, - }); - return; - } - - await alarmActor.onAlarm(); - })().catch((error) => { - logger().error({ - msg: "actor alarm failed", - actorId: actor.id, - error: stringifyError(error), - }); - }); - handler.alarmTimeout = undefined; - handler.alarmTimestamp = undefined; - }, delay); - - // TODO: This call may not be needed on ActorInstance.start, but it does help ensure that the local state is synced with the alarm state - // Set alarm on Rivet - // - // This does not call an "alarm" event like Durable Objects. - // Instead, it just wakes the actor on the alarm (if not - // already awake). - // - // onAlarm is automatically called on `ActorInstance.start` when waking - // again. - this.#envoy.setAlarm(actor.id, timestamp); - } - - // Engine drivers expose the native SQLite provider directly. - - getInitialSleepTimeoutMs( - _actor: AnyActorInstance, - defaultTimeoutMs: number, - ): number { - return Math.max(defaultTimeoutMs, INITIAL_SLEEP_TIMEOUT_MS); - } - - getNativeDatabaseProvider() { - const envoy = this.#envoy; - return { - open: async (actorId: string) => { - const database: JsNativeDatabaseLike = - await openDatabaseFromEnvoy(envoy, actorId); - return wrapJsNativeDatabase(database); - }, - }; - } - - // MARK: - Batch KV operations - async kvBatchPut( - actorId: string, - entries: [Uint8Array, Uint8Array][], - ): Promise { - await this.#envoy.kvPut(actorId, entries); - } - - async kvBatchGet( - actorId: string, - keys: Uint8Array[], - ): Promise<(Uint8Array | null)[]> { - return await this.#envoy.kvGet(actorId, keys); - } - - async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise { - await this.#envoy.kvDelete(actorId, keys); - } - - async kvDeleteRange( - actorId: string, - start: Uint8Array, - end: Uint8Array, - ): Promise { - await this.#envoy.kvDeleteRange(actorId, start, end); - } - - async kvList(actorId: string): Promise { - const entries = await this.#envoy.kvListPrefix( - actorId, - new Uint8Array(), - ); - const keys = entries.map(([key]) => key); - return keys; - } - - async kvListPrefix( - actorId: string, - prefix: Uint8Array, - options?: { - reverse?: boolean; - limit?: number; - }, - ): Promise<[Uint8Array, Uint8Array][]> { - const result = await this.#envoy.kvListPrefix(actorId, prefix, options); - logger().info({ - msg: "kvListPrefix called", - actorId, - prefixStr: new TextDecoder().decode(prefix), - entriesCount: result.length, - keys: result.map(([key]: [Uint8Array, ...unknown[]]) => - new TextDecoder().decode(key), - ), - }); - return result; - } - - async kvListRange( - actorId: string, - start: Uint8Array, - end: Uint8Array, - options?: { - reverse?: boolean; - limit?: number; - }, - ): Promise<[Uint8Array, Uint8Array][]> { - return await this.#envoy.kvListRange( - actorId, - start, - end, - true, - options, - ); - } - - ackHibernatableWebSocketMessage( - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - serverMessageIndex: number, - ): void { - this.#recordAckedHibernatableWebSocketMessage( - gatewayId, - requestId, - serverMessageIndex, - ); - this.#envoy.sendHibernatableWebSocketMessageAck( - gatewayId, - requestId, - serverMessageIndex, - ); - } - - // MARK: - Actor Lifecycle - async loadActor(actorId: string): Promise { - const handler = await this.#loadActorHandler(actorId); - if (!handler.actor) throw new Error(`Actor ${actorId} failed to load`); - return handler.actor; - } - - startSleep(actorId: string) { - // HACK: Track intent for onActorStop (see RVT-5284) - this.#actorStopIntent.set(actorId, "sleep"); - this.#envoy.sleepActor(actorId); - } - - startDestroy(actorId: string) { - // HACK: Track intent for onActorStop (see RVT-5284) - this.#actorStopIntent.set(actorId, "destroy"); - this.#envoy.destroyActor(actorId); - } - - async hardCrashActor(actorId: string): Promise { - const handler = this.#actors.get(actorId); - if (!handler) { - return; - } - - if (handler.actorStartPromise) { - await handler.actorStartPromise.promise.catch(() => undefined); - } - - logger().info({ - msg: "simulating hard crash for actor", - actorId, - }); - - await this.#discardCrashedActorState(actorId); - this.#actorStopIntent.set(actorId, "crash"); - this.#envoy.stopActor(actorId, undefined, "simulated hard crash"); - } - - async shutdown(immediate: boolean): Promise { - if (this.#isShuttingDown) { - return; - } - this.#isShuttingDown = true; - const envoyStopWaitMs = this.#envoyStopWaitMs(); - - logger().info({ msg: "stopping engine actor driver", immediate }); - if (!immediate) { - // Put actors through the normal sleep intent path before draining the - // runner. This ensures Pegboard marks the actor workflow as sleeping - // and preserves wakeability across runner handoff. - logger().debug({ - msg: "sending sleep intent to actors before runner drain", - actorCount: this.#actors.size, - }); - for (const actorId of this.#actors.keys()) { - this.startSleep(actorId); - } - - const actorSleepDeadline = Date.now() + envoyStopWaitMs; - while (this.#actors.size > 0 && Date.now() < actorSleepDeadline) { - await new Promise((resolve) => setTimeout(resolve, 50)); - } - - if (this.#actors.size > 0) { - logger().warn({ - msg: "timed out waiting for actors to stop before envoy drain", - remainingActors: this.#actors.size, - waitMs: envoyStopWaitMs, - }); - // Snapshot so concurrent removals from `stopActor` do not - // invalidate the iterator. - for (const actorId of Array.from(this.#actors.keys())) { - logger().warn({ - msg: "force stopping actor during driver shutdown", - actorId, - }); - this.#envoy.stopActor( - actorId, - undefined, - "driver shutdown after sleep timeout", - ); - } - } else { - logger().debug({ - msg: "all actors stopped before envoy drain", - }); - } - } - - try { - await this.#envoy.shutdown(immediate); - } catch (error) { - const message = - error instanceof Error ? error.message : String(error); - if ( - message.includes("WebSocket connection closed during shutdown") - ) { - logger().debug({ - msg: "ignoring shutdown websocket close race", - error: message, - }); - } else { - throw error; - } - } - - const stopped = await Promise.race([ - this.#envoyStopped.promise.then(() => true), - new Promise((resolve) => - setTimeout(() => resolve(false), envoyStopWaitMs), - ), - ]); - if (!stopped) { - logger().warn({ - msg: "timed out waiting for envoy shutdown", - waitMs: envoyStopWaitMs, - }); - } - - await this.#disposeAllDynamicRuntimes("driver shutdown"); - } - - async waitForReady(): Promise { - await this.#envoy.started(); - } - - #envoyStopWaitMs(): number { - const actorStopThreshold = Number( - this.#envoy.getProtocolMetadata()?.actorStopThreshold, - ); - if (Number.isFinite(actorStopThreshold) && actorStopThreshold > 0) { - return actorStopThreshold; - } - return FALLBACK_ENVOY_STOP_WAIT_MS; - } - - async #bindHibernatableConnectSocket( - binding: HibernatableConnectBinding, - isRestoringHibernatable: boolean, - ): Promise { - this.#detachHibernatableConnectBinding( - binding.gatewayId, - binding.requestId, - ); - this.#hibernatableConnectBindings.set( - this.#hibernatableWebSocketAckKey( - binding.gatewayId, - binding.requestId, - ), - binding, - ); - - if (this.#isDynamicActor(binding.actorId)) { - try { - await this.#bindDynamicHibernatableConnectSocket( - binding, - isRestoringHibernatable, - ); - } catch (error) { - this.#cleanupFailedHibernatableConnectBinding(binding); - throw error; - } - return; - } - - const wsHandler = await routeWebSocket( - binding.request, - binding.requestPath, - binding.requestHeaders, - this.#config, - this, - binding.actorId, - binding.encoding, - binding.connParams, - binding.gatewayId, - binding.requestId, - true, - isRestoringHibernatable, - ).catch((error) => { - this.#cleanupFailedHibernatableConnectBinding(binding); - throw error; - }); - - (binding.websocket as WSContextInit).raw = binding.websocket; - const wsContext = new WSContext(binding.websocket); - - const onOpen = (event: Event) => { - wsHandler.onOpen(event, wsContext); - }; - const onMessage = (event: RivetMessageEvent) => { - if ( - this.#maybeRespondToHibernatableAckStateProbe( - binding.websocket, - event.data, - binding.gatewayId, - binding.requestId, - ) - ) { - return; - } - - wsHandler.onMessage(event, wsContext); - - const actor = this.#actors.get(binding.actorId)?.actor; - if (!actor || !isStaticActorInstance(actor) || !wsHandler.conn) { - return; - } - - const conn = actor.connectionManager.findHibernatableConn( - binding.gatewayId, - binding.requestId, - ); - if (!conn) { - return; - } - - if (typeof event.rivetMessageIndex === "number") { - this.#recordInboundHibernatableWebSocketMessage( - binding.gatewayId, - binding.requestId, - event.rivetMessageIndex, - ); - } - actor.handleInboundHibernatableWebSocketMessage( - conn, - event.data, - event.rivetMessageIndex, - ); - }; - const onClose = (event: CloseEvent) => { - wsHandler.onClose(event, wsContext); - this.#deleteHibernatableWebSocketAckState( - binding.gatewayId, - binding.requestId, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - binding.remoteAckHookToken, - this.#config.test.enabled, - ); - this.#deleteHibernatableConnectBinding( - binding.gatewayId, - binding.requestId, - ); - }; - const onError = (event: Event) => { - wsHandler.onError(event, wsContext); - }; - - binding.websocket.addEventListener("message", onMessage); - binding.websocket.addEventListener("close", onClose); - binding.websocket.addEventListener("error", onError); - if (isRestoringHibernatable) { - wsHandler.onRestore?.(wsContext); - } else { - binding.websocket.addEventListener("open", onOpen); - } - - binding.detach = () => { - binding.websocket.removeEventListener("message", onMessage); - binding.websocket.removeEventListener("close", onClose); - binding.websocket.removeEventListener("error", onError); - if (!isRestoringHibernatable) { - binding.websocket.removeEventListener("open", onOpen); - } - }; - } - - async #bindDynamicHibernatableConnectSocket( - binding: HibernatableConnectBinding, - isRestoringHibernatable: boolean, - ): Promise { - const runtime = this.#requireDynamicRuntime(binding.actorId); - const proxyToActorWs = await runtime.openWebSocket( - binding.requestPath, - binding.encoding, - binding.connParams, - { - headers: binding.requestHeaders, - gatewayId: binding.gatewayId, - requestId: binding.requestId, - isHibernatable: true, - isRestoringHibernatable, - }, - ); - - const onProxyMessage = (event: RivetMessageEvent) => { - if (binding.websocket.readyState !== binding.websocket.OPEN) { - return; - } - binding.websocket.send(event.data as any); - }; - const onProxyClose = (event: CloseEvent) => { - if (event.reason === "dynamic.runtime.disposed") { - return; - } - if (binding.websocket.readyState !== binding.websocket.CLOSED) { - binding.websocket.close(event.code, event.reason); - } - }; - const onProxyError = () => { - if (binding.websocket.readyState !== binding.websocket.CLOSED) { - binding.websocket.close(1011, "dynamic.websocket_error"); - } - }; - const onMessage = (event: RivetMessageEvent) => { - if ( - this.#maybeRespondToHibernatableAckStateProbe( - binding.websocket, - event.data, - binding.gatewayId, - binding.requestId, - ) - ) { - return; - } - - if (typeof event.rivetMessageIndex === "number") { - this.#recordInboundHibernatableWebSocketMessage( - binding.gatewayId, - binding.requestId, - event.rivetMessageIndex, - ); - } - - void runtime - .forwardIncomingWebSocketMessage( - proxyToActorWs, - event.data as any, - event.rivetMessageIndex, - ) - .catch((error) => { - logger().error({ - msg: "failed forwarding websocket message to dynamic actor", - actorId: binding.actorId, - error: stringifyError(error), - }); - binding.websocket.close( - 1011, - "dynamic.websocket_forward_failed", - ); - }); - }; - const onClose = (event: CloseEvent) => { - if (proxyToActorWs.readyState !== proxyToActorWs.CLOSED) { - proxyToActorWs.close(event.code, event.reason); - } - this.#deleteHibernatableWebSocketAckState( - binding.gatewayId, - binding.requestId, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - binding.remoteAckHookToken, - this.#config.test.enabled, - ); - this.#deleteHibernatableConnectBinding( - binding.gatewayId, - binding.requestId, - ); - }; - const onError = () => { - if (proxyToActorWs.readyState !== proxyToActorWs.CLOSED) { - proxyToActorWs.close(1011, "dynamic.gateway_error"); - } - }; - - proxyToActorWs.addEventListener("message", onProxyMessage); - proxyToActorWs.addEventListener("close", onProxyClose); - proxyToActorWs.addEventListener("error", onProxyError); - binding.websocket.addEventListener("message", onMessage); - binding.websocket.addEventListener("close", onClose); - binding.websocket.addEventListener("error", onError); - - binding.detach = () => { - proxyToActorWs.removeEventListener("message", onProxyMessage); - proxyToActorWs.removeEventListener("close", onProxyClose); - proxyToActorWs.removeEventListener("error", onProxyError); - binding.websocket.removeEventListener("message", onMessage); - binding.websocket.removeEventListener("close", onClose); - binding.websocket.removeEventListener("error", onError); - if (proxyToActorWs.readyState !== proxyToActorWs.CLOSED) { - proxyToActorWs.close(1011, "dynamic.rebind"); - } - }; - } - - async #bindDynamicHibernatableRunnerWebSocket( - binding: HibernatableRunnerWebSocketBinding, - isRestoringHibernatable: boolean, - ): Promise { - this.#detachHibernatableRunnerWebSocketBinding( - binding.gatewayId, - binding.requestId, - ); - this.#hibernatableRunnerWebSocketBindings.set( - this.#hibernatableWebSocketAckKey( - binding.gatewayId, - binding.requestId, - ), - binding, - ); - - let runtime: DynamicActorIsolateRuntime; - let proxyToActorWs: UniversalWebSocket; - try { - runtime = this.#requireDynamicRuntime(binding.actorId); - proxyToActorWs = await runtime.openWebSocket( - binding.requestPath, - binding.encoding, - binding.connParams, - { - headers: binding.requestHeaders, - gatewayId: binding.gatewayId, - requestId: binding.requestId, - isHibernatable: true, - isRestoringHibernatable, - }, - ); - } catch (error) { - this.#cleanupFailedHibernatableRunnerWebSocketBinding(binding); - throw error; - } - binding.proxyToActorWs = proxyToActorWs; - - const onProxyMessage = (event: RivetMessageEvent) => { - if (binding.websocket.readyState !== binding.websocket.OPEN) { - return; - } - binding.websocket.send(event.data as any); - }; - const onProxyClose = (event: CloseEvent) => { - if (event.reason === "dynamic.runtime.disposed") { - return; - } - if (binding.websocket.readyState !== binding.websocket.CLOSED) { - binding.websocket.close(event.code, event.reason); - } - }; - const onProxyError = () => { - if (binding.websocket.readyState !== binding.websocket.CLOSED) { - binding.websocket.close(1011, "dynamic.websocket_error"); - } - }; - const onMessage = (event: RivetMessageEvent) => { - if ( - this.#maybeRespondToHibernatableAckStateProbe( - binding.websocket, - event.data, - binding.gatewayId, - binding.requestId, - ) - ) { - return; - } - - if (typeof event.rivetMessageIndex === "number") { - this.#recordInboundHibernatableWebSocketMessage( - binding.gatewayId, - binding.requestId, - event.rivetMessageIndex, - ); - } - - const currentRuntime = this.#dynamicRuntimes.get(binding.actorId); - const currentProxyToActorWs = binding.proxyToActorWs; - if (!currentRuntime || !currentProxyToActorWs) { - logger().error({ - msg: "dynamic runtime websocket binding is missing after restore", - actorId: binding.actorId, - }); - binding.websocket.close( - 1011, - "dynamic.websocket_forward_failed", - ); - return; - } - - void currentRuntime - .forwardIncomingWebSocketMessage( - currentProxyToActorWs, - event.data as any, - event.rivetMessageIndex, - ) - .catch((error) => { - logger().error({ - msg: "failed forwarding websocket message to dynamic actor", - actorId: binding.actorId, - error: stringifyError(error), - }); - binding.websocket.close( - 1011, - "dynamic.websocket_forward_failed", - ); - }); - }; - const onClose = (event: CloseEvent) => { - const currentProxyToActorWs = binding.proxyToActorWs; - if ( - currentProxyToActorWs && - currentProxyToActorWs.readyState !== - currentProxyToActorWs.CLOSED - ) { - currentProxyToActorWs.close(event.code, event.reason); - } - this.#deleteHibernatableWebSocketAckState( - binding.gatewayId, - binding.requestId, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - binding.remoteAckHookToken, - this.#config.test.enabled, - ); - this.#deleteHibernatableRunnerWebSocketBinding( - binding.gatewayId, - binding.requestId, - ); - }; - const onError = () => { - const currentProxyToActorWs = binding.proxyToActorWs; - if ( - currentProxyToActorWs && - currentProxyToActorWs.readyState !== - currentProxyToActorWs.CLOSED - ) { - currentProxyToActorWs.close(1011, "dynamic.gateway_error"); - } - }; - - proxyToActorWs.addEventListener("message", onProxyMessage); - proxyToActorWs.addEventListener("close", onProxyClose); - proxyToActorWs.addEventListener("error", onProxyError); - binding.websocket.addEventListener("message", onMessage); - binding.websocket.addEventListener("close", onClose); - binding.websocket.addEventListener("error", onError); - - binding.detach = () => { - proxyToActorWs.removeEventListener("message", onProxyMessage); - proxyToActorWs.removeEventListener("close", onProxyClose); - proxyToActorWs.removeEventListener("error", onProxyError); - binding.websocket.removeEventListener("message", onMessage); - binding.websocket.removeEventListener("close", onClose); - binding.websocket.removeEventListener("error", onError); - }; - } - - async #rebindDynamicHibernatableRunnerWebSockets( - actorId: string, - ): Promise { - const bindings = Array.from( - this.#hibernatableRunnerWebSocketBindings.values(), - ).filter((binding) => binding.actorId === actorId); - // Bind every binding even if some throw, so a single failed rebind - // does not strand the rest in an idle state. - const results = await Promise.allSettled( - bindings.map((binding) => - this.#bindDynamicHibernatableRunnerWebSocket(binding, true), - ), - ); - for (const result of results) { - if (result.status === "rejected") { - logger().warn({ - msg: "failed to rebind dynamic hibernatable runner websocket", - actorId, - error: stringifyError(result.reason), - }); - } - } - } - - async #rebindHibernatableConnectSockets(actorId: string): Promise { - const bindings = Array.from( - this.#hibernatableConnectBindings.values(), - ).filter((binding) => binding.actorId === actorId); - - // Same reasoning as `#rebindDynamicHibernatableRunnerWebSockets`: a - // failure in one binding should not block the others from rebinding. - const results = await Promise.allSettled( - bindings.map((binding) => - this.#bindHibernatableConnectSocket(binding, true), - ), - ); - for (const result of results) { - if (result.status === "rejected") { - logger().warn({ - msg: "failed to rebind hibernatable connect socket", - actorId, - error: stringifyError(result.reason), - }); - } - } - } - - async serverlessHandleStart(c: HonoContext): Promise { - const payload = await c.req.arrayBuffer(); - - return streamSSE(c, async (stream) => { - // NOTE: onAbort does not work reliably - stream.onAbort(() => {}); - c.req.raw.signal.addEventListener("abort", () => { - logger().debug("SSE aborted"); - }); - - await this.#envoyStarted.promise; - - if (this.#isShuttingDown) { - logger().debug({ - msg: "ignoring serverless start because driver is shutting down", - }); - return; - } - - await this.#envoy.startServerlessActor(payload); - - // Send ping every second to keep the connection alive - while (true) { - if (this.#isEnvoyStopped) { - logger().debug({ - msg: "envoy is stopped", - }); - break; - } - - if (stream.closed || stream.aborted) { - logger().debug({ - msg: "envoy sse stream closed", - closed: stream.closed, - aborted: stream.aborted, - }); - break; - } - - await stream.writeSSE({ event: "ping", data: "" }); - await stream.sleep(ENVOY_SSE_PING_INTERVAL); - } - }); - } - - #buildStartupPreloadMap( - preloadedKv: protocol.PreloadedKv | null, - persistDataOverride?: Uint8Array, - ): { preloadMap: PreloadMap | undefined; entries: number } { - if (preloadedKv == null) { - return { preloadMap: undefined, entries: 0 }; - } - - const entries: [Uint8Array, Uint8Array][] = preloadedKv.entries.map( - (entry) => [new Uint8Array(entry.key), new Uint8Array(entry.value)], - ); - - if (persistDataOverride) { - let replaced = false; - for (const entry of entries) { - if (compareBytes(entry[0], KEYS.PERSIST_DATA) === 0) { - entry[1] = persistDataOverride; - replaced = true; - break; - } - } - - if (!replaced) { - entries.push([KEYS.PERSIST_DATA, persistDataOverride]); - } - } - - entries.sort((a, b) => compareBytes(a[0], b[0])); - - const requestedGetKeys = preloadedKv.requestedGetKeys - .map((key) => new Uint8Array(key)) - .sort(compareBytes); - const requestedPrefixes = preloadedKv.requestedPrefixes - .map((prefix) => new Uint8Array(prefix)) - .sort(compareBytes); - - return { - preloadMap: createPreloadMap( - entries, - requestedGetKeys, - requestedPrefixes, - ), - entries: entries.length, - }; - } - - async #envoyOnActorStart( - _envoy: EnvoyHandle, - actorId: string, - generation: number, - actorConfig: protocol.ActorConfig, - preloadedKv: protocol.PreloadedKv | null, - _sqliteStartupData: protocol.SqliteStartupData | null, - ): Promise { - if (this.#isShuttingDown) { - logger().debug({ - msg: "rejecting actor start because driver is shutting down", - actorId, - name: actorConfig.name, - generation, - }); - throw new Error("engine actor driver is shutting down"); - } - - logger().debug({ - msg: "engine actor starting", - actorId, - name: actorConfig.name, - key: actorConfig.key, - generation, - }); - - // Deserialize input - let input: any; - if (actorConfig.input && actorConfig.input.byteLength > 0) { - input = cbor.decode(new Uint8Array(actorConfig.input)); - } - - // Get or create handler - let handler = this.#actors.get(actorId); - if (!handler) { - // IMPORTANT: We must set the handler in the map synchronously before doing any - // async operations to avoid race conditions where multiple calls might try to - // create the same handler simultaneously. - handler = { - actorStartPromise: promiseWithResolvers((reason) => - logger().warn({ - msg: "unhandled actor start promise rejection", - reason, - }), - ), - }; - this.#actors.set(actorId, handler); - } - handler.actorStartError = undefined; - - const name = actorConfig.name as string; - invariant(actorConfig.key, "actor should have a key"); - const key = deserializeActorKey(actorConfig.key); - handler.actorName = name; - - try { - let preloadMap: PreloadMap | undefined; - let persistDataBuffer: Uint8Array | null | undefined; - let checkPersistDataMs = 0; - let initNewActorMs = 0; - let preloadKvMs = 0; - let preloadKvEntries = 0; - let driverKvRoundTrips = 0; - - if (preloadedKv) { - const preloadStart = performance.now(); - const preloaded = this.#buildStartupPreloadMap(preloadedKv); - preloadMap = preloaded.preloadMap; - preloadKvEntries = preloaded.entries; - preloadKvMs = performance.now() - preloadStart; - persistDataBuffer = preloadMap?.get(KEYS.PERSIST_DATA)?.value; - logger().debug({ - msg: "received startup kv preload from start command", - actorId, - entries: preloadKvEntries, - durationMs: preloadKvMs, - }); - } - - if (persistDataBuffer === undefined) { - const checkStart = performance.now(); - const [persistData] = await this.#envoy.kvGet(actorId, [ - KEYS.PERSIST_DATA, - ]); - persistDataBuffer = persistData; - checkPersistDataMs = performance.now() - checkStart; - driverKvRoundTrips++; - } - - if (persistDataBuffer === null) { - const initStart = performance.now(); - const initialKvState = getInitialActorKvState(input); - const persistData = initialKvState[0]?.[1]; - await this.#envoy.kvPut(actorId, initialKvState); - initNewActorMs = performance.now() - initStart; - driverKvRoundTrips++; - if (preloadedKv && persistData) { - const preloadStart = performance.now(); - const preloaded = this.#buildStartupPreloadMap( - preloadedKv, - persistData, - ); - preloadMap = preloaded.preloadMap; - preloadKvEntries = preloaded.entries; - preloadKvMs += performance.now() - preloadStart; - } - logger().debug({ - msg: "initialized persist data for new actor", - actorId, - durationMs: initNewActorMs, - }); - } - - // Create actor instance - const definition = lookupInRegistry(this.#config, actorConfig.name); - if (isDynamicActorDefinition(definition)) { - let runtime = this.#dynamicRuntimes.get(actorId); - if (!runtime) { - runtime = new DynamicActorIsolateRuntime({ - actorId, - actorName: name, - actorKey: key, - input, - region: "unknown", - loader: definition.loader, - actorDriver: this, - inlineClient: this.#inlineClient, - test: this.#config.test, - }); - await runtime.start(); - this.#dynamicRuntimes.set(actorId, runtime); - } - - const dynamicActor = new DynamicActorInstance(actorId, runtime); - handler.actor = dynamicActor; - - handler.actorStartError = undefined; - handler.actorStartPromise?.resolve(); - handler.actorStartPromise = undefined; - - try { - await this.#rebindHibernatableConnectSockets(actorId); - await this.#rebindDynamicHibernatableRunnerWebSockets( - actorId, - ); - const rawMetaEntries = - await dynamicActor.getHibernatingWebSockets(); - const metaEntries = rawMetaEntries.map((entry) => ({ - gatewayId: entry.gatewayId, - requestId: entry.requestId, - rivetMessageIndex: entry.serverMessageIndex, - envoyMessageIndex: entry.clientMessageIndex, - path: entry.path, - headers: entry.headers, - })); - await this.#envoy.restoreHibernatingRequests( - actorId, - metaEntries, - ); - } catch (error) { - logger().warn({ - msg: "failed to restore dynamic hibernating requests after actor start", - actorId, - error: stringifyError(error), - }); - } - } else if (isStaticActorDefinition(definition)) { - const instantiateStart = performance.now(); - const staticActor = - (await definition.instantiate()) as AnyStaticActorInstance; - const instantiateMs = performance.now() - instantiateStart; - handler.actor = staticActor; - - // Record driver-level startup metrics on the actor. - staticActor.metrics.startup.checkPersistDataMs = - checkPersistDataMs; - staticActor.metrics.startup.initNewActorMs = initNewActorMs; - staticActor.metrics.startup.preloadKvMs = preloadKvMs; - staticActor.metrics.startup.preloadKvEntries = preloadKvEntries; - staticActor.metrics.startup.instantiateMs = instantiateMs; - staticActor.metrics.startup.kvRoundTrips = driverKvRoundTrips; - - // Cap the actor's graceful shutdown window to fit inside the - // engine's stop deadline, leaving a 1s safety margin. On - // serverless this also adds the drain grace period so sleep - // can run during the drain window. - const protocolMetadata = this.#envoy.getProtocolMetadata(); - if (protocolMetadata) { - const stopThresholdMax = Math.max( - Number(protocolMetadata.actorStopThreshold) - 1000, - 0, - ); - const drainMax = protocolMetadata.serverlessDrainGracePeriod - ? Math.max( - Number( - protocolMetadata.serverlessDrainGracePeriod, - ) - 1000, - 0, - ) - : 0; - staticActor.overrides.sleepGracePeriod = - stopThresholdMax + drainMax; - } - - // Start actor - await staticActor.start( - this, - this.#inlineClient, - actorId, - name, - key, - "unknown", // TODO: Add regions - preloadMap, - ); - } else { - throw new Error( - `actor definition for ${actorConfig.name} is not instantiable`, - ); - } - - logger().debug({ msg: "engine actor started", actorId, name, key }); - } catch (innerError) { - const dynamicRuntime = this.#dynamicRuntimes.get(actorId); - if (dynamicRuntime) { - try { - await dynamicRuntime.dispose(); - } catch (disposeError) { - logger().debug({ - msg: "failed to dispose dynamic runtime after actor start failure", - actorId, - error: stringifyError(disposeError), - }); - } - this.#dynamicRuntimes.delete(actorId); - } - const error = - innerError instanceof Error - ? new Error( - `Failed to start actor ${actorId}: ${innerError.message}`, - { cause: innerError }, - ) - : new Error( - `Failed to start actor ${actorId}: ${String(innerError)}`, - ); - handler.actor = undefined; - handler.actorStartError = error; - handler.actorStartPromise?.reject(error); - handler.actorStartPromise = undefined; - logger().error({ - msg: "engine actor failed to start", - actorId, - name, - key, - error: stringifyError(error), - }); - - try { - this.#envoy.stopActor(actorId, undefined); - } catch (stopError) { - logger().debug({ - msg: "failed to stop actor after start failure", - actorId, - error: stringifyError(stopError), - }); - } - } - } - - async #envoyOnActorStop( - _envoyHandle: EnvoyHandle, - actorId: string, - generation: number, - _reason: protocol.StopActorReason, - ): Promise { - logger().debug({ msg: "engine actor stopping", actorId, generation }); - - // HACK: Retrieve the stop intent we tracked locally (see RVT-5284) - // Default to "sleep" if no intent was recorded (e.g., if the runner - // initiated the stop) - // - // TODO: This will not work if the actor is destroyed from the API - // correctly. Currently, it will use the sleep intent, but it's - // actually a destroy intent. - const reason = this.#actorStopIntent.get(actorId) ?? "sleep"; - this.#actorStopIntent.delete(actorId); - - const handler = this.#actors.get(actorId); - if (!handler) { - logger().debug({ - msg: "no engine actor handler to stop", - actorId, - reason, - }); - return; - } - - if (handler.actorStartPromise) { - try { - logger().debug({ - msg: "engine actor stopping before it started, waiting", - actorId, - generation, - }); - await handler.actorStartPromise.promise; - } catch (err) { - // Start failed, but we still want to clean up the handler - logger().debug({ - msg: "actor start failed during stop, cleaning up handler", - actorId, - error: stringifyError(err), - }); - } - } - - if (handler.actor) { - try { - if ( - reason === "crash" && - isStaticActorInstance(handler.actor) - ) { - await handler.actor.debugForceCrash(); - } else if (reason !== "crash") { - await handler.actor.onStop(reason); - } - } catch (err) { - logger().error({ - msg: "error in onStop, proceeding with removing actor", - error: stringifyError(err), - }); - } - } - await this.#disposeDynamicRuntime(actorId, "actor stop"); - - if (handler.alarmTimeout) { - handler.alarmTimeout.abort(); - handler.alarmTimeout = undefined; - } - - this.#actors.delete(actorId); - - logger().debug({ msg: "engine actor stopped", actorId, reason }); - } - - async #disposeDynamicRuntime( - actorId: string, - reason: string, - ): Promise { - const runtime = this.#dynamicRuntimes.get(actorId); - if (!runtime) { - return; - } - - try { - await runtime.dispose(); - } catch (error) { - logger().warn({ - msg: "failed to dispose dynamic runtime", - actorId, - reason, - error: stringifyError(error), - }); - } finally { - this.#dynamicRuntimes.delete(actorId); - } - } - - async #disposeAllDynamicRuntimes(reason: string): Promise { - await Promise.all( - Array.from(this.#dynamicRuntimes.keys(), (actorId) => - this.#disposeDynamicRuntime(actorId, reason), - ), - ); - } - - // MARK: - Envoy Networking - async #envoyFetch( - _envoy: EnvoyHandle, - actorId: string, - _gatewayIdBuf: ArrayBuffer, - _requestIdBuf: ArrayBuffer, - request: Request, - ): Promise { - logger().debug({ - msg: "envoy fetch", - actorId, - url: request.url, - method: request.method, - }); - const overlayResponse = this.#routeOverlayRequest(actorId, request); - if (overlayResponse) { - return overlayResponse; - } - - if (this.#isDynamicActor(actorId)) { - return await this.#requireDynamicRuntime(actorId).fetch(request); - } - return await this.#actorRouter.fetch(request, { actorId }); - } - - #routeOverlayRequest(actorId: string, request: Request): Response | null { - const url = new URL(request.url); - switch (`${request.method} ${url.pathname}`) { - case "PUT /dynamic/reload": - return this.#handleDynamicReloadOverlay(actorId); - default: - return null; - } - } - - #handleDynamicReloadOverlay(actorId: string): Response { - if (!this.#isDynamicActor(actorId)) { - return new Response("not a dynamic actor", { status: 404 }); - } - this.startSleep(actorId); - return new Response(null, { status: 200 }); - } - - async #envoyWebSocket( - _envoy: EnvoyHandle, - actorId: string, - websocketRaw: any, - gatewayIdBuf: ArrayBuffer, - requestIdBuf: ArrayBuffer, - request: Request, - requestPath: string, - requestHeaders: Record, - isHibernatable: boolean, - isRestoringHibernatable: boolean, - ): Promise { - const websocket = websocketRaw as UniversalWebSocket; - - // Parse configuration from Sec-WebSocket-Protocol header (optional for path-based routing) - const protocols = request.headers.get("sec-websocket-protocol"); - const { encoding, connParams, ackHookToken } = - parseWebSocketProtocols(protocols); - const remoteAckHookToken = - ackHookToken ?? - new URL(request.url).searchParams.get( - REMOTE_ACK_HOOK_QUERY_PARAM, - ) ?? - undefined; - const requestPathWithoutQuery = requestPath.split("?")[0]; - - if (isHibernatable && requestPathWithoutQuery === PATH_CONNECT) { - this.#registerHibernatableWebSocketAckTestHooks( - websocket, - gatewayIdBuf, - requestIdBuf, - remoteAckHookToken, - ); - try { - await this.#bindHibernatableConnectSocket( - { - actorId, - websocket, - request, - requestPath, - requestHeaders, - encoding, - connParams, - gatewayId: gatewayIdBuf, - requestId: requestIdBuf, - remoteAckHookToken, - }, - isRestoringHibernatable, - ); - } catch (error) { - logger().error({ - msg: "failed to bind hibernatable connect websocket", - actorId, - error: stringifyError(error), - }); - websocket.close(1011, "ws.route_error"); - } - return; - } - - if (this.#isDynamicActor(actorId)) { - await this.#runnerDynamicWebSocket( - actorId, - websocket, - gatewayIdBuf, - requestIdBuf, - requestPath, - requestHeaders, - encoding, - connParams, - isHibernatable, - isRestoringHibernatable, - ); - return; - } - - // Fetch WS handler - // - // We store the promise since we need to add WebSocket event listeners immediately that will wait for the promise to resolve - let wsHandler: UpgradeWebSocketArgs; - try { - wsHandler = await routeWebSocket( - request, - requestPath, - requestHeaders, - this.#config, - this, - actorId, - encoding, - connParams, - gatewayIdBuf, - requestIdBuf, - isHibernatable, - isRestoringHibernatable, - ); - } catch (error) { - logger().error({ - msg: "building websocket handlers errored", - error, - }); - websocketRaw.close(1011, "ws.route_error"); - return; - } - - // Connect the Hono WS hook to the adapter - // - // We need to assign to `raw` in order for WSContext to expose it on - // `ws.raw` - (websocket as WSContextInit).raw = websocket; - const wsContext = new WSContext(websocket); - - // Get connection and actor from wsHandler (may be undefined for inspector endpoint) - const conn = wsHandler.conn; - const actor = wsHandler.actor; - const _connStateManager = conn?.[CONN_STATE_MANAGER_SYMBOL]; - - // Bind event listeners to Hono WebSocket handlers - // - // We update the HWS data after calling handlers in order to ensure - // that the handler ran successfully. By doing this, we ensure at least - // once delivery of events to the event handlers. - - if (isHibernatable) { - this.#registerHibernatableWebSocketAckTestHooks( - websocket, - gatewayIdBuf, - requestIdBuf, - remoteAckHookToken, - ); - } - - if (isRestoringHibernatable) { - wsHandler.onRestore?.(wsContext); - } - - const isRawWebSocketPath = - requestPath === PATH_WEBSOCKET_BASE || - requestPath.startsWith(PATH_WEBSOCKET_PREFIX); - const handleMessageEvent = (event: RivetMessageEvent) => { - if ( - isHibernatable && - this.#maybeRespondToHibernatableAckStateProbe( - websocket, - event.data, - gatewayIdBuf, - requestIdBuf, - ) - ) { - return; - } - - const currentActor = this.#actors.get(actorId)?.actor; - const actorForDispatch = - currentActor && isStaticActorInstance(currentActor) - ? currentActor - : actor; - const connForDispatch = - isHibernatable && actorForDispatch - ? (actorForDispatch.connectionManager.findHibernatableConn( - gatewayIdBuf, - requestIdBuf, - ) ?? conn) - : conn; - - if (actorForDispatch?.isStopping) { - logger().debug({ - msg: "ignoring ws message, actor is stopping", - connId: connForDispatch?.id, - actorId: actorForDispatch?.id, - messageIndex: event.rivetMessageIndex, - }); - if ( - !isRawWebSocketPath && - websocket.readyState !== websocket.CLOSED - ) { - websocket.close(1011, "actor.stopping"); - } - return; - } - - const run = async () => { - // Process message - if ( - isHibernatable && - typeof event.rivetMessageIndex === "number" - ) { - this.#recordInboundHibernatableWebSocketMessage( - gatewayIdBuf, - requestIdBuf, - event.rivetMessageIndex, - ); - } - wsHandler.onMessage(event, wsContext); - - // Runtime-owned hibernatable websocket bookkeeping lives on the - // actor instance so static and dynamic paths share the same logic. - if (connForDispatch && actorForDispatch) { - actorForDispatch.handleInboundHibernatableWebSocketMessage( - connForDispatch, - event.data, - event.rivetMessageIndex, - ); - } - }; - - if (isRawWebSocketPath && actorForDispatch) { - void actorForDispatch.internalKeepAwake(run); - } else { - void run(); - } - }; - const attachMessageListener = () => { - websocket.addEventListener("message", handleMessageEvent); - }; - let postOpenListenersAttached = false; - const attachPostOpenListeners = () => { - if (postOpenListenersAttached) { - return; - } - postOpenListenersAttached = true; - - if (!isRawWebSocketPath) { - attachMessageListener(); - } - - websocket.addEventListener("close", (event) => { - if (isRawWebSocketPath && actor) { - void actor.internalKeepAwake(async () => { - await Promise.resolve(); - wsHandler.onClose(event, wsContext); - }); - } else { - wsHandler.onClose(event, wsContext); - } - if (isHibernatable) { - this.#deleteHibernatableWebSocketAckState( - gatewayIdBuf, - requestIdBuf, - ); - unregisterRemoteHibernatableWebSocketAckHooks( - remoteAckHookToken, - this.#config.test.enabled, - ); - } - }); - - websocket.addEventListener("error", (event) => { - wsHandler.onError(event, wsContext); - }); - }; - - websocket.addEventListener("open", (event) => { - if (isRawWebSocketPath) { - attachMessageListener(); - } - - // Attach close and error listeners before onOpen so an actor that - // immediately rejects the connection does not lose its close event. - attachPostOpenListeners(); - - wsHandler.onOpen(event, wsContext); - }); - - if (!isRawWebSocketPath) { - attachPostOpenListeners(); - } - } - - async #runnerDynamicWebSocket( - actorId: string, - websocket: UniversalWebSocket, - gatewayIdBuf: ArrayBuffer, - requestIdBuf: ArrayBuffer, - requestPath: string, - requestHeaders: Record, - encoding: Encoding, - connParams: unknown, - isHibernatable: boolean, - isRestoringHibernatable: boolean, - ): Promise { - let runtime: DynamicActorIsolateRuntime; - const remoteAckHookToken = - parseWebSocketProtocols( - requestHeaders["sec-websocket-protocol"] ?? undefined, - ).ackHookToken ?? - new URL(`http://actor${requestPath}`).searchParams.get( - REMOTE_ACK_HOOK_QUERY_PARAM, - ) ?? - undefined; - if (isHibernatable) { - this.#registerHibernatableWebSocketAckTestHooks( - websocket, - gatewayIdBuf, - requestIdBuf, - remoteAckHookToken, - ); - try { - await this.#bindDynamicHibernatableRunnerWebSocket( - { - actorId, - websocket, - requestPath, - requestHeaders, - encoding, - connParams, - gatewayId: gatewayIdBuf, - requestId: requestIdBuf, - remoteAckHookToken, - }, - isRestoringHibernatable, - ); - } catch (error) { - const { group, code } = deconstructError(error, false); - logger().error({ - msg: "failed to bind dynamic hibernatable websocket", - actorId, - error: stringifyError(error), - }); - websocket.close(1011, `${group}.${code}`); - } - return; - } - - try { - runtime = this.#requireDynamicRuntime(actorId); - } catch (error) { - logger().error({ - msg: "dynamic runtime missing for websocket", - actorId, - error: stringifyError(error), - }); - websocket.close(1011, "dynamic.runtime_missing"); - return; - } - - let proxyToActorWs: UniversalWebSocket; - try { - proxyToActorWs = await runtime.openWebSocket( - requestPath, - encoding, - connParams, - { - headers: requestHeaders, - gatewayId: gatewayIdBuf, - requestId: requestIdBuf, - isHibernatable, - isRestoringHibernatable, - }, - ); - } catch (error) { - const { group, code } = deconstructError(error, false); - logger().error({ - msg: "failed to open dynamic websocket", - actorId, - error: stringifyError(error), - }); - websocket.close(1011, `${group}.${code}`); - return; - } - - proxyToActorWs.addEventListener( - "message", - (event: RivetMessageEvent) => { - if (websocket.readyState !== websocket.OPEN) { - return; - } - websocket.send(event.data as any); - }, - ); - - proxyToActorWs.addEventListener("close", (event) => { - if (isHibernatable && event.reason === "dynamic.runtime.disposed") { - logger().debug({ - msg: "ignoring dynamic runtime dispose close for hibernatable websocket", - actorId, - code: event.code, - reason: event.reason, - }); - return; - } - if (websocket.readyState !== websocket.CLOSED) { - websocket.close(event.code, event.reason); - } - }); - - proxyToActorWs.addEventListener("error", (_event) => { - if (websocket.readyState !== websocket.CLOSED) { - websocket.close(1011, "dynamic.websocket_error"); - } - }); - - websocket.addEventListener("message", (event: RivetMessageEvent) => { - if ( - isHibernatable && - this.#maybeRespondToHibernatableAckStateProbe( - websocket, - event.data, - gatewayIdBuf, - requestIdBuf, - ) - ) { - return; - } - - const actorHandler = this.#actors.get(actorId); - if (actorHandler?.actor?.isStopping) { - return; - } - if (isHibernatable && typeof event.rivetMessageIndex === "number") { - this.#recordInboundHibernatableWebSocketMessage( - gatewayIdBuf, - requestIdBuf, - event.rivetMessageIndex, - ); - } - void runtime - .forwardIncomingWebSocketMessage( - proxyToActorWs, - event.data as any, - event.rivetMessageIndex, - ) - .catch((error) => { - logger().error({ - msg: "failed forwarding websocket message to dynamic actor", - actorId, - error: stringifyError(error), - }); - websocket.close(1011, "dynamic.websocket_forward_failed"); - }); - }); - - websocket.addEventListener("close", (event) => { - if (proxyToActorWs.readyState !== proxyToActorWs.CLOSED) { - proxyToActorWs.close(event.code, event.reason); - } - }); - - websocket.addEventListener("error", () => { - if (proxyToActorWs.readyState !== proxyToActorWs.CLOSED) { - proxyToActorWs.close(1011, "dynamic.gateway_error"); - } - }); - } - - // MARK: - Hibernating WebSockets - #hwsCanHibernate( - actorId: string, - gatewayId: ArrayBuffer, - requestId: ArrayBuffer, - request: Request, - ): boolean { - const url = new URL(request.url); - const path = url.pathname; - - // Resolve actor name from either the envoy's actor view or the local - // handler. WebSocket opens can race with actor startup, so the local - // handler may know the actor name slightly earlier than the envoy. - const actorInstance = this.#envoy.getActor(actorId); - const handler = this.#actors.get(actorId); - const actorName = - actorInstance && - "config" in actorInstance && - actorInstance.config && - typeof actorInstance.config === "object" && - "name" in actorInstance.config && - typeof actorInstance.config.name === "string" - ? actorInstance.config.name - : handler?.actorName; - if (!actorName) { - logger().warn({ - msg: "actor name unavailable in #hwsCanHibernate", - actorId, - }); - return false; - } - - // Determine configuration for new WS - logger().debug({ - msg: "no existing hibernatable websocket found", - gatewayId: Buffer.from(gatewayId).toString("hex"), - requestId: Buffer.from(requestId).toString("hex"), - }); - if (path === PATH_CONNECT) { - return true; - } else if ( - path === PATH_WEBSOCKET_BASE || - path.startsWith(PATH_WEBSOCKET_PREFIX) - ) { - // Find actor config - // Hibernation capability is a definition-level property, so the - // envoy can decide it before the actor has fully started. - const definition = lookupInRegistry(this.#config, actorName); - - // Check if can hibernate - const canHibernateWebSocket = - definition.config.options?.canHibernateWebSocket; - if (canHibernateWebSocket === true) { - return true; - } else if (typeof canHibernateWebSocket === "function") { - try { - // Truncate the path to match the behavior on onRawWebSocket - const newPath = truncateRawWebSocketPathPrefix( - url.pathname, - ); - const truncatedRequest = new Request( - `http://actor${newPath}`, - request, - ); - - const canHibernate = - canHibernateWebSocket(truncatedRequest); - return canHibernate; - } catch (error) { - logger().error({ - msg: "error calling canHibernateWebSocket", - error, - }); - return false; - } - } else { - return false; - } - } else if (path === PATH_INSPECTOR_CONNECT) { - return false; - } else { - logger().warn({ - msg: "unexpected path for getActorHibernationConfig", - path, - }); - return false; - } - } - - async #hwsLoadAll( - actorId: string, - ): Promise { - const actor = await this.loadActor(actorId); - if (!isStaticActorInstance(actor)) { - const runtime = this.#dynamicRuntimes.get(actorId); - if (!runtime) { - return []; - } - const entries = await runtime.getHibernatingWebSockets(); - return entries.map((entry) => ({ - gatewayId: entry.gatewayId, - requestId: entry.requestId, - rivetMessageIndex: entry.serverMessageIndex, - envoyMessageIndex: entry.clientMessageIndex, - path: entry.path, - headers: entry.headers, - })); - } - return actor.getHibernatingWebSocketMetadata().map((entry) => ({ - gatewayId: entry.gatewayId, - requestId: entry.requestId, - rivetMessageIndex: entry.serverMessageIndex, - envoyMessageIndex: entry.clientMessageIndex, - path: entry.path, - headers: entry.headers, - })); - } - - async onBeforeActorStart(actor: AnyStaticActorInstance): Promise { - // Resolve promise if waiting. - // - // The websocket restore path needs to be able to load the actor while - // rebinding persisted sockets, so this promise cannot wait on restore. - const handler = this.#actors.get(actor.id); - invariant(handler, "missing actor handler in onBeforeActorReady"); - handler.actorStartError = undefined; - handler.actorStartPromise?.resolve(); - handler.actorStartPromise = undefined; - - await this.#rebindHibernatableConnectSockets(actor.id); - - // Restore hibernating requests - const metaEntries = await this.#hwsLoadAll(actor.id); - await this.#envoy.restoreHibernatingRequests(actor.id, metaEntries); - } -} diff --git a/rivetkit-typescript/packages/rivetkit/src/dynamic/instance.ts b/rivetkit-typescript/packages/rivetkit/src/dynamic/instance.ts index 2707c867ce..741f68eaf5 100644 --- a/rivetkit-typescript/packages/rivetkit/src/dynamic/instance.ts +++ b/rivetkit-typescript/packages/rivetkit/src/dynamic/instance.ts @@ -18,10 +18,6 @@ export class DynamicActorInstance { await this.runtime.onAlarm(); } - async cleanupPersistedConnections(reason?: string): Promise { - return await this.runtime.cleanupPersistedConnections(reason); - } - async getHibernatingWebSockets() { return await this.runtime.getHibernatingWebSockets(); } diff --git a/rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts b/rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts index 3562516d60..7d96bdfb01 100644 --- a/rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts @@ -50,10 +50,6 @@ export class DynamicActorIsolateRuntime { async onAlarm(): Promise {} - async cleanupPersistedConnections(_reason?: string): Promise { - return 0; - } - async getHibernatingWebSockets(): Promise< DynamicHibernatingWebSocketMetadata[] > { diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-api/actors.ts b/rivetkit-typescript/packages/rivetkit/src/engine-api/actors.ts index b92b87273b..898fe6a210 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-api/actors.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-api/actors.ts @@ -16,11 +16,6 @@ export const ActorSchema = z.object({ }); export type Actor = z.infer; -export const ActorNameSchema = z.object({ - metadata: z.record(z.string(), z.unknown()), -}); -export type ActorName = z.infer; - // MARK: GET /actors export const ActorsListResponseSchema = z.object({ actors: z.array(ActorSchema), @@ -67,17 +62,3 @@ export type ActorsGetOrCreateResponse = z.infer< // MARK: DELETE /actors/{} export const ActorsDeleteResponseSchema = z.object({}); export type ActorsDeleteResponse = z.infer; - -// MARK: GET /actors/names -export const ActorsListNamesResponseSchema = z.object({ - names: z.record(z.string(), ActorNameSchema), -}); -export type ActorsListNamesResponse = z.infer< - typeof ActorsListNamesResponseSchema ->; - -// MARK: GET /actors/{actor_id}/kv/keys/{key} -export const ActorsKvGetResponseSchema = z.object({ - value: z.string().nullable(), -}); -export type ActorsKvGetResponse = z.infer; diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/api-endpoints.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/api-endpoints.ts index 6ebd541395..3e945c17b8 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/api-endpoints.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/api-endpoints.ts @@ -107,49 +107,6 @@ export async function getDatacenters( return apiCall(config, "GET", `/datacenters`); } -// MARK: Get runner configs -export interface RunnerConfig { - normal?: { - drain_on_version_upgrade?: boolean; - actor_eviction_period?: number; - actor_eviction_rate?: number; - }; - serverless?: { - url: string; - headers: Record; - drain_grace_period?: number; - max_runners: number; - min_runners: number; - request_lifespan: number; - runners_margin: number; - slots_per_runner: number; - metadata_poll_interval?: number; - drain_on_version_upgrade?: boolean; - actor_eviction_period?: number; - actor_eviction_rate?: number; - }; - protocol_version?: number; -} - -export interface RunnerConfigDatacenters { - datacenters: Record; -} - -export interface RunnerConfigsResponse { - runner_configs: Record; -} - -export async function getRunnerConfig( - config: ClientConfig, - name: string, -): Promise { - return apiCall( - config, - "GET", - `/runner-configs?runner_name=${name}`, - ); -} - // MARK: Update runner config export interface RegistryConfigRequest { datacenters: Record< diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts index c488c7d971..1c45b82621 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/envoy.ts @@ -29,5 +29,4 @@ export const EnvoyConfigSchema = z.object({ totalSlots: z.number().default(() => getRivetTotalSlots() ?? 100000), envoyKey: z.string().optional(), }); -export type EnvoyConfigInput = z.input; export type EnvoyConfig = z.infer; diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts b/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts index 7064be4665..fb679e5cd5 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/config/serverless.ts @@ -62,5 +62,4 @@ export const ServerlessConfigSchema = z.object({ // cannot use different namespaces. The namespace is extracted from the // publicEndpoint URL auth syntax if provided. }); -export type ServerlessConfigInput = z.input; export type ServerlessConfig = z.infer; diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/process-metrics.ts b/rivetkit-typescript/packages/rivetkit/src/registry/process-metrics.ts deleted file mode 100644 index aae0a5b071..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/registry/process-metrics.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Node.js runtime health metrics. - * - * Collects JS-internal data (event loop lag, GC, heap, libuv handles, - * event loop utilization, CPU) using Node built-ins (`node:perf_hooks`, - * `process`, `node:v8`, `PerformanceObserver`) and pushes them across NAPI - * into Rust-side prometheus collectors registered with - * `rivet_metrics::REGISTRY` so they appear on the existing `/metrics` - * endpoint. - * - * All data collection happens here in TypeScript. The NAPI bridge is pure - * type marshalling and the Rust side only registers + stores the metrics. - */ -import { - monitorEventLoopDelay, - PerformanceObserver, - performance, -} from "node:perf_hooks"; -import { getHeapStatistics } from "node:v8"; -import * as napi from "@rivetkit/rivetkit-napi"; - -type OptionalProcessMetricsNapi = typeof napi & { - jsObserveGcDuration?: (kind: string, durationSeconds: number) => void; - jsSetEventloopHeartbeatTsMs?: (timestampMs: number) => void; - jsSetEventloopLagQuantile?: ( - quantile: string, - valueSeconds: number, - ) => void; - jsSetEventloopUtilization?: (utilization: number) => void; - jsAddProcessCpuSeconds?: (mode: string, valueSeconds: number) => void; - jsSetProcessResidentMemoryBytes?: (bytes: number) => void; - jsSetHeapBytes?: (kind: string, bytes: number) => void; - jsSetActiveHandles?: (count: number) => void; - jsSetActiveRequests?: (count: number) => void; -}; - -type GcPerformanceEntry = { - duration: number; - detail?: { kind?: number }; - kind?: number; -}; - -const processMetricsNapi = napi as OptionalProcessMetricsNapi; - -// Some napi process-metrics symbols may be missing on older native binaries -// (the auto-generated index.js destructures them as `undefined` if the -// underlying `.node` was built before they were added). Guard each call so -// the metrics collection runs as a no-op instead of throwing -// `TypeError: napi.jsXxx is not a function` on every interval tick. -function callIfFn( - fn: ((...args: T) => void) | undefined, - ...args: T -): void { - if (typeof fn === "function") { - fn(...args); - } -} - -const SCRAPE_INTERVAL_MS = 5_000; -const HEARTBEAT_INTERVAL_MS = 100; -const EVENTLOOP_DELAY_RESOLUTION_MS = 20; -const NS_PER_SECOND = 1e9; -const US_PER_SECOND = 1e6; - -// V8 GC kind bitfield from Node's perf_hooks documentation. A `gc` performance -// entry's `kind` field is one of these values. -const GC_KIND_NAMES: Record = { - 1: "minor", - 2: "major", - 4: "incremental", - 8: "weakcb", -}; - -interface ProcessMetricsState { - scrapeInterval: NodeJS.Timeout; - heartbeatInterval: NodeJS.Timeout; - gcObserver: PerformanceObserver; - eventLoopHistogram: ReturnType; - lastCpuUsage: NodeJS.CpuUsage; - lastEventLoopUtilization: ReturnType< - typeof performance.eventLoopUtilization - >; -} - -let state: ProcessMetricsState | undefined; - -export function startProcessMetrics(): void { - if (state) { - return; - } - - const eventLoopHistogram = monitorEventLoopDelay({ - resolution: EVENTLOOP_DELAY_RESOLUTION_MS, - }); - eventLoopHistogram.enable(); - - const gcObserver = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - const gcEntry = entry as GcPerformanceEntry; - const kind = gcEntry.detail?.kind ?? gcEntry.kind; - if (typeof kind !== "number") continue; - const kindName = GC_KIND_NAMES[kind]; - if (!kindName) continue; - // `entry.duration` is in milliseconds; convert to seconds. - callIfFn( - processMetricsNapi.jsObserveGcDuration, - kindName, - gcEntry.duration / 1000, - ); - } - }); - gcObserver.observe({ entryTypes: ["gc"], buffered: false }); - - const lastCpuUsage = process.cpuUsage(); - const lastEventLoopUtilization = performance.eventLoopUtilization(); - - const heartbeatInterval = setInterval(() => { - callIfFn(processMetricsNapi.jsSetEventloopHeartbeatTsMs, Date.now()); - }, HEARTBEAT_INTERVAL_MS); - heartbeatInterval.unref(); - - const scrapeInterval = setInterval(() => { - try { - collectAndPush(); - } catch { - // Collection errors must never bring down the process; metrics - // are best-effort. - } - }, SCRAPE_INTERVAL_MS); - scrapeInterval.unref(); - - state = { - scrapeInterval, - heartbeatInterval, - gcObserver, - eventLoopHistogram, - lastCpuUsage, - lastEventLoopUtilization, - }; - - // Emit one snapshot immediately so freshly-scraped instances have data. - callIfFn(processMetricsNapi.jsSetEventloopHeartbeatTsMs, Date.now()); - try { - collectAndPush(); - } catch { - // As above; best-effort. - } -} - -export function stopProcessMetrics(): void { - if (!state) { - return; - } - clearInterval(state.scrapeInterval); - clearInterval(state.heartbeatInterval); - state.gcObserver.disconnect(); - state.eventLoopHistogram.disable(); - state = undefined; -} - -function collectAndPush(): void { - if (!state) return; - - // Event loop delay quantiles. `monitorEventLoopDelay()` reports values in - // nanoseconds; convert to seconds. Reset after reading so the next window - // reflects only the new interval. - const hist = state.eventLoopHistogram; - callIfFn( - processMetricsNapi.jsSetEventloopLagQuantile, - "p50", - hist.percentile(50) / NS_PER_SECOND, - ); - callIfFn( - processMetricsNapi.jsSetEventloopLagQuantile, - "p90", - hist.percentile(90) / NS_PER_SECOND, - ); - callIfFn( - processMetricsNapi.jsSetEventloopLagQuantile, - "p99", - hist.percentile(99) / NS_PER_SECOND, - ); - callIfFn( - processMetricsNapi.jsSetEventloopLagQuantile, - "max", - hist.max / NS_PER_SECOND, - ); - hist.reset(); - - // Event loop utilization delta over the scrape window. - const nextElu = performance.eventLoopUtilization(); - const eluDelta = performance.eventLoopUtilization( - nextElu, - state.lastEventLoopUtilization, - ); - state.lastEventLoopUtilization = nextElu; - callIfFn( - processMetricsNapi.jsSetEventloopUtilization, - eluDelta.utilization, - ); - - // CPU usage delta. `process.cpuUsage()` returns microseconds. - const nextCpu = process.cpuUsage(); - const userDeltaUs = nextCpu.user - state.lastCpuUsage.user; - const systemDeltaUs = nextCpu.system - state.lastCpuUsage.system; - state.lastCpuUsage = nextCpu; - if (userDeltaUs > 0) { - callIfFn( - processMetricsNapi.jsAddProcessCpuSeconds, - "user", - userDeltaUs / US_PER_SECOND, - ); - } - if (systemDeltaUs > 0) { - callIfFn( - processMetricsNapi.jsAddProcessCpuSeconds, - "system", - systemDeltaUs / US_PER_SECOND, - ); - } - - // Memory + heap. - const mem = process.memoryUsage(); - callIfFn(processMetricsNapi.jsSetProcessResidentMemoryBytes, mem.rss); - callIfFn(processMetricsNapi.jsSetHeapBytes, "used", mem.heapUsed); - callIfFn(processMetricsNapi.jsSetHeapBytes, "total", mem.heapTotal); - const heapLimit = getHeapStatistics().heap_size_limit; - callIfFn(processMetricsNapi.jsSetHeapBytes, "limit", heapLimit); - - // libuv active handles + requests. These are unstable Node internals - // guarded behind underscore-prefixed names; if a future Node release - // removes them the try/catch above keeps the rest of the collection - // alive. - const proc = process as unknown as { - _getActiveHandles?: () => unknown[]; - _getActiveRequests?: () => unknown[]; - }; - if (typeof proc._getActiveHandles === "function") { - callIfFn( - processMetricsNapi.jsSetActiveHandles, - proc._getActiveHandles().length, - ); - } - if (typeof proc._getActiveRequests === "function") { - callIfFn( - processMetricsNapi.jsSetActiveRequests, - proc._getActiveRequests().length, - ); - } -} diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts index 35f8e4748d..6c58e2ef4d 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts @@ -1,5 +1,4 @@ import type { SqliteNativeMetrics } from "@/common/database/config"; -import type { RegistryConfig } from "./config"; declare const handleBrand: unique symbol; @@ -73,11 +72,6 @@ export interface RuntimeQueueInspectMessage { createdAtMs: number; } -export interface RuntimeQueueSendResult { - status: string; - response?: RuntimeBytes; -} - export interface RuntimeQueueNextBatchOptions { names?: string[]; count?: number; @@ -569,45 +563,3 @@ export interface CoreRuntime { callback: (event: RuntimeWebSocketEvent) => void, ): void; } - -export interface RuntimeBundle { - runtime: CoreRuntime; -} - -export async function buildServeConfig( - config: RegistryConfig, - loadEnginePath: () => Promise, - version: string, -): Promise { - if (!config.endpoint) { - throw new Error("registry endpoint is required"); - } - - const serveConfig: RuntimeServeConfig = { - version: config.envoy.version, - endpoint: config.endpoint, - token: config.token, - namespace: config.namespace, - poolName: config.envoy.poolName, - handleInspectorHttpInRuntime: true, - serverlessBasePath: config.serverless.basePath, - serverlessPackageVersion: version, - serverlessClientEndpoint: config.publicEndpoint, - serverlessClientNamespace: config.publicNamespace, - serverlessClientToken: config.publicToken, - serverlessValidateEndpoint: config.validateServerlessEndpoint, - serverlessMaxStartPayloadBytes: config.serverless.maxStartPayloadBytes, - }; - - if (config.startEngine) { - serveConfig.engineBinaryPath = await loadEnginePath(); - serveConfig.engineHost = config.engineHost; - serveConfig.enginePort = config.enginePort; - } - if (config.test?.enabled) { - serveConfig.inspectorTestToken = - process.env._RIVET_TEST_INSPECTOR_TOKEN ?? "token"; - } - - return serveConfig; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/serde.ts b/rivetkit-typescript/packages/rivetkit/src/serde.ts index a9c07f2cd9..b166928848 100644 --- a/rivetkit-typescript/packages/rivetkit/src/serde.ts +++ b/rivetkit-typescript/packages/rivetkit/src/serde.ts @@ -26,16 +26,6 @@ export function uint8ArrayToBase64(uint8Array: Uint8Array): string { return btoa(binary); } -export function encodingIsBinary(encoding: Encoding): boolean { - if (encoding === "json") { - return false; - } else if (encoding === "cbor" || encoding === "bare") { - return true; - } else { - assertUnreachable(encoding); - } -} - export function contentTypeForEncoding(encoding: Encoding): string { if (encoding === "json") { return "application/json"; @@ -60,18 +50,6 @@ export function decodeCborJsonCompat(buffer: Uint8Array): T { }) as T; } -export function wsBinaryTypeForEncoding( - encoding: Encoding, -): "arraybuffer" | "blob" { - if (encoding === "json") { - return "blob"; - } else if (encoding === "cbor" || encoding === "bare") { - return "arraybuffer"; - } else { - assertUnreachable(encoding); - } -} - export function serializeWithEncoding( encoding: Encoding, value: T, diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/crypto.ts b/rivetkit-typescript/packages/rivetkit/src/utils/crypto.ts index 453ff79d36..52474aa46a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/utils/crypto.ts +++ b/rivetkit-typescript/packages/rivetkit/src/utils/crypto.ts @@ -1,28 +1,3 @@ -/** - * Timing-safe comparison of two Uint8Arrays or strings. - * This prevents timing attacks by always comparing all bytes. - */ -export function timingSafeEqual( - a: Uint8Array | string, - b: Uint8Array | string, -): boolean { - const encoder = new TextEncoder(); - const bufferA = typeof a === "string" ? encoder.encode(a) : a; - const bufferB = typeof b === "string" ? encoder.encode(b) : b; - - // Pad to max length to avoid leaking length information - const maxLength = Math.max(bufferA.byteLength, bufferB.byteLength); - let result = bufferA.byteLength ^ bufferB.byteLength; - - for (let i = 0; i < maxLength; i++) { - const byteA = i < bufferA.byteLength ? bufferA[i] : 0; - const byteB = i < bufferB.byteLength ? bufferB[i] : 0; - result |= byteA ^ byteB; - } - - return result === 0; -} - export async function sha256Hex(value: string): Promise { if (!globalThis.crypto?.subtle) { throw new Error("Web Crypto API is required to compute SHA-256 hashes"); diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/node.ts b/rivetkit-typescript/packages/rivetkit/src/utils/node.ts index 2e70c1560b..e108d42f8d 100644 --- a/rivetkit-typescript/packages/rivetkit/src/utils/node.ts +++ b/rivetkit-typescript/packages/rivetkit/src/utils/node.ts @@ -15,7 +15,6 @@ let nodeFs: typeof import("node:fs/promises") | undefined; let nodePath: typeof import("node:path") | undefined; let nodeOs: typeof import("node:os") | undefined; let nodeChildProcess: typeof import("node:child_process") | undefined; -let nodeStream: typeof import("node:stream/promises") | undefined; let nodeUrl: typeof import("node:url") | undefined; let hasImportedDependencies = false; @@ -60,9 +59,6 @@ export function importNodeDependencies(): void { nodeChildProcess = requireFn( /* webpackIgnore: true */ "node:child_process", ); - nodeStream = requireFn( - /* webpackIgnore: true */ "node:stream/promises", - ); nodeUrl = requireFn(/* webpackIgnore: true */ "node:url"); hasImportedDependencies = true; } catch (err) { @@ -153,20 +149,8 @@ export function getNodeChildProcess(): typeof import("node:child_process") { } /** - * Gets the Node.js stream/promises module. - * @throws Error if stream/promises module is not loaded - */ -export function getNodeStream(): typeof import("node:stream/promises") { - if (!nodeStream) { - throw new Error( - "Node stream/promises module not loaded. Ensure importNodeDependencies() has been called.", - ); - } - return nodeStream; -} - -/** - * Gets the Node.js url module lazily. + * Gets the Node.js url module. + * @throws Error if url module is not loaded */ export function getNodeUrl(): typeof import("node:url") { if (!nodeUrl) { diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/router.ts b/rivetkit-typescript/packages/rivetkit/src/utils/router.ts deleted file mode 100644 index 6c80f7904d..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/utils/router.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { OpenAPIHono } from "@hono/zod-openapi"; -import type { Hono } from "hono"; -import { createMiddleware } from "hono/factory"; -import { cors } from "@/common/cors"; -import { getLogger } from "@/common/log"; -import { - handleRouteError, - handleRouteNotFound, - loggerMiddleware, -} from "@/common/router"; - -export function logger() { - return getLogger("router"); -} - -export function createRouter( - basePath: string, - builder: (app: OpenAPIHono) => void, -): { - router: Hono; - openapi: OpenAPIHono; -} { - const router = new OpenAPIHono({ strict: false }).basePath(basePath); - - router.use("*", loggerMiddleware(logger()), cors()); - - // HACK: Add Sec-WebSocket-Protocol header to fix KIT-339 - // - // Some Deno WebSocket providers do not auto-set the protocol, which - // will cause some WebSocket clients to fail - router.use( - "*", - createMiddleware(async (c, next) => { - const upgrade = c.req.header("upgrade"); - const isWebSocket = upgrade?.toLowerCase() === "websocket"; - const isGet = c.req.method === "GET"; - - if (isGet && isWebSocket) { - c.header("Sec-WebSocket-Protocol", "rivet"); - } - - await next(); - }), - ); - - builder(router); - - // Error handling - router.notFound(handleRouteNotFound); - router.onError(handleRouteError); - - return { router: router as Hono, openapi: router }; -} - -export function buildOpenApiResponses(schema: T) { - return { - 200: { - description: "Success", - content: { - "application/json": { - schema, - }, - }, - }, - 400: { - description: "User error", - }, - 500: { - description: "Internal error", - }, - }; -} - -export function buildOpenApiRequestBody(schema: T) { - return { - required: true, - content: { - "application/json": { - schema, - }, - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts b/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts index a735716966..6169a9e6e8 100644 --- a/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts +++ b/rivetkit-typescript/packages/rivetkit/src/utils/serve.ts @@ -1,39 +1,14 @@ -// TODO: Go back to dynamic import for this -import getPort from "get-port"; import type { Hono } from "hono"; import type { RegistryConfig } from "@/registry/config"; import { logger } from "@/registry/log"; import { detectRuntime, type Runtime, stringifyError } from "../utils"; -const DEFAULT_PORT = 6421; export type ServeStatic = typeof import("@hono/node-server/serve-static").serveStatic; const serveStaticLoaderPromises: Partial< Record> > = {}; -/** - * Finds a free port starting from the given port. - * - * Tries ports incrementally until a free one is found. - */ -export async function findFreePort( - startPort: number = DEFAULT_PORT, -): Promise { - // TODO: Fix this - // const getPortModule = "get-port"; - // const { default: getPort } = await import(/* webpackIgnore: true */ getPortModule); - - // Create an iterable of ports starting from startPort - function* portRange(start: number, count: number = 100): Iterable { - for (let i = 0; i < count; i++) { - yield start + i; - } - } - - return getPort({ port: portRange(startPort) }); -} - export async function crossPlatformServe( config: RegistryConfig, httpPort: number, diff --git a/rivetkit-typescript/packages/rivetkit/src/workflow/context.ts b/rivetkit-typescript/packages/rivetkit/src/workflow/context.ts index 72809666f2..2ee168436f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/workflow/context.ts +++ b/rivetkit-typescript/packages/rivetkit/src/workflow/context.ts @@ -135,7 +135,6 @@ export class ActorWorkflowContext< >; #actorAccessDepth = 0; #allowActorAccess = false; - #guardViolation = false; constructor( inner: WorkflowContextInterface, @@ -642,7 +641,6 @@ export class ActorWorkflowContext< #ensureActorAccess(feature: string): void { if (!this.#allowActorAccess) { - this.#guardViolation = true; this.#markGuardTriggered(); throw new Error( `${feature} is only available inside workflow steps`, @@ -650,12 +648,6 @@ export class ActorWorkflowContext< } } - consumeGuardViolation(): boolean { - const violated = this.#guardViolation; - this.#guardViolation = false; - return violated; - } - #markGuardTriggered(): void { try { const state = this.#runCtx.state as Record; diff --git a/rivetkit-typescript/packages/rivetkit/tests/runtime-parity.test.ts b/rivetkit-typescript/packages/rivetkit/tests/runtime-parity.test.ts index 12e508e1d3..ec89aec63d 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/runtime-parity.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/runtime-parity.test.ts @@ -285,10 +285,6 @@ function fakeWasmBindings(scenario: ParityScenario): WasmBindings { ActorContext: class {}, ConnHandle: class {}, WebSocketHandle: class {}, - bridgeRivetErrorPrefix: () => BRIDGE_RIVET_ERROR_PREFIX, - roundTripBytes: (bytes: Uint8Array) => bytes, - uint8ArrayFromBytes: (bytes: Uint8Array) => bytes, - awaitPromise: async (promise: Promise) => await promise, default: async () => {}, } as unknown as WasmBindings; } diff --git a/rivetkit-typescript/packages/rivetkit/tests/wasm-host-smoke.test.ts b/rivetkit-typescript/packages/rivetkit/tests/wasm-host-smoke.test.ts index e8163417da..fd3d4a33ee 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/wasm-host-smoke.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/wasm-host-smoke.test.ts @@ -411,10 +411,6 @@ function fakeWasmBindings( ActorContext: class {}, ConnHandle: class {}, WebSocketHandle: class {}, - bridgeRivetErrorPrefix: () => "__RIVET_ERROR_JSON__:", - roundTripBytes: (bytes: Uint8Array) => bytes, - uint8ArrayFromBytes: (bytes: Uint8Array) => bytes, - awaitPromise: async (promise: Promise) => await promise, default: async () => {}, } as unknown as WasmBindings; } diff --git a/rivetkit-typescript/packages/rivetkit/tests/wasm-runtime.test.ts b/rivetkit-typescript/packages/rivetkit/tests/wasm-runtime.test.ts index a9bf9d42d2..a29e4e8018 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/wasm-runtime.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/wasm-runtime.test.ts @@ -227,10 +227,6 @@ function fakeWasmBindings( ActorContext: class {}, ConnHandle: class {}, WebSocketHandle: class {}, - bridgeRivetErrorPrefix: () => BRIDGE_RIVET_ERROR_PREFIX, - roundTripBytes: (bytes: Uint8Array) => bytes, - uint8ArrayFromBytes: (bytes: Uint8Array) => bytes, - awaitPromise: async (promise: Promise) => await promise, default: defaultFn, } as unknown as WasmBindings; } diff --git a/rivetkit-typescript/packages/rivetkit/tsconfig.json b/rivetkit-typescript/packages/rivetkit/tsconfig.json index 4ba9ab3b75..b99f91ecd7 100644 --- a/rivetkit-typescript/packages/rivetkit/tsconfig.json +++ b/rivetkit-typescript/packages/rivetkit/tsconfig.json @@ -18,9 +18,5 @@ "include": [ "src/**/*", "runtime/index.ts" - ], - "exclude": [ - "src/actor/instance/mod.ts", - "src/drivers/engine/actor-driver.ts" ] }