a3s-flow 0.4.1

Durable workflow engine and Rust SDK for A3S
Documentation
use a3s_flow::{
    FlowEngine, FlowError, FlowRuntime, FlowScheduler, FlowWorker, InMemoryFlowTaskQueue,
    RuntimeCommand, StepInvocation, WorkflowInvocation, WorkflowSpec,
};
use async_trait::async_trait;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;

struct ReminderRuntime;

#[async_trait]
impl FlowRuntime for ReminderRuntime {
    async fn run_workflow(
        &self,
        invocation: WorkflowInvocation,
    ) -> a3s_flow::Result<RuntimeCommand> {
        let ctx = invocation.context();
        if ctx.wait_completed("reminder-due") {
            return Ok(ctx.complete(json!({
                "sent": true,
                "message": ctx.input()["message"],
            })));
        }

        let resume_at = ctx.input()["resumeAt"]
            .as_str()
            .ok_or_else(|| FlowError::Runtime("missing resumeAt".to_string()))?
            .parse::<DateTime<Utc>>()
            .map_err(|err| FlowError::Runtime(format!("invalid resumeAt: {err}")))?;

        Ok(ctx.wait_until("reminder-due", resume_at))
    }

    async fn run_step(&self, _invocation: StepInvocation) -> a3s_flow::Result<serde_json::Value> {
        unreachable!("reminder runtime does not schedule steps")
    }
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> a3s_flow::Result<()> {
    let now = Utc::now();
    let engine = FlowEngine::in_memory(Arc::new(ReminderRuntime));
    let queue = Arc::new(InMemoryFlowTaskQueue::new());
    let scheduler = FlowScheduler::new(engine.clone(), queue.clone());
    let worker = FlowWorker::new(engine.clone(), queue);
    let spec = WorkflowSpec::rust_embedded("examples.reminder", "0.1.0", "examples", "main");

    let run_id = engine
        .start(
            spec,
            json!({
                "message": "review the release checklist",
                "resumeAt": (now - ChronoDuration::seconds(1)).to_rfc3339(),
            }),
        )
        .await?;
    let suspended = engine.snapshot(&run_id).await?;
    println!("before_scheduler={:?}", suspended.status);
    let next_delay = scheduler.next_wakeup_delay(now).await?;
    println!(
        "next_wakeup_delay_ms={:?}",
        next_delay.map(|delay| delay.as_millis())
    );
    assert_eq!(next_delay, Some(Duration::ZERO));

    let tick = scheduler.enqueue_due_work(now).await?;
    println!(
        "scheduler_due_waits={} enqueued_tasks={}",
        tick.due_waits.len(),
        tick.enqueued_tasks
    );

    let outcomes = worker.run_until_idle().await?;
    let completed = engine.snapshot(&run_id).await?;

    println!("worker_outcomes={}", outcomes.len());
    println!("after_worker={:?}", completed.status);
    println!(
        "output={}",
        serde_json::to_string_pretty(&completed.output).unwrap()
    );
    Ok(())
}