branch: main
sql_counter.rs
3854 bytesRaw
use worker::{
    durable_object, wasm_bindgen, DurableObject, Env, Request, Response, Result, SqlStorage, State,
};

/// A simple SQLite-backed counter stored in Durable Object storage.
///
/// Each Durable Object instance owns its own private SQLite database.  We keep a
/// single table `counter` with one row that stores the current value.
#[durable_object]
pub struct SqlCounter {
    sql: SqlStorage,
}

impl DurableObject for SqlCounter {
    fn new(state: State, _env: Env) -> Self {
        let sql = state.storage().sql();
        // Create table if it does not exist.  Note: `exec` is synchronous.
        sql.exec("CREATE TABLE IF NOT EXISTS counter(value INTEGER);", None)
            .expect("create table");
        Self { sql }
    }

    async fn fetch(&self, req: Request) -> Result<Response> {
        let url = req.url()?;
        let path = url.path();

        // Parse path to determine action
        if path.contains("/set-large/") {
            self.handle_set_large_value(&url)
        } else {
            self.handle_increment()
        }
    }
}

impl SqlCounter {
    fn handle_increment(&self) -> Result<Response> {
        // Read current value (if any)
        #[derive(serde::Deserialize)]
        struct Row {
            value: i32,
        }

        let rows: Vec<Row> = self
            .sql
            .exec("SELECT value FROM counter LIMIT 1;", None)?
            .to_array()?;
        let current = rows.first().map_or(0, |r| r.value);
        let next = current + 1;

        // Upsert new value – simplest way: delete and insert again.
        self.sql.exec("DELETE FROM counter;", None)?;
        self.sql
            .exec("INSERT INTO counter(value) VALUES (?);", vec![next.into()])?;

        Response::ok(format!("SQL counter is now {next}"))
    }

    fn handle_set_large_value(&self, url: &worker::Url) -> Result<Response> {
        // Extract the large value from the path /set-large/{value}
        let path = url.path();
        let value_str = path.split("/set-large/").nth(1).unwrap_or("0");

        // Parse the value as i64
        let large_value: i64 = value_str
            .parse()
            .map_err(|_| worker::Error::from("Invalid number format"))?;

        // Use try_from_i64 to safely create SqlStorageValue
        let safe_value = match worker::SqlStorageValue::try_from_i64(large_value) {
            Ok(value) => value,
            Err(e) => {
                return Response::ok(format!(
                    "Error: Cannot store value {large_value} - {e}. JavaScript safe range is ±9007199254740991"
                ));
            }
        };

        // Store the safe value
        self.sql.exec("DELETE FROM counter;", None)?;
        self.sql
            .exec("INSERT INTO counter(value) VALUES (?);", vec![safe_value])?;

        Response::ok(format!("Successfully stored large value: {large_value}"))
    }
}

#[worker::send]
/// Route handler that proxies a request to our SqlCounter Durable Object with id derived from the
/// path `/sql-counter/{name}` (so every name gets its own instance).
pub async fn handle_sql_counter(
    req: Request,
    env: Env,
    _data: super::SomeSharedData,
) -> Result<Response> {
    let uri = req.url()?;
    let mut segments = uri.path_segments().unwrap();
    // skip "sql-counter"
    let _ = segments.next();
    let name = segments.next().unwrap_or("default");

    // Build the remaining path for the durable object request
    let remaining_path: Vec<&str> = segments.collect();
    let full_path = if remaining_path.is_empty() {
        "https://fake-host/".to_string()
    } else {
        format!("https://fake-host/{}", remaining_path.join("/"))
    };

    let namespace = env.durable_object("SQL_COUNTER")?;
    let stub = namespace.id_from_name(name)?.get_stub()?;
    stub.fetch_with_str(&full_path).await
}