Skip to main content

little_durable_objects/
state_transport.rs

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