Skip to content

← Table Of Contents

7. Async

AsyncRaw() / Async<T>() / DownloadAsync(sink) return a ValueTask<T>. For a server call that doesn't need the response result, use the argument-less Async(). It returns no normal completion value or transport status, delivering only a pre-start failure as an asynchronous error. The HTTP request builder has no Yield<T>() that gives back the Spot turn.

Non-Blocking Guarantee

An HTTP call completes asynchronously. The completion value is never pulled out with a blocking API to wait for the response.

public async ValueTask NotifyMatchResultAsync(ZLinkHttpClient client, MatchResult result)
{
    var ack = await client.Post($"/matches/{result.MatchId}/result")
        .Body(result)
        .Fetch<AckRes>();

    if (!ack.Accepted)
    {
        // The HTTP call succeeded, but the application rejected the result.
        throw new InvalidOperationException("match result was not accepted");
    }
}

If you don't need the response, use the one-way terminator on the server client injected via DI.

await client.Post($"/matches/{result.MatchId}/events")
    .Body(result)
    .Async(); // No normal completion value — this only starts request execution.

Choosing The Spot Turn

Fetch<T>()/Async<T>() keep the current Spot turn. While waiting for the response, the same Spot's next callback doesn't start, so use this when you need to preserve state invariants across the request.

To give back the shared Spot gate while waiting for an external HTTP response, run Async<T>() inside RunIoWorker(...) and wait on the worker call's Yield(). In the continuation after re-acquiring the gate, re-check state, since another callback may have changed Spot state.

public async ValueTask<PlayerProfile> LoadProfileAsync(
    IZLinkSpotContext context,
    ZLinkHttpServerClient client,
    string playerId)
{
    return await context
        .RunIoWorker(async workerCancellation =>
            await client.Get($"/players/{playerId}")
                .Fetch<PlayerProfile>(workerCancellation))
        .Yield(); // The worker call gives back the gate, not the HTTP client.
}

There's no public terminator that synchronously pulls out the completion value. Blocking unwrapping like .GetAwaiter().GetResult() is also not used in a framework handler.

Call Site Recommended
A state-preserving request in a framework handler await Async<T>()
Independent external I/O with gate release in a framework handler await Async<T>() inside RunIoWorker(...).Yield()
Tests, client scenarios, CLI, batch await Async<T>()

Callback Completion

The callback overload doesn't return an awaitable. Called from a Spot handler, it returns without occupying the current execution line, and the completion callback is handled as a new turn on the same execution line.

client.Get("/health").Async<HealthRes>((error, response) =>
{
    if (error is not null) return; // Confirm transport/status/decode failures.
    RecordHealth(response!.Body);  // This callback runs on a separate Spot turn.
});

Streaming Callback Location

DownloadAsync(sink)'s sink is called in the asynchronous context reading the response chunk. Don't block the thread with heavy synchronous work inside the sink — hand it off to a thread-safe queue if needed.

Next: Streaming →