Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scheduling and suspension

host/browser/tasks.mjs implements the scheduler shared by runtime adapters. The Snail-Core task API and application event and storage waits use it in the CLI and browser. It uses one JavaScript execution agent and shipping JSPI; it does not create workers.

A live task is either queued, executing or suspended. The runtime keeps a set of these tasks. Dispatch inserts a child before returning to its parent. Returning or trapping removes only the completed task; independently dispatched work remains live. drain() resolves when the set is empty. Work dispatched after exhaustion begins another interval of activity.

The ready queue contains starts and resumptions. An execution permit allows only one of them to use a program’s dynamic handler globals. Short tasks normally advance through microtasks; a bounded number of steps yields an event-loop turn so input is not starved. A CPU loop that does not suspend still monopolizes this execution agent.

Handler bindings

Ordinary handlers are reusable and satisfy Send + Sync. Dispatch copies the current binding vector and gives it to the child. The implementations may be shared, but the vector belongs to that task. This is why a child can keep using a handler after its parent returns. Root entries start with runtime defaults.

Wasm handler globals act as the executing task’s registers. At a JSPI wait, the runtime saves them, restores the defaults and releases the permit. I/O completion only makes a resumption ready. The scheduler later installs that task’s bindings and resolves or rejects its wait Promise. It keeps the permit until the resumed Wasm suspends again or settles; resolving the Promise alone does not establish that Wasm has run yet.

One scheduler owns each program and all its handler globals. Runtime authors must route suspending imports through its suspension bridge. Dynamically loaded modules must appear in the handler-global enumeration too. Their compiler-generated handler globals initially contain null.

Cancellation and adapters

cancel() requests cancellation; done marks completion. Queued work that has never entered Wasm can be discarded immediately. A suspended invocation must resume with rejection and unwind before it leaves the live set. Cancellation is sticky, including when a successful I/O result has already queued a resumption.

An adapter receives an AbortSignal and returns one Promise covering both its operation and cleanup. It must handle an already aborted signal, remove listeners on normal completion, and settle only after owned cleanup finishes. Timers and event waits can disarm promptly. An operation that cannot abort may finish normally; the runtime then rejects its resumption because cancellation was requested. Its external effects are not rolled back.

suspend(operation, discard) can take a synchronous native disposer for a successful result. The suspension owns that result until the scheduler accepts delivery. If cancellation wins while the result is queued, or a non-abortable operation succeeds after cancellation, the runtime disposes the result exactly once and rejects the parked invocation. It does not dispose delivered results or operation failures. A disposer failure also rejects the parked invocation, so Wasm still unwinds before task completion.

The disposer must not suspend or reenter Snail-Core. If it starts OS resource closure, the native owner awaits that remaining cleanup during shutdown. Work must return its complete invocation Promise, and suspending imports must return or await their gate; abandoning either loses the relationship between task completion and its parked stack.

This makes cleanup part of the suspended task’s lifetime, without a second cleanup counter. A host adapter that never settles can prevent exhaustion. Runtime authors must uphold this boundary; a pending Promise is not cancellation machinery by itself.

Closing a runtime rejects new work, cancels its live tasks and waits for them to settle. A task failure does not cancel siblings. A Node host must also hold a process-lifetime token until drain: unresolved Promises alone do not keep Node alive.