use durare::{params, DurableContext, DurableEngine, Result, SqliteProvider, Tx, WorkflowOptions};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Serialize, Deserialize, Clone)]
struct Transfer {
from: String,
to: String,
cents: i64,
}
#[durare::transaction]
async fn setup(ctx: &DurableContext, tx: &mut Tx<'_>) -> Result<()> {
tx.execute(
"CREATE TABLE IF NOT EXISTS account (name TEXT PRIMARY KEY, cents INTEGER)",
¶ms![],
)
.await?;
for (name, cents) in [("alice", 100_i64), ("bob", 0_i64)] {
tx.execute(
"INSERT INTO account (name, cents) VALUES (?, ?) ON CONFLICT (name) DO NOTHING",
¶ms![name, cents],
)
.await?;
}
Ok(())
}
#[durare::transaction]
async fn transfer(
ctx: &DurableContext,
tx: &mut Tx<'_>,
from: String,
to: String,
cents: i64,
) -> Result<()> {
println!(" >> moving {cents} cents from {from} to {to}");
tx.execute(
"UPDATE account SET cents = cents - ? WHERE name = ?",
¶ms![cents, from],
)
.await?;
tx.execute(
"UPDATE account SET cents = cents + ? WHERE name = ?",
¶ms![cents, to],
)
.await?;
Ok(())
}
#[durare::transaction]
async fn balance(ctx: &DurableContext, tx: &mut Tx<'_>, name: String) -> Result<i64> {
let row = tx
.query_one("SELECT cents FROM account WHERE name = ?", ¶ms![name])
.await?;
Ok(row.get::<i64>("cents"))
}
#[durare::workflow]
async fn run_transfer(ctx: DurableContext, req: Transfer) -> Result<(i64, i64)> {
setup(&ctx).await?;
transfer(&ctx, req.from.clone(), req.to.clone(), req.cents).await?;
Ok((balance(&ctx, req.from).await?, balance(&ctx, req.to).await?))
}
#[tokio::main]
async fn main() -> Result<()> {
let path = std::env::temp_dir().join(format!("durare-transfer-{}.db", uuid::Uuid::new_v4()));
let url = format!("sqlite://{}", path.display());
let engine = DurableEngine::new(Arc::new(SqliteProvider::connect(&url).await?)).await?;
engine.launch().await?;
let req = Transfer {
from: "alice".to_string(),
to: "bob".to_string(),
cents: 30,
};
println!("[run 1] xfer-1");
let (alice, bob): (i64, i64) = engine
.start_with(RunTransfer, req.clone(), WorkflowOptions::with_id("xfer-1"))
.await?
.await?;
println!(" balances: alice={alice}, bob={bob}");
println!("[run 2] xfer-1 again (same id — must replay, not re-transfer)");
let (alice2, bob2): (i64, i64) = engine
.start_with(RunTransfer, req, WorkflowOptions::with_id("xfer-1"))
.await?
.await?;
println!(" balances: alice={alice2}, bob={bob2}");
assert_eq!((alice, bob), (alice2, bob2), "re-run must be a no-op");
println!("[ok] transfer applied exactly once");
engine.shutdown(Duration::from_secs(1)).await?;
let _ = std::fs::remove_file(&path);
Ok(())
}