Skip to main content

rivetkit_client/
client.rs

1use std::{collections::HashMap, env, sync::Arc};
2
3use anyhow::{Context, Result};
4use rivetkit_shared_types::endpoint::{is_local_engine_endpoint, parse_endpoint_auth};
5use serde_json::Value as JsonValue;
6
7use crate::{
8	common::{ActorKey, EncodingKind, TransportKind},
9	handle::ActorHandle,
10	protocol::query::*,
11	remote_manager::RemoteManager,
12};
13
14#[derive(Default)]
15pub struct GetWithIdOptions {
16	pub params: Option<JsonValue>,
17}
18
19#[derive(Default)]
20pub struct GetOptions {
21	pub params: Option<JsonValue>,
22}
23
24#[derive(Default)]
25pub struct GetOrCreateOptions {
26	pub params: Option<JsonValue>,
27	pub create_in_region: Option<String>,
28	pub create_with_input: Option<JsonValue>,
29	/// Overrides the client's configured pool name for this actor if it is created.
30	pub pool_name: Option<String>,
31}
32
33#[derive(Default)]
34pub struct CreateOptions {
35	pub params: Option<JsonValue>,
36	pub region: Option<String>,
37	pub input: Option<JsonValue>,
38	/// Overrides the client's configured pool name for this actor.
39	pub pool_name: Option<String>,
40}
41
42/// Connection settings resolved by [`Client::new`] the same way RivetKit for
43/// TypeScript resolves `createClient` options.
44pub struct ClientConfig {
45	/// Rivet Engine endpoint. Supports URL auth for the namespace and token
46	/// (`https://namespace:token@api.rivet.dev`); the credentials are stripped
47	/// from the endpoint before use.
48	pub endpoint: String,
49	/// Token sent with every request. Falls back to `RIVET_TOKEN`, then to
50	/// `default` for an engine on this machine. A remote endpoint without a
51	/// token connects unauthenticated.
52	pub token: Option<String>,
53	/// Falls back to `RIVET_NAMESPACE`, then `default`.
54	pub namespace: Option<String>,
55	/// Node pool that `get_or_create` and `create` place new actors in. Falls
56	/// back to `RIVET_POOL`, then `default`.
57	pub pool_name: Option<String>,
58	pub encoding: EncodingKind,
59	pub transport: TransportKind,
60	pub headers: Option<HashMap<String, String>>,
61	pub max_input_size: Option<usize>,
62	pub disable_metadata_lookup: bool,
63}
64
65impl ClientConfig {
66	pub fn new(endpoint: impl Into<String>) -> Self {
67		Self {
68			endpoint: endpoint.into(),
69			token: None,
70			namespace: None,
71			pool_name: None,
72			encoding: EncodingKind::Bare,
73			transport: TransportKind::WebSocket,
74			headers: None,
75			max_input_size: None,
76			disable_metadata_lookup: false,
77		}
78	}
79
80	pub fn token(mut self, token: impl Into<String>) -> Self {
81		self.token = Some(token.into());
82		self
83	}
84
85	pub fn token_opt(mut self, token: Option<String>) -> Self {
86		self.token = token;
87		self
88	}
89
90	pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
91		self.namespace = Some(namespace.into());
92		self
93	}
94
95	pub fn pool_name(mut self, pool_name: impl Into<String>) -> Self {
96		self.pool_name = Some(pool_name.into());
97		self
98	}
99
100	pub fn encoding(mut self, encoding: EncodingKind) -> Self {
101		self.encoding = encoding;
102		self
103	}
104
105	pub fn transport(mut self, transport: TransportKind) -> Self {
106		self.transport = transport;
107		self
108	}
109
110	pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
111		self.headers
112			.get_or_insert_with(HashMap::new)
113			.insert(key.into(), value.into());
114		self
115	}
116
117	pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
118		self.headers = Some(headers);
119		self
120	}
121
122	pub fn max_input_size(mut self, max_input_size: usize) -> Self {
123		self.max_input_size = Some(max_input_size);
124		self
125	}
126
127	pub fn disable_metadata_lookup(mut self, disable: bool) -> Self {
128		self.disable_metadata_lookup = disable;
129		self
130	}
131}
132
133pub struct Client {
134	remote_manager: RemoteManager,
135	encoding_kind: EncodingKind,
136	transport_kind: TransportKind,
137	shutdown_tx: Arc<tokio::sync::broadcast::Sender<()>>,
138}
139
140impl Clone for Client {
141	fn clone(&self) -> Self {
142		Self {
143			remote_manager: self.remote_manager.clone(),
144			encoding_kind: self.encoding_kind,
145			transport_kind: self.transport_kind,
146			shutdown_tx: self.shutdown_tx.clone(),
147		}
148	}
149}
150
151impl std::fmt::Debug for Client {
152	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153		f.debug_struct("Client")
154			.field("encoding_kind", &self.encoding_kind)
155			.field("transport_kind", &self.transport_kind)
156			.finish_non_exhaustive()
157	}
158}
159
160/// Connection settings after endpoint URL auth, environment fallbacks, and
161/// defaults are applied.
162struct ResolvedConnection {
163	endpoint: String,
164	token: Option<String>,
165	namespace: String,
166	pool_name: String,
167}
168
169/// Mirrors `transformClientConfig` in RivetKit for TypeScript. Credentials in
170/// the endpoint URL win, then the explicit config, then `RIVET_TOKEN`,
171/// `RIVET_NAMESPACE`, and `RIVET_POOL`; giving a credential both in the URL
172/// and separately is an error rather than a silent override.
173fn resolve_connection(config: &ClientConfig) -> Result<ResolvedConnection> {
174	let parsed = parse_endpoint_auth(&config.endpoint).context("invalid endpoint")?;
175	let configured_token = config
176		.token
177		.clone()
178		.or_else(|| env::var("RIVET_TOKEN").ok());
179	let configured_namespace = config
180		.namespace
181		.clone()
182		.or_else(|| env::var("RIVET_NAMESPACE").ok());
183	if parsed.namespace.is_some() && configured_namespace.is_some() {
184		anyhow::bail!(
185			"cannot specify namespace both in the endpoint URL and as a separate config option"
186		);
187	}
188	if parsed.token.is_some() && configured_token.is_some() {
189		anyhow::bail!(
190			"cannot specify token both in the endpoint URL and as a separate config option"
191		);
192	}
193
194	// Only an engine this machine runs gets the built-in `default` token.
195	let token = match parsed.token.or(configured_token) {
196		Some(token) => Some(token),
197		None if is_local_engine_endpoint(&parsed.endpoint) => Some("default".to_owned()),
198		None => None,
199	};
200
201	Ok(ResolvedConnection {
202		endpoint: parsed.endpoint,
203		token,
204		namespace: parsed
205			.namespace
206			.or(configured_namespace)
207			.unwrap_or_else(|| "default".to_owned()),
208		pool_name: config
209			.pool_name
210			.clone()
211			.or_else(|| env::var("RIVET_POOL").ok())
212			.unwrap_or_else(|| "default".to_owned()),
213	})
214}
215
216impl Client {
217	/// Never fails. An endpoint that is not a valid URL, or a credential given
218	/// both in the endpoint URL and as a separate option, makes every request
219	/// from this client return that error instead. See [`ClientConfig`] for
220	/// how the connection settings are resolved.
221	pub fn new(config: ClientConfig) -> Self {
222		let (connection, connection_error) = match resolve_connection(&config) {
223			Ok(connection) => (connection, None),
224			Err(error) => {
225				let connection = ResolvedConnection {
226					endpoint: config.endpoint.clone(),
227					token: config.token.clone(),
228					namespace: "default".to_owned(),
229					pool_name: "default".to_owned(),
230				};
231				(connection, Some(format!("{error:#}")))
232			}
233		};
234
235		let mut remote_manager = RemoteManager::from_config(
236			connection.endpoint,
237			connection.token,
238			connection.namespace,
239			connection.pool_name,
240			config.headers,
241			config.max_input_size,
242			config.disable_metadata_lookup,
243		);
244		if let Some(error) = connection_error {
245			remote_manager = remote_manager.with_connection_error(error);
246		}
247
248		Self {
249			remote_manager,
250			encoding_kind: config.encoding,
251			transport_kind: config.transport,
252			shutdown_tx: Arc::new(tokio::sync::broadcast::channel(1).0),
253		}
254	}
255
256	pub fn from_endpoint(endpoint: impl Into<String>) -> Self {
257		Self::new(ClientConfig::new(endpoint))
258	}
259
260	fn create_handle(&self, params: Option<JsonValue>, query: ActorQuery) -> ActorHandle {
261		let handle = ActorHandle::new(
262			self.remote_manager.clone(),
263			params,
264			query,
265			self.shutdown_tx.clone(),
266			self.transport_kind,
267			self.encoding_kind,
268		);
269
270		handle
271	}
272
273	pub fn get(&self, name: &str, key: ActorKey, opts: GetOptions) -> Result<ActorHandle> {
274		let actor_query = ActorQuery::GetForKey {
275			get_for_key: GetForKeyRequest {
276				name: name.to_string(),
277				key,
278			},
279		};
280
281		let handle = self.create_handle(opts.params, actor_query);
282
283		Ok(handle)
284	}
285
286	pub fn get_for_id(&self, name: &str, actor_id: &str, opts: GetOptions) -> Result<ActorHandle> {
287		let actor_query = ActorQuery::GetForId {
288			get_for_id: GetForIdRequest {
289				name: name.to_string(),
290				actor_id: actor_id.to_string(),
291			},
292		};
293
294		let handle = self.create_handle(opts.params, actor_query);
295
296		Ok(handle)
297	}
298
299	pub fn get_or_create(
300		&self,
301		name: &str,
302		key: ActorKey,
303		opts: GetOrCreateOptions,
304	) -> Result<ActorHandle> {
305		let input = opts.create_with_input;
306		let region = opts.create_in_region;
307
308		let actor_query = ActorQuery::GetOrCreateForKey {
309			get_or_create_for_key: GetOrCreateRequest {
310				name: name.to_string(),
311				key: key,
312				input,
313				region,
314				pool_name: opts.pool_name,
315			},
316		};
317
318		let handle = self.create_handle(opts.params, actor_query);
319
320		Ok(handle)
321	}
322
323	pub async fn create(
324		&self,
325		name: &str,
326		key: ActorKey,
327		opts: CreateOptions,
328	) -> Result<ActorHandle> {
329		let input = opts.input;
330		let _region = opts.region;
331
332		let actor_id = self
333			.remote_manager
334			.create_actor(name, &key, input, opts.pool_name)
335			.await?;
336
337		let get_query = ActorQuery::GetForId {
338			get_for_id: GetForIdRequest {
339				name: name.to_string(),
340				actor_id,
341			},
342		};
343
344		let handle = self.create_handle(opts.params, get_query);
345
346		Ok(handle)
347	}
348
349	pub fn disconnect(self) {
350		drop(self)
351	}
352
353	pub fn dispose(self) {
354		self.disconnect()
355	}
356}
357
358impl Drop for Client {
359	fn drop(&mut self) {
360		// Notify all subscribers to shutdown
361		let _ = self.shutdown_tx.send(());
362	}
363}