Task

Work that waits without blocking the frame loop.

Spawn tasks from update! or another task. Waiting effects park the task; its single return value arrives on a later Input.messages. Tasks run serially on the frame thread, so pure computation still blocks frames.

update! = |model, input, io| {
    if input.devices.key_pressed(KeyEnter) {
        Task.spawn!(
            input,
            || {
                Task.sleep!(300)
                Woke
            },
        )
    }
    Ok(model)
}

Thirty-two tasks run at once. A spawn past that is queued and started in submission order as a slot frees, so spawning never fails and no closure is dropped. At shutdown live tasks are cancelled, queued closures are dropped, and messages that were produced but never delivered are released.

spawn! : Input(msg), (() => msg) => {}

Start a task. Its message arrives on a later Input.messages, in the order the tasks finished.

Pass the current App.Input as the first argument. It pins the closure's return type to the app's Msg; it is not otherwise read.

A task closure may capture the input, so a task can spawn more tasks.

The task may start before spawn! returns or after update!; it reaches its first wait or finishes before that cycle's render!. Do not assume ordering between its synchronous work and the spawning update!. Messages are delivered in task-completion order.

When more than one task of the same kind can be running, an older reply can arrive after a newer one. Put a generation counter or id in the message and drop replies that do not match the latest; examples/http_fetch shows the shape.

This is the only way to start a task. Input is a pure platform value with no effectful methods, so there is no input.spawn! form.

Legal in update! and in tasks; refused in init! and render!. init! never sees an input for the message to arrive on, and render! does not change the world.

spawn_with! : Input(msg), (() => a), (a -> msg) => {}

Start a task whose message belongs to a component, wrapped into the app's own Msg on the way back.

spawn! needs the closure to return the app's Msg, which forces a component to know the type of the app that hosts it. spawn_with! splits that in two: the closure returns the component's own message type, and the parent supplies the constructor that converts it to the app's Msg.

# Counter.roc -- knows nothing about the app's Msg
Msg : [Loaded(U64), LoadFailed]
load! : () => Msg

# the app -- Msg : [CounterMsg(Counter.Msg), ...]
Task.spawn_with!(input, Counter.load!, |m| CounterMsg(m))

A bare tag name is not a function, so the wrapper is written as the lambda |m| CounterMsg(m) rather than as CounterMsg.

The wrapper runs on the task's own stack, right after the closure returns and before the message is returned to the host, so it is ordinary pure code and not a second scheduled step.

Everything spawn! says about the App.Input witness, about when a task's message arrives, and about which callbacks may spawn applies here unchanged.

Legal in update! and in tasks; refused in init! and render!.

sleep! : U64 => {}

Wait at least millis without stalling the frame.

Legal in init!, where it blocks startup, and in tasks, where it parks the task; refused in update! and render!.