Skip to main content

little_durable_objects/
state_transport.rs

1use 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}