From 6a52d2ee426d078ad1680d797923a55bab4aac9b Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Mon, 4 May 2026 06:41:45 -0700 Subject: [PATCH] fix(gateway): support skip ready wait routing --- .../errors/guard.invalid_header.json | 5 ++ engine/packages/guard/src/errors.rs | 12 +++ .../packages/guard/src/routing/actor_path.rs | 22 ++--- engine/packages/guard/src/routing/mod.rs | 6 +- .../guard/src/routing/pegboard_gateway/mod.rs | 83 ++++++++++++----- .../packages/guard/tests/parse_actor_path.rs | 30 +++---- .../kitchen-sink/scripts/mock-agentic-loop.ts | 16 +--- .../rivetkit/src/client/actor-common.ts | 26 ++---- .../rivetkit/src/client/actor-conn.ts | 6 +- .../rivetkit/src/client/actor-handle.ts | 14 +-- .../packages/rivetkit/src/client/config.ts | 6 +- .../src/common/actor-router-consts.ts | 7 +- .../src/engine-client/actor-http-client.ts | 11 +-- .../engine-client/actor-websocket-client.ts | 20 ++--- .../rivetkit/src/engine-client/driver.ts | 5 +- .../rivetkit/src/engine-client/mod.ts | 20 +++-- .../rivetkit/tests/actor-gateway-url.test.ts | 37 ++------ ...=> gateway-skip-ready-wait-client.test.ts} | 88 +++++++++++-------- .../remote-engine-client-public-token.test.ts | 84 ++++++++++++++---- website/src/content/docs/actors/lifecycle.mdx | 2 +- .../content/docs/actors/request-handler.mdx | 2 +- .../content/docs/actors/websocket-handler.mdx | 2 +- .../src/content/docs/clients/javascript.mdx | 6 +- 23 files changed, 295 insertions(+), 215 deletions(-) create mode 100644 engine/artifacts/errors/guard.invalid_header.json rename rivetkit-typescript/packages/rivetkit/tests/driver/{gateway-bypass-client.test.ts => gateway-skip-ready-wait-client.test.ts} (57%) diff --git a/engine/artifacts/errors/guard.invalid_header.json b/engine/artifacts/errors/guard.invalid_header.json new file mode 100644 index 0000000000..116e1d9f33 --- /dev/null +++ b/engine/artifacts/errors/guard.invalid_header.json @@ -0,0 +1,5 @@ +{ + "code": "invalid_header", + "group": "guard", + "message": "Invalid header value." +} \ No newline at end of file diff --git a/engine/packages/guard/src/errors.rs b/engine/packages/guard/src/errors.rs index 013f73fffc..20f7851cbb 100644 --- a/engine/packages/guard/src/errors.rs +++ b/engine/packages/guard/src/errors.rs @@ -13,6 +13,18 @@ pub struct MissingHeader { pub header: String, } +#[derive(RivetError, Serialize)] +#[error( + "guard", + "invalid_header", + "Invalid header value.", + "Invalid {header} header: {detail}." +)] +pub struct InvalidHeader { + pub header: String, + pub detail: String, +} + #[derive(RivetError, Serialize)] #[error( "guard", diff --git a/engine/packages/guard/src/routing/actor_path.rs b/engine/packages/guard/src/routing/actor_path.rs index a042e72c40..64dd0302d4 100644 --- a/engine/packages/guard/src/routing/actor_path.rs +++ b/engine/packages/guard/src/routing/actor_path.rs @@ -30,7 +30,7 @@ pub enum QueryActorQuery { namespace: String, name: String, key: Vec, - bypass_connectable: bool, + skip_ready_wait: bool, }, GetOrCreate { namespace: String, @@ -40,19 +40,19 @@ pub enum QueryActorQuery { input: Option>, region: Option, crash_policy: Option, - bypass_connectable: bool, + skip_ready_wait: bool, }, } impl QueryActorQuery { - pub fn bypass_connectable(&self) -> bool { + pub fn skip_ready_wait(&self) -> bool { match self { QueryActorQuery::Get { - bypass_connectable, .. + skip_ready_wait, .. } | QueryActorQuery::GetOrCreate { - bypass_connectable, .. - } => *bypass_connectable, + skip_ready_wait, .. + } => *skip_ready_wait, } } } @@ -97,8 +97,8 @@ struct RvtParams { crash_policy: Option, #[serde(default)] token: Option, - #[serde(default)] - bypass_connectable: bool, + #[serde(default, rename = "skip-ready-wait")] + skip_ready_wait: bool, } /// Parse actor routing information from path. @@ -244,7 +244,7 @@ fn extract_rvt_params(rvt_params: &[(String, String)]) -> Result { .build()); } let value = match stripped { - "bypass_connectable" => parse_query_bool(value) + "skip-ready-wait" => parse_query_bool(value) .map(serde_json::Value::Bool) .unwrap_or_else(|| serde_json::Value::String(value.clone())), _ => serde_json::Value::String(value.clone()), @@ -294,7 +294,7 @@ fn build_actor_query(name: &str, rvt: RvtParams) -> Result { namespace: rvt.namespace, name: name.to_string(), key, - bypass_connectable: rvt.bypass_connectable, + skip_ready_wait: rvt.skip_ready_wait, }) } "getOrCreate" => { @@ -319,7 +319,7 @@ fn build_actor_query(name: &str, rvt: RvtParams) -> Result { input, region: rvt.region, crash_policy, - bypass_connectable: rvt.bypass_connectable, + skip_ready_wait: rvt.skip_ready_wait, }) } other => Err(errors::QueryInvalidParams { diff --git a/engine/packages/guard/src/routing/mod.rs b/engine/packages/guard/src/routing/mod.rs index 7f5ec70706..4efa03111e 100644 --- a/engine/packages/guard/src/routing/mod.rs +++ b/engine/packages/guard/src/routing/mod.rs @@ -16,14 +16,14 @@ mod ws_health; pub(crate) const X_RIVET_TARGET: HeaderName = HeaderName::from_static("x-rivet-target"); pub(crate) const X_RIVET_TOKEN: HeaderName = HeaderName::from_static("x-rivet-token"); -pub(crate) const X_RIVET_BYPASS_CONNECTABLE: HeaderName = - HeaderName::from_static("x-rivet-bypass-connectable"); +pub(crate) const X_RIVET_SKIP_READY_WAIT: HeaderName = + HeaderName::from_static("x-rivet-skip-ready-wait"); pub(crate) const SEC_WEBSOCKET_PROTOCOL: HeaderName = HeaderName::from_static("sec-websocket-protocol"); pub(crate) const WS_PROTOCOL_TARGET: &str = "rivet_target."; pub(crate) const WS_PROTOCOL_ACTOR: &str = "rivet_actor."; pub(crate) const WS_PROTOCOL_TOKEN: &str = "rivet_token."; -pub(crate) const WS_PROTOCOL_BYPASS_CONNECTABLE: &str = "rivet_bypass_connectable"; +pub(crate) const WS_PROTOCOL_SKIP_READY_WAIT: &str = "rivet_skip_ready_wait"; /// Creates the main routing function that handles all incoming requests #[tracing::instrument(skip_all)] diff --git a/engine/packages/guard/src/routing/pegboard_gateway/mod.rs b/engine/packages/guard/src/routing/pegboard_gateway/mod.rs index 48ba2a27e4..7de614ae80 100644 --- a/engine/packages/guard/src/routing/pegboard_gateway/mod.rs +++ b/engine/packages/guard/src/routing/pegboard_gateway/mod.rs @@ -9,8 +9,8 @@ use hyper::header::HeaderName; use rivet_guard_core::{RouteConfig, RouteTarget, RoutingOutput, request_context::RequestContext}; use super::{ - SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_ACTOR, WS_PROTOCOL_BYPASS_CONNECTABLE, WS_PROTOCOL_TOKEN, - X_RIVET_BYPASS_CONNECTABLE, X_RIVET_TOKEN, actor_path::ParsedActorPath, + SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_ACTOR, WS_PROTOCOL_SKIP_READY_WAIT, WS_PROTOCOL_TOKEN, + X_RIVET_SKIP_READY_WAIT, X_RIVET_TOKEN, actor_path::ParsedActorPath, }; use crate::{ errors, @@ -70,14 +70,13 @@ pub async fn route_request_path_based_inner( tracing::debug!(?actor_path, "routing using path-based actor routing"); - let (actor_id, token, stripped_path, bypass_connectable) = match actor_path { + let (actor_id, token, stripped_path, skip_ready_wait) = match actor_path { ParsedActorPath::Direct(path) => ( Id::parse(&path.actor_id).context("invalid actor id in path")?, read_gateway_token_for_path_based(req_ctx, path.token.as_deref())? .map(ToOwned::to_owned), path.stripped_path.clone(), - // TODO: - false, + read_skip_ready_wait_for_path_based(req_ctx)?, ), ParsedActorPath::Query(path) => match resolve_query(ctx, &path.query).await? { ResolveQueryActorResult::Found { actor_id } => ( @@ -85,7 +84,7 @@ pub async fn route_request_path_based_inner( read_gateway_token_for_path_based(req_ctx, path.token.as_deref())? .map(ToOwned::to_owned), path.stripped_path.clone(), - path.query.bypass_connectable(), + path.query.skip_ready_wait(), ), ResolveQueryActorResult::Forward { dc_label } => { let peer_dc = ctx @@ -116,7 +115,7 @@ pub async fn route_request_path_based_inner( actor_id, &stripped_path, token.as_deref(), - bypass_connectable, + skip_ready_wait, ) .await .map(Some) @@ -148,7 +147,7 @@ pub async fn route_request( set_non_preflight_cors(req_ctx); // Extract actor ID and token from WebSocket protocol or HTTP headers - let (actor_id_str, token, bypass_connectable) = if req_ctx.is_websocket() { + let (actor_id_str, token, skip_ready_wait) = if req_ctx.is_websocket() { // For WebSocket, parse the sec-websocket-protocol header let protocols_header = req_ctx .headers() @@ -182,11 +181,11 @@ pub async fn route_request( .find_map(|p| p.strip_prefix(WS_PROTOCOL_TOKEN)) .map(ToOwned::to_owned); - let bypass_connectable = protocols + let skip_ready_wait = protocols .iter() - .any(|p| p == &WS_PROTOCOL_BYPASS_CONNECTABLE); + .any(|p| p == &WS_PROTOCOL_SKIP_READY_WAIT); - (actor_id, token, bypass_connectable) + (actor_id, token, skip_ready_wait) } else { // For HTTP, use headers let actor_id = req_ctx @@ -210,9 +209,9 @@ pub async fn route_request( .context("invalid x-rivet-token header")? .map(ToOwned::to_owned); - let bypass_connectable = req_ctx.headers().contains_key(X_RIVET_BYPASS_CONNECTABLE); + let skip_ready_wait = read_skip_ready_wait_header(req_ctx)?; - (actor_id.to_string(), token, bypass_connectable) + (actor_id.to_string(), token, skip_ready_wait) }; // Find actor to route to @@ -226,7 +225,7 @@ pub async fn route_request( actor_id, &stripped_path, token.as_deref(), - bypass_connectable, + skip_ready_wait, ) .await .map(Some) @@ -247,7 +246,7 @@ async fn route_request_inner( actor_id: Id, stripped_path: &str, _token: Option<&str>, - bypass_connectable: bool, + skip_ready_wait: bool, ) -> Result { // NOTE: Token validation implemented in EE @@ -323,7 +322,7 @@ async fn route_request_inner( actor_id, actor, stripped_path, - bypass_connectable, + skip_ready_wait, ready_sub2, stopped_sub2, fail_sub2, @@ -338,7 +337,7 @@ async fn route_request_inner( actor_id, actor, stripped_path, - bypass_connectable, + skip_ready_wait, ready_sub, stopped_sub, fail_sub, @@ -361,7 +360,7 @@ async fn handle_actor_v2( actor_id: Id, actor: pegboard::ops::actor::get_for_gateway::Output, stripped_path: &str, - bypass_connectable: bool, + skip_ready_wait: bool, mut ready_sub: SubscriptionHandle, mut stopped_sub: SubscriptionHandle, mut fail_sub: SubscriptionHandle, @@ -378,7 +377,7 @@ async fn handle_actor_v2( } let envoy_key = if let (Some(envoy_key), true) = - (actor.envoy_key, actor.connectable || bypass_connectable) + (actor.envoy_key, actor.connectable || skip_ready_wait) { envoy_key } else { @@ -464,7 +463,7 @@ async fn handle_actor_v1( actor_id: Id, actor: pegboard::ops::actor::get_for_gateway::Output, stripped_path: &str, - bypass_connectable: bool, + skip_ready_wait: bool, mut ready_sub: SubscriptionHandle, mut stopped_sub: SubscriptionHandle, mut fail_sub: SubscriptionHandle, @@ -490,7 +489,7 @@ async fn handle_actor_v1( } let runner_id = if let (Some(runner_id), true) = - (actor.runner_id, actor.connectable || bypass_connectable) + (actor.runner_id, actor.connectable || skip_ready_wait) { runner_id } else { @@ -555,7 +554,7 @@ async fn handle_actor_v1( actor_id, actor, stripped_path, - bypass_connectable, + skip_ready_wait, ready_sub2, stopped_sub2, fail_sub2, @@ -626,6 +625,46 @@ fn read_gateway_token_for_path_based<'a>( } } +fn read_skip_ready_wait_for_path_based(req_ctx: &RequestContext) -> Result { + if req_ctx.is_websocket() { + Ok(req_ctx + .headers() + .get(SEC_WEBSOCKET_PROTOCOL) + .and_then(|protocols| protocols.to_str().ok()) + .is_some_and(|protocols| { + protocols + .split(',') + .map(|p| p.trim()) + .any(|p| p == WS_PROTOCOL_SKIP_READY_WAIT) + })) + } else { + read_skip_ready_wait_header(req_ctx) + } +} + +fn read_skip_ready_wait_header(req_ctx: &RequestContext) -> Result { + let Some(value) = req_ctx.headers().get(X_RIVET_SKIP_READY_WAIT) else { + return Ok(false); + }; + + let value = value.to_str().context("invalid x-rivet-skip-ready-wait header")?; + parse_skip_ready_wait_bool(value).ok_or_else(|| { + crate::errors::InvalidHeader { + header: X_RIVET_SKIP_READY_WAIT.to_string(), + detail: "expected true, false, 1, or 0".to_string(), + } + .build() + }) +} + +fn parse_skip_ready_wait_bool(value: &str) -> Option { + match value { + "true" | "1" => Some(true), + "false" | "0" => Some(false), + _ => None, + } +} + /// Waits for initial delay, then periodically checks for runner pool errors. /// /// Returns `true` if the pool has an active error, `false` otherwise. diff --git a/engine/packages/guard/tests/parse_actor_path.rs b/engine/packages/guard/tests/parse_actor_path.rs index b02501943e..8a4a9083cf 100644 --- a/engine/packages/guard/tests/parse_actor_path.rs +++ b/engine/packages/guard/tests/parse_actor_path.rs @@ -39,7 +39,7 @@ fn parses_query_actor_get_paths() { "shard-2".to_string(), "alpha@beta".to_string(), ], - bypass_connectable: false, + skip_ready_wait: false, } ); } @@ -74,7 +74,7 @@ fn parses_query_actor_get_or_create_paths_with_input_and_region() { input: Some(input_bytes), region: Some("us-west-2".to_string()), crash_policy: None, - bypass_connectable: false, + skip_ready_wait: false, } ); } @@ -104,7 +104,7 @@ fn parses_query_actor_get_or_create_paths_with_multi_component_key() { input: Some(input_bytes), region: None, crash_policy: None, - bypass_connectable: false, + skip_ready_wait: false, } ); assert_eq!(path.stripped_path, "/socket"); @@ -126,7 +126,7 @@ fn parses_query_actor_get_paths_with_empty_key() { namespace: "default".to_string(), name: "lobby".to_string(), key: Vec::new(), - bypass_connectable: false, + skip_ready_wait: false, } ); assert_eq!(path.stripped_path, "/"); @@ -152,7 +152,7 @@ fn omits_key_when_not_present() { input: None, region: None, crash_policy: None, - bypass_connectable: false, + skip_ready_wait: false, } ); assert_eq!(path.stripped_path, "/"); @@ -174,7 +174,7 @@ fn parses_simple_multi_component_keys() { namespace: "default".to_string(), name: "lobby".to_string(), key: vec!["a".to_string(), "b".to_string(), "c".to_string()], - bypass_connectable: false, + skip_ready_wait: false, } ); } @@ -199,7 +199,7 @@ fn parses_crash_policy_param() { input: None, region: None, crash_policy: Some(rivet_types::actors::CrashPolicy::Restart), - bypass_connectable: false, + skip_ready_wait: false, } ); } @@ -208,8 +208,8 @@ fn parses_crash_policy_param() { } #[test] -fn parses_bypass_connectable_query_bool_strings() { - let path = "/gateway/worker/request/bypass?rvt-namespace=default&rvt-method=getOrCreate&rvt-runner=default&rvt-bypass_connectable=true"; +fn parses_skip_ready_wait_query_bool_strings() { + let path = "/gateway/worker/request/skip-ready-wait?rvt-namespace=default&rvt-method=getOrCreate&rvt-runner=default&rvt-skip-ready-wait=true"; let result = parse_actor_path(path).unwrap().unwrap(); match result { @@ -224,10 +224,10 @@ fn parses_bypass_connectable_query_bool_strings() { input: None, region: None, crash_policy: None, - bypass_connectable: true, + skip_ready_wait: true, } ); - assert_eq!(path.stripped_path, "/request/bypass"); + assert_eq!(path.stripped_path, "/request/skip-ready-wait"); } ParsedActorPath::Direct(_) => panic!("expected query actor path"), } @@ -236,10 +236,10 @@ fn parses_bypass_connectable_query_bool_strings() { #[test] fn identifies_gateway_paths_without_parsing_query_params() { assert!(is_actor_gateway_path( - "/gateway/worker/request/bypass?rvt-bypass_connectable=true" + "/gateway/worker/request/skip-ready-wait?rvt-skip-ready-wait=true" )); assert!(is_actor_gateway_path("/gateway/actor-id")); - assert!(!is_actor_gateway_path("/request/bypass")); + assert!(!is_actor_gateway_path("/request/skip-ready-wait")); assert!(!is_actor_gateway_path("/gateway//worker")); } @@ -317,7 +317,7 @@ fn handles_interleaved_rvt_and_actor_params() { namespace: "default".to_string(), name: "lobby".to_string(), key: Vec::new(), - bypass_connectable: false, + skip_ready_wait: false, } ); } @@ -341,7 +341,7 @@ fn decodes_plus_as_space_in_rvt_values() { namespace: "my ns".to_string(), name: "lobby".to_string(), key: vec!["hello world".to_string()], - bypass_connectable: false, + skip_ready_wait: false, } ); // Actor param + is preserved literally. diff --git a/examples/kitchen-sink/scripts/mock-agentic-loop.ts b/examples/kitchen-sink/scripts/mock-agentic-loop.ts index 4060ca3320..789a8f9baa 100644 --- a/examples/kitchen-sink/scripts/mock-agentic-loop.ts +++ b/examples/kitchen-sink/scripts/mock-agentic-loop.ts @@ -236,18 +236,14 @@ type BypassHandle = { fetch: ( input: string, init?: RequestInit & { - gateway?: { - skipReadyWait?: boolean; - }; + skipReadyWait?: boolean; }, ) => Promise; webSocket: ( path?: string, protocols?: string | string[], options?: { - gateway?: { - skipReadyWait?: boolean; - }; + skipReadyWait?: boolean; }, ) => Promise; }; @@ -1223,9 +1219,7 @@ async function runBypassAttempt( handle.fetch(`/bypass?probe=${encodeURIComponent(probeId)}`, { method: "GET", signal: controller.signal, - gateway: { - skipReadyWait: true, - }, + skipReadyWait: true, }), "bypass http", BYPASS_TIMEOUT_MS, @@ -1263,9 +1257,7 @@ async function runBypassAttempt( const ws = await withTimeout( handle.webSocket("/bypass", undefined, { - gateway: { - skipReadyWait: true, - }, + skipReadyWait: true, }), "bypass websocket create", BYPASS_TIMEOUT_MS, diff --git a/rivetkit-typescript/packages/rivetkit/src/client/actor-common.ts b/rivetkit-typescript/packages/rivetkit/src/client/actor-common.ts index 193e7acdc2..e9ca7e1a5f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/actor-common.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/actor-common.ts @@ -32,7 +32,6 @@ export type ActorActionFunction< ) => Promise; export interface ActorGatewayOptions { - bypassConnectable?: boolean; skipReadyWait?: boolean; } @@ -42,35 +41,22 @@ export function resolveActorGatewayOptions( defaults: ActorGatewayOptions = {}, overrides?: ActorGatewayOptions, ): ResolvedActorGatewayOptions { - const bypassConnectable = - overrides?.bypassConnectable ?? - overrides?.skipReadyWait ?? - defaults.bypassConnectable ?? - defaults.skipReadyWait ?? - false; + const skipReadyWait = overrides?.skipReadyWait ?? defaults.skipReadyWait ?? false; return { - bypassConnectable, - skipReadyWait: bypassConnectable, + skipReadyWait, }; } -export interface ActorActionOptions { - gateway?: ActorGatewayOptions; +export interface ActorActionOptions extends ActorGatewayOptions { signal?: AbortSignal; } -export interface ActorConnectOptions { - gateway?: ActorGatewayOptions; -} +export type ActorConnectOptions = ActorGatewayOptions; -export interface ActorFetchInit extends RequestInit { - gateway?: ActorGatewayOptions; -} +export type ActorFetchInit = RequestInit & ActorGatewayOptions; -export interface ActorWebSocketOptions { - gateway?: ActorGatewayOptions; -} +export type ActorWebSocketOptions = ActorGatewayOptions; /** * Maps action methods from actor definition to typed function signatures. diff --git a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts index 6988f5ca54..8f969d0c98 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts @@ -578,8 +578,8 @@ export class ActorConnRaw { async #connectWebSocket() { const params = await this.#resolveConnectionParams(); - const target = this.#gatewayOptions.bypassConnectable - ? await this.#resolveGatewayTargetForBypass() + const target = this.#gatewayOptions.skipReadyWait + ? await this.#resolveGatewayTargetForSkipReadyWait() : getGatewayTarget(this.#actorResolutionState); const ws = await this.#driver.openWebSocket( PATH_CONNECT, @@ -634,7 +634,7 @@ export class ActorConnRaw { }); } - async #resolveGatewayTargetForBypass() { + async #resolveGatewayTargetForSkipReadyWait() { if ("getForId" in this.#actorResolutionState) { return { directId: this.#actorResolutionState.getForId.actorId, diff --git a/rivetkit-typescript/packages/rivetkit/src/client/actor-handle.ts b/rivetkit-typescript/packages/rivetkit/src/client/actor-handle.ts index 16c858182d..cdd5632593 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/actor-handle.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/actor-handle.ts @@ -264,7 +264,7 @@ export class ActorHandleRaw { let useQueryTarget = false; const gatewayOptions = resolveActorGatewayOptions( this.#gatewayOptions, - opts.gateway, + opts, ); for (let attempt = 0; attempt < maxAttempts; attempt++) { @@ -586,7 +586,7 @@ export class ActorHandleRaw { getParams, this.#encoding, this.#actorResolutionState, - resolveActorGatewayOptions(this.#gatewayOptions, options.gateway), + resolveActorGatewayOptions(this.#gatewayOptions, options), ); return this.#client[CREATE_ACTOR_CONN_PROXY]( @@ -608,10 +608,12 @@ export class ActorHandleRaw { ) { const maxAttempts = this.#getDynamicQueryMaxAttempts(); let useQueryTarget = false; - const { gateway, ...requestInit } = init ?? {}; + const { skipReadyWait, ...requestInit } = init ?? {}; const gatewayOptions = resolveActorGatewayOptions( this.#gatewayOptions, - gateway, + { + skipReadyWait, + }, ); for (let attempt = 0; attempt < maxAttempts; attempt++) { @@ -820,9 +822,9 @@ export class ActorHandleRaw { const params = await this.#resolveConnectionParams(); const gatewayOptions = resolveActorGatewayOptions( this.#gatewayOptions, - options.gateway, + options, ); - const target = gatewayOptions.bypassConnectable + const target = gatewayOptions.skipReadyWait ? await this.#resolveActionTarget(false) : getGatewayTarget(this.#actorResolutionState); return await rawWebSocket( diff --git a/rivetkit-typescript/packages/rivetkit/src/client/config.ts b/rivetkit-typescript/packages/rivetkit/src/client/config.ts index 5cc956013c..0999bbb902 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/config.ts @@ -72,10 +72,10 @@ export const ClientConfigSchemaBase = z.object({ gateway: z .object({ - bypassConnectable: z.boolean().optional().default(false), + skipReadyWait: z.boolean().optional().default(false), }) .optional() - .default(() => ({ bypassConnectable: false })), + .default(() => ({ skipReadyWait: false })), // See RunConfig.getUpgradeWebSocket // @@ -154,7 +154,7 @@ export function convertRegistryConfigToClientConfig( namespace: config.namespace, poolName: config.envoy.poolName, headers: config.headers, - gateway: { bypassConnectable: false }, + gateway: { skipReadyWait: false }, encoding: "bare", getUpgradeWebSocket: undefined, // We don't need health checks for internal clients diff --git a/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts b/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts index 2194a2fecd..c7dadeedf9 100644 --- a/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts +++ b/rivetkit-typescript/packages/rivetkit/src/common/actor-router-consts.ts @@ -21,8 +21,7 @@ export const HEADER_RIVET_TOKEN = "x-rivet-token"; export const HEADER_RIVET_TARGET = "x-rivet-target"; export const HEADER_RIVET_ACTOR = "x-rivet-actor"; export const HEADER_RIVET_NAMESPACE = "x-rivet-namespace"; -export const HEADER_RIVET_BYPASS_CONNECTABLE = - "x-rivet-bypass-connectable"; +export const HEADER_RIVET_SKIP_READY_WAIT = "x-rivet-skip-ready-wait"; // MARK: WebSocket Protocol Prefixes /** Some servers (such as node-ws & Cloudflare) require explicitly match a certain WebSocket protocol. This gives us a static protocol to match against. */ @@ -32,7 +31,7 @@ export const WS_PROTOCOL_ACTOR = "rivet_actor."; export const WS_PROTOCOL_ENCODING = "rivet_encoding."; export const WS_PROTOCOL_CONN_PARAMS = "rivet_conn_params."; export const WS_PROTOCOL_TOKEN = "rivet_token."; -export const WS_PROTOCOL_BYPASS_CONNECTABLE = "rivet_bypass_connectable"; +export const WS_PROTOCOL_SKIP_READY_WAIT = "rivet_skip_ready_wait"; export const WS_PROTOCOL_TEST_ACK_HOOK = "rivet_test_ack_hook."; // MARK: WebSocket Inline Test Protocol Prefixes @@ -54,5 +53,5 @@ export const ALLOWED_PUBLIC_HEADERS = [ HEADER_RIVET_ACTOR, HEADER_RIVET_NAMESPACE, HEADER_RIVET_TOKEN, - HEADER_RIVET_BYPASS_CONNECTABLE, + HEADER_RIVET_SKIP_READY_WAIT, ]; diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts index 4783400d22..3f5f5e8ad7 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-http-client.ts @@ -1,14 +1,11 @@ import type { ClientConfig } from "@/client/config"; import { HEADER_RIVET_ACTOR, - HEADER_RIVET_BYPASS_CONNECTABLE, + HEADER_RIVET_SKIP_READY_WAIT, HEADER_RIVET_TARGET, HEADER_RIVET_TOKEN, } from "@/common/actor-router-consts"; -import { - shouldBypassConnectable, - type GatewayRequestOptions, -} from "./driver"; +import { shouldSkipReadyWait, type GatewayRequestOptions } from "./driver"; export interface HttpGatewayRequestOptions extends GatewayRequestOptions { directActorId?: string; @@ -82,8 +79,8 @@ function buildGuardHeaders( headers.set(HEADER_RIVET_TARGET, "actor"); headers.set(HEADER_RIVET_ACTOR, options.directActorId); } - if (shouldBypassConnectable(options)) { - headers.set(HEADER_RIVET_BYPASS_CONNECTABLE, "1"); + if (shouldSkipReadyWait(options)) { + headers.set(HEADER_RIVET_SKIP_READY_WAIT, "1"); } return headers; } diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-websocket-client.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-websocket-client.ts index 9442c988ae..34e694de2c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-websocket-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/actor-websocket-client.ts @@ -8,7 +8,7 @@ import { WS_PROTOCOL_STANDARD as WS_PROTOCOL_RIVETKIT, WS_PROTOCOL_TARGET, WS_PROTOCOL_ACTOR, - WS_PROTOCOL_BYPASS_CONNECTABLE, + WS_PROTOCOL_SKIP_READY_WAIT, WS_PROTOCOL_TEST_ACK_HOOK, WS_PROTOCOL_TOKEN, } from "@/common/actor-router-consts"; @@ -18,10 +18,7 @@ import type { ActorGatewayQuery, CrashPolicy } from "@/client/query"; import type { Encoding, UniversalWebSocket } from "@/mod"; import { encodeCborCompat, uint8ArrayToBase64 } from "@/serde"; import { combineUrlPath } from "@/utils"; -import { - shouldBypassConnectable, - type GatewayRequestOptions, -} from "./driver"; +import { shouldSkipReadyWait, type GatewayRequestOptions } from "./driver"; import { logger } from "./log"; class BufferedRemoteWebSocket implements UniversalWebSocket { @@ -272,8 +269,8 @@ export function buildActorQueryGatewayUrl( if (token !== undefined) { params.append("rvt-token", token); } - if (shouldBypassConnectable(options)) { - params.append("rvt-bypass_connectable", "true"); + if (shouldSkipReadyWait(options)) { + params.append("rvt-skip-ready-wait", "true"); } const queryString = params.toString(); @@ -378,7 +375,7 @@ export async function openWebSocketToGateway( } export function buildWebSocketProtocols( - _runConfig: ClientConfig, + runConfig: ClientConfig, encoding: Encoding, params?: unknown, ackHookToken?: string, @@ -394,9 +391,12 @@ export function buildWebSocketProtocols( if (target) { protocols.push(`${WS_PROTOCOL_TARGET}${target.target}`); protocols.push(`${WS_PROTOCOL_ACTOR}${target.actorId}`); + if (runConfig.token) { + protocols.push(`${WS_PROTOCOL_TOKEN}${runConfig.token}`); + } } - if (shouldBypassConnectable(options)) { - protocols.push(WS_PROTOCOL_BYPASS_CONNECTABLE); + if (shouldSkipReadyWait(options)) { + protocols.push(WS_PROTOCOL_SKIP_READY_WAIT); } if (params) { protocols.push( diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/driver.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/driver.ts index 83919c570d..4d83a0fb94 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/driver.ts @@ -7,14 +7,13 @@ import type { ActorQuery, CrashPolicy } from "@/client/query"; export type GatewayTarget = { directId: string } | ActorQuery; export interface GatewayRequestOptions { - bypassConnectable?: boolean; skipReadyWait?: boolean; } -export function shouldBypassConnectable( +export function shouldSkipReadyWait( options: GatewayRequestOptions = {}, ): boolean { - return options.bypassConnectable === true || options.skipReadyWait === true; + return options.skipReadyWait === true; } export interface EngineControlClient { diff --git a/rivetkit-typescript/packages/rivetkit/src/engine-client/mod.ts b/rivetkit-typescript/packages/rivetkit/src/engine-client/mod.ts index ef6eb41aea..3ac79c195b 100644 --- a/rivetkit-typescript/packages/rivetkit/src/engine-client/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/engine-client/mod.ts @@ -9,7 +9,7 @@ import { } from "@/common/actor-router-consts"; import { noopNext } from "@/common/utils"; import type { Actor as ApiActor } from "@/engine-api/actors"; -import { shouldBypassConnectable } from "@/engine-client/driver"; +import { shouldSkipReadyWait } from "@/engine-client/driver"; import type { ActorOutput, CreateInput, @@ -265,7 +265,7 @@ export class RemoteEngineControlClient implements EngineControlClient { ); const httpOptions = { ...options, - directActorId: shouldBypassConnectable(options) + directActorId: shouldSkipReadyWait(options) ? directActorIdFromTarget(target) : undefined, }; @@ -300,7 +300,7 @@ export class RemoteEngineControlClient implements EngineControlClient { params, { ...options, - directActorId: shouldBypassConnectable(options) + directActorId: shouldSkipReadyWait(options) ? directActorIdFromTarget(target) : undefined, }, @@ -425,9 +425,9 @@ export class RemoteEngineControlClient implements EngineControlClient { const endpoint = getEndpoint(this.#config); if ( - shouldBypassConnectable(options) && + shouldSkipReadyWait(options) && directActorIdFromTarget(target) && - canUseDirectBypassPath(path) + canUseDirectSkipReadyWaitPath(path) ) { return combineUrlPath(endpoint, path); } @@ -476,15 +476,19 @@ export class RemoteEngineControlClient implements EngineControlClient { } } -function canUseDirectBypassPath(path: string): boolean { +function canUseDirectSkipReadyWaitPath(path: string): boolean { return ( isActorHttpRequestPath(path) || - path === PATH_CONNECT || - path === PATH_WEBSOCKET_BASE || + isPathOrQuery(path, PATH_CONNECT) || + isPathOrQuery(path, PATH_WEBSOCKET_BASE) || path.startsWith(PATH_WEBSOCKET_PREFIX) ); } +function isPathOrQuery(path: string, basePath: string): boolean { + return path === basePath || path.startsWith(`${basePath}?`); +} + function isActorHttpRequestPath(path: string): boolean { const stripped = path.slice("/request".length); return ( diff --git a/rivetkit-typescript/packages/rivetkit/tests/actor-gateway-url.test.ts b/rivetkit-typescript/packages/rivetkit/tests/actor-gateway-url.test.ts index 1aec332f61..56f667c2a3 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/actor-gateway-url.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/actor-gateway-url.test.ts @@ -75,45 +75,26 @@ describe("gateway URL builders", () => { { skipReadyWait: true }, ); - expect(new URL(url).searchParams.get("rvt-bypass_connectable")).toBe( + expect(new URL(url).searchParams.get("rvt-skip-ready-wait")).toBe( "true", ); }); - test("serializes bypassConnectable for query routing", () => { - const url = buildActorQueryGatewayUrl( - "https://api.rivet.dev/manager", - "prod", - { - getForKey: { - name: "room", - key: ["alpha"], - }, - }, - undefined, - "/status", - undefined, - undefined, - undefined, - { bypassConnectable: true }, - ); - - expect(new URL(url).searchParams.get("rvt-bypass_connectable")).toBe( - "true", - ); - }); - - test("serializes bypassConnectable for websocket protocols", () => { + test("serializes skipReadyWait for websocket protocols", () => { const protocols = buildWebSocketProtocols( - ClientConfigSchema.parse({ endpoint: "https://api.rivet.dev" }), + ClientConfigSchema.parse({ + endpoint: "https://api.rivet.dev", + token: "public-token", + }), "json", undefined, undefined, { target: "actor", actorId: "actor-1" }, - { bypassConnectable: true }, + { skipReadyWait: true }, ); - expect(protocols).toContain("rivet_bypass_connectable"); + expect(protocols).toContain("rivet_skip_ready_wait"); + expect(protocols).toContain("rivet_token.public-token"); }); test("serializes getOrCreate queries with rvt-* params", () => { diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-bypass-client.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-skip-ready-wait-client.test.ts similarity index 57% rename from rivetkit-typescript/packages/rivetkit/tests/driver/gateway-bypass-client.test.ts rename to rivetkit-typescript/packages/rivetkit/tests/driver/gateway-skip-ready-wait-client.test.ts index ec6cb9ab15..d28e3a6862 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-bypass-client.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/gateway-skip-ready-wait-client.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "vitest"; import { describeDriverMatrix } from "./shared-matrix"; import { setupDriverTest } from "./shared-utils"; -const BYPASS_HEADER = "x-rivet-bypass-connectable"; -const BYPASS_PROTOCOL = "rivet_bypass_connectable"; +const SKIP_READY_WAIT_HEADER = "x-rivet-skip-ready-wait"; +const SKIP_READY_WAIT_PROTOCOL = "rivet_skip_ready_wait"; function websocketProtocols(headers: Record): string[] { return (headers["sec-websocket-protocol"] ?? "") @@ -12,60 +12,64 @@ function websocketProtocols(headers: Record): string[] { .filter(Boolean); } -describeDriverMatrix("Gateway Bypass Client", (driverTestConfig) => { - describe("Gateway Bypass Client", () => { - test("action calls can enable and disable gateway bypass", async (c) => { +describeDriverMatrix("Gateway Skip Ready Wait Client", (driverTestConfig) => { + describe("Gateway Skip Ready Wait Client", () => { + test("action calls can enable and disable gateway skip ready wait", async (c) => { const { client } = await setupDriverTest(c, driverTestConfig); const enabledView = client.requestAccessActor.getOrCreate([ - "action-bypass-enabled", + "action-skip-ready-wait-enabled", ]); const enabledTracking = client.requestAccessActor.getOrCreate( - ["action-bypass-enabled"], + ["action-skip-ready-wait-enabled"], { params: { trackRequest: true } }, ); await enabledTracking.action({ name: "ping", args: [], - gateway: { bypassConnectable: true }, + skipReadyWait: true, }); const enabledInfo = await enabledView.getRequestInfo(); expect( - enabledInfo.onBeforeConnect.requestHeaders[BYPASS_HEADER], + enabledInfo.onBeforeConnect.requestHeaders[ + SKIP_READY_WAIT_HEADER + ], ).toBe("1"); const disabledView = client.requestAccessActor.getOrCreate([ - "action-bypass-disabled", + "action-skip-ready-wait-disabled", ]); const disabledTracking = client.requestAccessActor.getOrCreate( - ["action-bypass-disabled"], + ["action-skip-ready-wait-disabled"], { params: { trackRequest: true } }, ); await disabledTracking.action({ name: "ping", args: [], - gateway: { bypassConnectable: false }, + skipReadyWait: false, }); const disabledInfo = await disabledView.getRequestInfo(); expect( - disabledInfo.onBeforeConnect.requestHeaders[BYPASS_HEADER], + disabledInfo.onBeforeConnect.requestHeaders[ + SKIP_READY_WAIT_HEADER + ], ).toBeUndefined(); }); - test("client gateway bypass default can be overridden per action", async (c) => { + test("client gateway skip ready wait default can be overridden per action", async (c) => { const { client } = await setupDriverTest(c, driverTestConfig, { - client: { gateway: { bypassConnectable: true } }, + client: { gateway: { skipReadyWait: true } }, }); const defaultView = client.requestAccessActor.getOrCreate([ - "client-action-bypass-default", + "client-action-skip-ready-wait-default", ]); const defaultTracking = client.requestAccessActor.getOrCreate( - ["client-action-bypass-default"], + ["client-action-skip-ready-wait-default"], { params: { trackRequest: true } }, ); @@ -73,34 +77,38 @@ describeDriverMatrix("Gateway Bypass Client", (driverTestConfig) => { const defaultInfo = await defaultView.getRequestInfo(); expect( - defaultInfo.onBeforeConnect.requestHeaders[BYPASS_HEADER], + defaultInfo.onBeforeConnect.requestHeaders[ + SKIP_READY_WAIT_HEADER + ], ).toBe("1"); const overrideView = client.requestAccessActor.getOrCreate([ - "client-action-bypass-override", + "client-action-skip-ready-wait-override", ]); const overrideTracking = client.requestAccessActor.getOrCreate( - ["client-action-bypass-override"], + ["client-action-skip-ready-wait-override"], { params: { trackRequest: true } }, ); await overrideTracking.action({ name: "ping", args: [], - gateway: { bypassConnectable: false }, + skipReadyWait: false, }); const overrideInfo = await overrideView.getRequestInfo(); expect( - overrideInfo.onBeforeConnect.requestHeaders[BYPASS_HEADER], + overrideInfo.onBeforeConnect.requestHeaders[ + SKIP_READY_WAIT_HEADER + ], ).toBeUndefined(); }); - test("connect can enable gateway bypass for its websocket", async (c) => { + test("connect can enable gateway skip ready wait for its websocket", async (c) => { const { client } = await setupDriverTest(c, driverTestConfig); const defaultConn = client.requestAccessActor - .getOrCreate(["connect-bypass-default"], { + .getOrCreate(["connect-skip-ready-wait-default"], { params: { trackRequest: true }, }) .connect(); @@ -108,31 +116,33 @@ describeDriverMatrix("Gateway Bypass Client", (driverTestConfig) => { const defaultInfo = await defaultConn.getRequestInfo(); expect( websocketProtocols(defaultInfo.onBeforeConnect.requestHeaders), - ).not.toContain(BYPASS_PROTOCOL); + ).not.toContain(SKIP_READY_WAIT_PROTOCOL); await defaultConn.dispose(); - const bypassConn = client.requestAccessActor - .getOrCreate(["connect-bypass-enabled"], { + const skipReadyWaitConn = client.requestAccessActor + .getOrCreate(["connect-skip-ready-wait-enabled"], { params: { trackRequest: true }, }) .connect(undefined, { - gateway: { bypassConnectable: true }, + skipReadyWait: true, }); - const bypassInfo = await bypassConn.getRequestInfo(); + const skipReadyWaitInfo = await skipReadyWaitConn.getRequestInfo(); expect( - websocketProtocols(bypassInfo.onBeforeConnect.requestHeaders), - ).toContain(BYPASS_PROTOCOL); - await bypassConn.dispose(); + websocketProtocols( + skipReadyWaitInfo.onBeforeConnect.requestHeaders, + ), + ).toContain(SKIP_READY_WAIT_PROTOCOL); + await skipReadyWaitConn.dispose(); }); - test("client gateway bypass default can be overridden per connect", async (c) => { + test("client gateway skip ready wait default can be overridden per connect", async (c) => { const { client } = await setupDriverTest(c, driverTestConfig, { - client: { gateway: { bypassConnectable: true } }, + client: { gateway: { skipReadyWait: true } }, }); const defaultConn = client.requestAccessActor - .getOrCreate(["client-connect-bypass-default"], { + .getOrCreate(["client-connect-skip-ready-wait-default"], { params: { trackRequest: true }, }) .connect(); @@ -140,21 +150,21 @@ describeDriverMatrix("Gateway Bypass Client", (driverTestConfig) => { const defaultInfo = await defaultConn.getRequestInfo(); expect( websocketProtocols(defaultInfo.onBeforeConnect.requestHeaders), - ).toContain(BYPASS_PROTOCOL); + ).toContain(SKIP_READY_WAIT_PROTOCOL); await defaultConn.dispose(); const overrideConn = client.requestAccessActor - .getOrCreate(["client-connect-bypass-override"], { + .getOrCreate(["client-connect-skip-ready-wait-override"], { params: { trackRequest: true }, }) .connect(undefined, { - gateway: { bypassConnectable: false }, + skipReadyWait: false, }); const overrideInfo = await overrideConn.getRequestInfo(); expect( websocketProtocols(overrideInfo.onBeforeConnect.requestHeaders), - ).not.toContain(BYPASS_PROTOCOL); + ).not.toContain(SKIP_READY_WAIT_PROTOCOL); await overrideConn.dispose(); }); }); diff --git a/rivetkit-typescript/packages/rivetkit/tests/remote-engine-client-public-token.test.ts b/rivetkit-typescript/packages/rivetkit/tests/remote-engine-client-public-token.test.ts index 0c44ab5374..1f20d69e5a 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/remote-engine-client-public-token.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/remote-engine-client-public-token.test.ts @@ -2,9 +2,13 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { ClientConfigSchema } from "@/client/config"; import { HEADER_RIVET_ACTOR, - HEADER_RIVET_BYPASS_CONNECTABLE, + HEADER_RIVET_SKIP_READY_WAIT, HEADER_RIVET_TARGET, HEADER_RIVET_TOKEN, + WS_PROTOCOL_ACTOR, + WS_PROTOCOL_SKIP_READY_WAIT, + WS_PROTOCOL_TARGET, + WS_PROTOCOL_TOKEN, } from "@/common/actor-router-consts"; import { createClient } from "@/client/mod"; import { RemoteEngineControlClient } from "@/engine-client/mod"; @@ -82,7 +86,7 @@ describe.sequential("RemoteEngineControlClient public token usage", () => { expect(actorRequest?.headers.get("x-user-header")).toBe("present"); }); - test("sets bypass connectable header for actor HTTP gateway requests", async () => { + test("sets skip ready wait header for actor HTTP gateway requests", async () => { const fetchCalls: Request[] = []; const fetchMock = vi.fn(async (input: Request | URL | string) => { const request = normalizeRequest(input); @@ -99,26 +103,28 @@ describe.sequential("RemoteEngineControlClient public token usage", () => { ); const response = await driver.sendRequest( - { directId: "actor-http-bypass" }, - new Request("http://actor/request/bypass"), - { bypassConnectable: true }, + { directId: "actor-http-skip-ready-wait" }, + new Request("http://actor/request/skip-ready-wait"), + { skipReadyWait: true }, ); expect(response.status).toBe(200); expect(fetchCalls).toHaveLength(1); const actorRequest = fetchCalls[0]; - expect(actorRequest?.url).toBe("https://api.rivet.dev/request/bypass"); + expect(actorRequest?.url).toBe( + "https://api.rivet.dev/request/skip-ready-wait", + ); expect(actorRequest?.headers.get(HEADER_RIVET_TARGET)).toBe("actor"); expect(actorRequest?.headers.get(HEADER_RIVET_ACTOR)).toBe( - "actor-http-bypass", + "actor-http-skip-ready-wait", ); - expect(actorRequest?.headers.get(HEADER_RIVET_BYPASS_CONNECTABLE)).toBe( + expect(actorRequest?.headers.get(HEADER_RIVET_SKIP_READY_WAIT)).toBe( "1", ); }); - test("handle fetch forwards bypass connectable to browser request", async () => { + test("handle fetch forwards skip ready wait to browser request", async () => { const fetchCalls: Request[] = []; const fetchMock = vi.fn(async (input: Request | URL | string) => { const request = normalizeRequest(input); @@ -131,22 +137,27 @@ describe.sequential("RemoteEngineControlClient public token usage", () => { endpoint: "https://api.rivet.dev", disableMetadataLookup: true, }); - const handle = client.getForId("mockAgenticLoop", "actor-http-bypass"); + const handle = client.getForId( + "mockAgenticLoop", + "actor-http-skip-ready-wait", + ); - const response = await handle.fetch("/bypass", { - gateway: { bypassConnectable: true }, + const response = await handle.fetch("/skip-ready-wait", { + skipReadyWait: true, }); expect(response.status).toBe(200); expect(fetchCalls).toHaveLength(1); const actorRequest = fetchCalls[0]; - expect(actorRequest?.url).toBe("https://api.rivet.dev/request/bypass"); + expect(actorRequest?.url).toBe( + "https://api.rivet.dev/request/skip-ready-wait", + ); expect(actorRequest?.headers.get(HEADER_RIVET_TARGET)).toBe("actor"); expect(actorRequest?.headers.get(HEADER_RIVET_ACTOR)).toBe( - "actor-http-bypass", + "actor-http-skip-ready-wait", ); - expect(actorRequest?.headers.get(HEADER_RIVET_BYPASS_CONNECTABLE)).toBe( + expect(actorRequest?.headers.get(HEADER_RIVET_SKIP_READY_WAIT)).toBe( "1", ); }); @@ -204,6 +215,49 @@ describe.sequential("RemoteEngineControlClient public token usage", () => { expect(sockets[0]?.url).toBe( "https://public-ws.example/manager/gateway/actor%2Fws@public-ws-token/connect", ); + + await driver.openWebSocket( + "/connect", + { directId: "actor/ws-skip-ready-wait" }, + "bare", + { room: "lobby" }, + { skipReadyWait: true }, + ); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(sockets).toHaveLength(2); + expect(sockets[1]?.url).toBe( + "https://public-ws.example/manager/connect", + ); + expect(sockets[1]?.protocols).toEqual( + expect.arrayContaining([ + `${WS_PROTOCOL_TARGET}actor`, + `${WS_PROTOCOL_ACTOR}actor/ws-skip-ready-wait`, + `${WS_PROTOCOL_TOKEN}public-ws-token`, + WS_PROTOCOL_SKIP_READY_WAIT, + ]), + ); + + await driver.openWebSocket( + "/websocket?room=lobby", + { directId: "actor/ws-query" }, + "bare", + undefined, + { skipReadyWait: true }, + ); + + expect(sockets).toHaveLength(3); + expect(sockets[2]?.url).toBe( + "https://public-ws.example/manager/websocket?room=lobby", + ); + expect(sockets[2]?.protocols).toEqual( + expect.arrayContaining([ + `${WS_PROTOCOL_TARGET}actor`, + `${WS_PROTOCOL_ACTOR}actor/ws-query`, + `${WS_PROTOCOL_TOKEN}public-ws-token`, + WS_PROTOCOL_SKIP_READY_WAIT, + ]), + ); }); }); diff --git a/website/src/content/docs/actors/lifecycle.mdx b/website/src/content/docs/actors/lifecycle.mdx index e565fe07ee..f10a7797ed 100644 --- a/website/src/content/docs/actors/lifecycle.mdx +++ b/website/src/content/docs/actors/lifecycle.mdx @@ -763,7 +763,7 @@ curl -X POST \ ### Skip Ready Wait -The gateway normally holds requests until the actor is ready. The actor is not ready during startup (before `onWake` finishes) or during the sleep grace period (while `onSleep` and `waitUntil` are running). Probes and readiness checks can opt out with `gateway.skipReadyWait` to reach the actor's `onRequest` or `onWebSocket` handler in either window. +The gateway normally holds requests until the actor is ready. The actor is not ready during startup (before `onWake` finishes) or during the sleep grace period (while `onSleep` and `waitUntil` are running). Probes and readiness checks can opt out with `skipReadyWait` to reach the actor's `onRequest` or `onWebSocket` handler in either window. See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) on the JavaScript client page for usage. diff --git a/website/src/content/docs/actors/request-handler.mdx b/website/src/content/docs/actors/request-handler.mdx index efb62d293e..04a6336eb0 100644 --- a/website/src/content/docs/actors/request-handler.mdx +++ b/website/src/content/docs/actors/request-handler.mdx @@ -253,7 +253,7 @@ The `onRequest` handler is WinterTC compliant and will work with existing librar ### Skip Ready Wait -Requests are normally held at the gateway until the actor is ready. Pass `gateway.skipReadyWait: true` on `handle.fetch()` to deliver immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details. +Requests are normally held at the gateway until the actor is ready. Pass `skipReadyWait: true` on `handle.fetch()` to deliver immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details. ## API Reference diff --git a/website/src/content/docs/actors/websocket-handler.mdx b/website/src/content/docs/actors/websocket-handler.mdx index a02dce7db7..1e3e13305d 100644 --- a/website/src/content/docs/actors/websocket-handler.mdx +++ b/website/src/content/docs/actors/websocket-handler.mdx @@ -297,7 +297,7 @@ const myActor = actor({ ### Skip Ready Wait -Connections are normally held at the gateway until the actor is ready. Pass `gateway.skipReadyWait: true` on `handle.webSocket()` to connect immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details. +Connections are normally held at the gateway until the actor is ready. Pass `skipReadyWait: true` on `handle.webSocket()` to connect immediately, including while the actor is still starting or in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence). See [Skip Ready Wait](/docs/clients/javascript#skip-ready-wait) for details. ### Async Handlers diff --git a/website/src/content/docs/clients/javascript.mdx b/website/src/content/docs/clients/javascript.mdx index ce2d2d7eaf..fb2247c2a2 100644 --- a/website/src/content/docs/clients/javascript.mdx +++ b/website/src/content/docs/clients/javascript.mdx @@ -259,7 +259,7 @@ You can also pass the endpoint without auth and provide `RIVET_NAMESPACE` and `R Requests are normally held at the gateway until the actor is ready to accept traffic. An actor is not ready while it's still starting (before `onWake` finishes) or while it's in the [sleep grace period](/docs/actors/lifecycle#shutdown-sequence) (running `onSleep`, `waitUntil`, and pending disconnects). -Pass `gateway.skipReadyWait: true` on the [low-level HTTP and WebSocket APIs](#low-level-http--websocket) to deliver immediately and reach the actor's `onRequest` / `onWebSocket` handler in either window: +Pass `skipReadyWait: true` on the [low-level HTTP and WebSocket APIs](#low-level-http--websocket) to deliver immediately and reach the actor's `onRequest` / `onWebSocket` handler in either window: ```ts @nocheck import { createClient } from "rivetkit/client"; @@ -268,11 +268,11 @@ const client = createClient(); const handle = client.chatRoom.getOrCreate(["general"]); const response = await handle.fetch("/healthz", { - gateway: { skipReadyWait: true }, + skipReadyWait: true, }); const ws = await handle.webSocket("probe", undefined, { - gateway: { skipReadyWait: true }, + skipReadyWait: true, }); ```