Current section

Files

Jump to
aion_flow src aion workflow run.gleam
Raw

src/aion/workflow/run.gleam

//// workflow.run (recorded activity dispatch) + now + random (determinism bindings)
import aion/activity.{type Activity}
import aion/error
import aion/internal/activity_dispatch
import aion/internal/ffi
import gleam/float
import gleam/int
import gleam/option.{None}
/// A timestamp supplied by AD's determinism context.
///
/// The inner value is the canonical millisecond timestamp string returned by the
/// engine. Workflow code should treat it as recorded deterministic data, not as
/// a wall-clock reading.
pub opaque type Timestamp {
Timestamp(milliseconds: Int)
}
/// Return the canonical millisecond representation of a deterministic timestamp.
pub fn timestamp_to_milliseconds(timestamp: Timestamp) -> Int {
timestamp.milliseconds
}
/// Dispatch an activity through the single recorded side-effect boundary.
///
/// Plain Gleam workflow code is re-run on replay. The only recorded
/// side-effectful path exposed by this SDK is `run` (and later concurrency
/// combinators over activities); there is intentionally no generic
/// `side_effect(fn)` escape hatch. The activity input is encoded with the
/// activity's input `Codec`, the engine dispatches and records via AD, and the
/// returned payload is decoded with the output `Codec`.
pub fn run(activity_value: Activity(i, o)) -> Result(o, error.ActivityError) {
run_with_default(activity_value, None)
}
/// Dispatch an activity, supplying the workflow-level default task queue used
/// when the activity itself selects none.
///
/// Resolution precedence (activity override > workflow default > the named
/// `"default"` queue) is applied once at the engine schedule seam; this SDK
/// only carries both unresolved selections across the boundary. A `None`
/// workflow default behaves exactly as `run`.
pub fn run_with_default(
activity_value: Activity(i, o),
workflow_default_task_queue: option.Option(String),
) -> Result(o, error.ActivityError) {
activity_dispatch.run(activity_value, workflow_default_task_queue)
}
/// Return AD's recorded deterministic timestamp.
///
/// Return the current workflow execution's unique identifier.
///
/// The engine assigns a v4 UUID at workflow start and records it in the
/// `WorkflowStarted` event. This NIF reads the identifier from the
/// process's registered workflow handle, so it is stable across replay.
pub fn id() -> Result(String, error.EngineError) {
case ffi.workflow_id() {
Ok(workflow_id) -> Ok(workflow_id)
Error(raw_error) -> Error(error.EngineFailure(message: raw_error))
}
}
/// This is the only time source exposed to workflow code. Workflow authors must
/// not call wall-clock APIs such as Gleam/Erlang clocks from workflow logic, as
/// ambient time would desynchronise replay.
pub fn now() -> Result(Timestamp, error.EngineError) {
case ffi.now() {
Ok(raw_timestamp) -> parse_timestamp(raw_timestamp)
Error(raw_error) -> Error(error.EngineFailure(message: raw_error))
}
}
/// Draw a deterministic floating-point value from AD's seeded RNG.
///
/// The engine keys the RNG seed on WorkflowId + RunId so replay observes the
/// same sequence. Workflow authors must not call ambient entropy sources.
pub fn random() -> Result(Float, error.EngineError) {
case ffi.random() {
Ok(raw_random) -> parse_float(raw_random, "random")
Error(raw_error) -> Error(error.EngineFailure(message: raw_error))
}
}
/// Draw a deterministic integer in the engine-defined inclusive range.
///
/// Values come from AD's seeded RNG through the FFI boundary; no wall-clock or
/// ambient entropy binding is exposed by the SDK.
pub fn random_int(min: Int, max: Int) -> Result(Int, error.EngineError) {
case min > max {
True ->
Error(error.EngineFailure(
message: "Invalid deterministic random_int range: min is greater than max",
))
False ->
case ffi.random_int(int.to_string(min), int.to_string(max)) {
Ok(raw_random) -> parse_int(raw_random, "random_int")
Error(raw_error) -> Error(error.EngineFailure(message: raw_error))
}
}
}
fn parse_timestamp(raw: String) -> Result(Timestamp, error.EngineError) {
case int.parse(raw) {
Ok(milliseconds) -> Ok(Timestamp(milliseconds: milliseconds))
Error(_) ->
Error(error.EngineFailure(
message: "Invalid deterministic timestamp: " <> raw,
))
}
}
fn parse_float(raw: String, label: String) -> Result(Float, error.EngineError) {
case float.parse(raw) {
Ok(value) -> Ok(value)
Error(_) ->
Error(error.EngineFailure(
message: "Invalid deterministic " <> label <> ": " <> raw,
))
}
}
fn parse_int(raw: String, label: String) -> Result(Int, error.EngineError) {
case int.parse(raw) {
Ok(value) -> Ok(value)
Error(_) ->
Error(error.EngineFailure(
message: "Invalid deterministic " <> label <> ": " <> raw,
))
}
}