little_durable_objects/
state_transport.rs1use anyhow::{Context, Result, ensure};
2use async_trait::async_trait;
3
4use crate::storage_urls::STATE_CONTENT_TYPE;
5
6#[derive(Debug)]
7pub struct LoadedState {
8 pub bytes: Vec<u8>,
9}
10
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum StateWrite {
13 Written,
14 AlreadyExists,
15}
16
17#[async_trait]
18pub trait StateTransport: Send + Sync {
19 async fn read(&self, signed_url: &str) -> Result<LoadedState>;
20 async fn write(&self, signed_url: &str, bytes: Vec<u8>) -> Result<StateWrite>;
21}
22
23#[derive(Clone, Default)]
24pub struct HttpStateTransport {
25 client: reqwest::Client,
26}
27
28impl HttpStateTransport {
29 pub fn new() -> Self {
30 Self {
31 client: reqwest::Client::new(),
32 }
33 }
34}
35
36#[async_trait]
37impl StateTransport for HttpStateTransport {
38 async fn read(&self, signed_url: &str) -> Result<LoadedState> {
39 validate_url(signed_url)?;
40 let response = self
41 .client
42 .get(signed_url)
43 .send()
44 .await
45 .context("read actor state through signed URL")?;
46 ensure!(
47 response.status().is_success(),
48 "actor-state read failed with HTTP {}",
49 response.status()
50 );
51 let bytes = response
52 .bytes()
53 .await
54 .context("read actor-state response body")?;
55 Ok(LoadedState {
56 bytes: bytes.to_vec(),
57 })
58 }
59
60 async fn write(&self, signed_url: &str, bytes: Vec<u8>) -> Result<StateWrite> {
61 validate_url(signed_url)?;
62 let response = self
63 .client
64 .put(signed_url)
65 .header(reqwest::header::CONTENT_TYPE, STATE_CONTENT_TYPE)
66 .body(bytes)
67 .send()
68 .await
69 .context("write actor state through signed URL")?;
70 if response.status() == reqwest::StatusCode::PRECONDITION_FAILED {
71 return Ok(StateWrite::AlreadyExists);
72 }
73 ensure!(
74 response.status().is_success(),
75 "actor-state write failed with HTTP {}",
76 response.status()
77 );
78 Ok(StateWrite::Written)
79 }
80}
81
82fn validate_url(url: &str) -> Result<()> {
83 let url = reqwest::Url::parse(url).context("parse signed actor-state URL")?;
84 ensure!(
85 matches!(url.scheme(), "http" | "https") && url.host_str().is_some(),
86 "signed actor-state URL must be HTTP or HTTPS"
87 );
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn rejects_non_http_state_urls() {
97 assert!(validate_url("file:///tmp/state").is_err());
98 }
99}