Skip to content

Coming from vibe-core ​

The direct-style part should feel familiar: vibe-core already uses fibers so network and file waits can read like sequential code. The migration is primarily about task ownership, explicit results, and buffer lifetime.

This guide uses vibe-core at f0e8795495affd0d517b335e70c6f7139a6a9f5b, whose facilities include fiber-based tasks, I/O, timers, and synchronization. Its task implementation includes joining; describing all vibe-core tasks as necessarily detached would be misleading.

NOTE

Each source tab imports a complete executable tutorial with error handling and cleanup. Run it with dub run --single <file> -b checked. Contract examples linked after each group retain the behavioral assertions. ci --verify checks every displayed output and independently executes those contract counterparts. See Running the examples for prerequisites.

Choose the continuation style, not a different I/O engine ​

The six three-way groups below show vibe-core, event-horizon fibers, and event-horizon callbacks, each with its own verified output. Both EH styles submit completion operations. Callback programs create DefaultLoop directly; typed context handlers remove the raw cast, but state must still outlive every accepted operation. No callback example hides a fiber runtime.

ResponsibilityTier A: callbacksFiber style
Continue after I/Ovoid function(void*, ref Completion) nothrow @nogcResume after a verb returns an IoResult
Keep state aliveExplicit context until terminal completionLocals in a suspended fiber, owned by its scope
Start an operationCheck submit/submitAfter for admission failureCheck the operation's returned result
Observe completionCompletion.res is a value or negative errnoTyped IoResult!T; scope outcomes are separate
Own a bufferMove into the op; recover/move Completion.bufMove through a sequential I/O verb
CancelRequest cancellation, then keep driving to terminal completionCancel the owning scope and join protected cleanup
Combine operationsExplicit pending count/state machinefork/join within withScope

A submission failure produces no callback. An accepted cancellation request is not permission to free the context: the target's terminal callback still runs, and a real completion may win the race. Do not call run/runOnce from a callback. The examples keep contexts and pools alive until inFlight == 0 and print after dispatch, outside the nothrow @nogc callback boundary.

Where a callback comparison stops being equivalent ​

The channel, signal, capture and supervision sections retain their fiber programs. Tier A has raw read/wait/process-related operations, but no drop-in callback version of scheduler-local Channel, scoped SignalFd consumption, execCapture, or supervise. Rebuilding their buffering, cancellation, drain, and reaping policies would be an application adapter, not a second spelling of the same high-level API. No such equivalence is implied by the six native operation comparisons.

Map the lifetime boundary ​

For a vibe-core port, Event Horizon's fiber style preserves the familiar sequential shape. Choosing Tier A instead is an explicit move to callback state machines: locals that previously survived a cooperative wait must move into a context that outlives every operation. Neither choice proves CPU parallelism.

vibe-core patternEvent Horizon fibersEvent Horizon callbacks
Fiber-aware sleep inside a loopenv.clock.sleep inside a loopsubmitAfter rearms from its completion
Retained task handles and joiningScoped fork/join with typed outcomesExplicit fan-in state; no implicit child joining
Sequential TCP reads/writesOwning stream verbsOpRecv/OpSend and a framing state machine
Sequential file readingOwned file/buffer verbsOpen → repeated positioned read → EOF → close
Timeout around cooperative workwithDeadline plus protected cleanupDeadline callback → cancel request → terminal result → cleanup
vibe-core idiomEvent Horizon idiomWhat to review
Launch a task and retain its handlesc.fork(handle, body)Choose the lexical scope that owns the child.
Join a taskhandle.join(s)Inspect the typed outcome before accessing its value.
Fiber-aware sleepenv.clock.sleepHandle cancellation as an I/O result.
Task interruption or timeoutwithDeadline, Scope.cancelA deadline joins children; it cannot preempt CPU-bound code.
Synchronization and message passingScoped children and bounded channelsEvent Horizon channels stay on their owning scheduler.
Streams and exception-based failuresOwned buffer verbs and IoResult!TOrdinary I/O errors are values; defects still exist.

LoopGroup defaults to one loop on the calling thread. A multi-worker topology is a separate choice, not an automatic effect of using a group.

The lifecycle is common; the mechanism is not. Callback registrations need an explicit owner and terminal path. Fibers make sequential control flow possible, but still need cancellation and lifetime rules. A lexical scope is useful when it actually owns the work being started.

Timers and cadence ​

Each program prints three ticks and then returns. EH uses relative sleeps or one-shot timers, not Ticker: work between waits shifts the next deadline. Neither style promises exact timing. For absolute cadence, maintain an absolute deadline and define how missed ticks are skipped.

The callback tutorial arms a timer, drives it to completion, prints its result, and repeats. The fiber tutorial loops over sleeps. The contract version also exercises rearming directly from a completion callback.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_timers"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln;
import vibe.core.core : sleep;

void main() @system
{
    foreach (tick; 1 .. 4) { sleep(10.msecs); writeln("tick ", tick); }
}
tick 1
tick 2
tick 3
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_timers"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln, stderr;
import sparkles.event_horizon : runApplication, RootScope, Env, ioOk;

int main() @system
{
    auto result = runApplication((ref RootScope root, ref Env env) {
        foreach (tick; 1 .. 4)
        {
            auto slept = env.clock.sleep(10.msecs);
            if (slept.hasError) return slept;
            writeln("tick ", tick);
        }
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
tick 1
tick 2
tick 3
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_timers"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion;

struct Timer
{
    int ticks;
    int error;
}

void tick(ref Timer timer, ref Completion done) nothrow @nogc
{
    if (done.res < 0) timer.error = done.res;
    else ++timer.ticks;
}

int main() @system
{
    Timer timer; // Outlives the loop, including cleanup after a drive error.
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) loop.destroy();
    foreach (number; 1 .. 4)
    {
        auto armed = loop.submitAfter!tick(10.msecs, timer);
        if (armed.hasError) { stderr.writeln(armed.error); return 1; }
        auto ran = loop.run();
        if (ran.hasError) { stderr.writeln(ran.error); return 1; }
        if (timer.error) { stderr.writeln("timer failed: ", timer.error); return 1; }
        writeln("tick ", timer.ticks);
    }
    return 0;
}
tick 1
tick 2
tick 3

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

Concurrency and joined lifetime ​

Start independent work and combine its results as 1 * 10 + 2. Both fiber programs join both tasks before retrieving or reporting failures. This is concurrency, not evidence of simultaneous execution on multiple CPU cores.

Keep task handles and callback contexts alive through completion. An EH fork routes its typed failure to its join handle, not directly into the scope policy. The example explicitly promotes joined failures with root.fail. Vibe-core's Future.getResult propagates task exceptions after joining.

The callback program admits two operations before driving the loop. Their result slots provide explicit fan-in; there is no implicit lexical child scope. The assertion-heavy fiber fixture retains the ready/release handshake that proves overlap independently of timer scheduling.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_concurrency"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln;
import vibe.core.concurrency : async;
import vibe.core.core : sleep;

void main() @system
{
    auto slow = async({ sleep(20.msecs); return 1; });
    auto fast = async({ sleep(5.msecs); return 2; });
    // Join both before retrieving values, which can propagate task exceptions.
    slow.task.join();
    fast.task.join();
    writeln("joined: ", slow.getResult() * 10 + fast.getResult());
}
joined: 12
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_concurrency"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    auto result = runApplication((ref RootScope root, ref Env env) {
        JoinHandle!int slow, fast; // Slots remain pinned until both joins.
        root.fork(slow, () {
            auto slept = env.clock.sleep(20.msecs);
            return slept.hasError ? ioErr!int(slept.error) : ioOk(1);
        });
        root.fork(fast, () {
            auto slept = env.clock.sleep(5.msecs);
            return slept.hasError ? ioErr!int(slept.error) : ioOk(2);
        });
        auto a = root.join(slow);
        auto b = root.join(fast);
        if (a.hasError) root.fail(a.error);
        if (b.hasError) root.fail(b.error);
        if (!a.hasError && !b.hasError) writeln("joined: ", a.value * 10 + b.value);
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
joined: 12
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_concurrency"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion;

struct Job { int input, result, error; }
void completed(ref Job job, ref Completion done) nothrow @nogc
{
    if (done.res < 0) job.error = done.res;
    else job.result = job.input;
}

int main() @system
{
    Job[2] jobs = [Job(1), Job(2)]; // Outlive the loop and all its callbacks.
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) loop.destroy();
    foreach (ref job; jobs)
    {
        auto armed = loop.submitAfter!completed(10.msecs, job);
        if (armed.hasError) { stderr.writeln(armed.error); return 1; }
    }
    auto ran = loop.run(); // Explicit fan-in; not a structured fiber scope.
    if (ran.hasError) { stderr.writeln(ran.error); return 1; }
    foreach (job; jobs)
        if (job.error) { stderr.writeln("timer failed: ", job.error); return 1; }
    writeln("joined: ", jobs[0].result * 10 + jobs[1].result);
    return 0;
}
joined: 12

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

Cancellation, deadlines and cleanup ​

The examples distinguish timeout detection from successful completion and print whether cleanup ran. Read the foreign operation's interruption mechanism carefully: cancelling a registration, signalling a flag and interrupting a fiber are not interchangeable. A timeout on a wait does not necessarily stop the operation being waited for.

Event Horizon's deadline latches cooperative cancellation. The parked sleep returns ECANCELED; protect lets cleanup cross checkpoints even with that latch set. Keep cleanup bounded. The foreign tab documents its own status representation rather than pretending its errors are event-horizon values.

The callback deadline requests cancellation of an already-admitted long timer, observes its terminal -ECANCELED, and only then submits asynchronous cleanup. run() drains that cleanup before the context dies. This is a manually owned deadline state machine, not withDeadline or a callback-level protected scope. The fixture's long delay makes the intended cancellation path testable; real code must also handle completion winning the race.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_deadline"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs, seconds;
import std.stdio : writeln;
import vibe.core.concurrency : async;
import vibe.core.core : setTimer, sleep, sleepUninterruptible;
import vibe.core.task : InterruptException;

void main() @system
{
    bool cleaned;
    auto work = async({
        scope(exit) { sleepUninterruptible(5.msecs); cleaned = true; }
        try { sleep(10.seconds); return false; }
        catch (InterruptException) { return true; }
    });
    auto timer = setTimer(50.msecs, () nothrow { work.task.interrupt(); });
    scope(exit) timer.stop();
    const interrupted = work.getResult(); // Other task exceptions propagate.
    writeln("sleep returned: ", interrupted ? "InterruptException" : "ok");
    writeln("timed out: ", interrupted);
    writeln("cleaned up: ", cleaned);
}
sleep returned: InterruptException
timed out: true
cleaned up: true
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_deadline"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs, seconds;
import core.stdc.errno : ECANCELED;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    auto result = runApplication((ref RootScope root, ref Env env) {
        ref scheduler = currentScheduler();
        bool cleaned;
        auto timed = withDeadline!((ref scope_) {
            auto slept = env.clock.sleep(10.seconds);
            if (slept.hasError && slept.error.code != ECANCELED)
                scope_.fail(Cause!IoError.fromFailure(slept.error));
            writeln("sleep returned: ", slept.hasError ? "ECANCELED" : "ok");
            // Cleanup is allowed to finish even with cancellation latched.
            auto cleanup = protect!(() => env.clock.sleep(5.msecs))(scheduler);
            cleaned = !cleanup.hasError;
            if (cleanup.hasError) scope_.fail(Cause!IoError.fromFailure(cleanup.error));
        })(scheduler, 50.msecs);
        if (timed.hasError && !timed.error.isTimeout) root.fail(timed.error);
        writeln("timed out: ", timed.hasError && timed.error.isTimeout);
        writeln("cleaned up: ", cleaned);
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
sleep returned: ECANCELED
timed out: true
cleaned up: true
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_deadline"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs, seconds;
import core.stdc.errno : ECANCELED;
import std.stdio : writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion, OpHandle;

struct State
{
    DefaultLoop* loop;
    OpHandle operation;
    bool timedOut, finished, cleaned, stopping;
    int result, error;
}
void cleaned(ref State state, ref Completion done) nothrow @nogc
{
    if (done.res < 0) state.error = done.res;
    else state.cleaned = true;
}
void finished(ref State state, ref Completion done) nothrow @nogc
{
    if (state.stopping) return;
    state.finished = true;
    state.result = done.res;
    // Cleanup starts only after terminal completion, not after cancel().
    auto armed = state.loop.submitAfter!cleaned(5.msecs, state);
    if (armed.hasError) state.error = -armed.error.code;
}
void deadline(ref State state, ref Completion done) nothrow @nogc
{
    if (state.stopping) return;
    if (done.res < 0) { state.error = done.res; return; }
    if (state.finished) return;
    state.timedOut = true;
    auto cancelled = state.loop.cancel(state.operation);
    if (cancelled.hasError) state.error = -cancelled.error.code;
}

int main() @system
{
    State state;
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) { state.stopping = true; loop.destroy(); }
    state.loop = &loop;
    auto work = loop.submitAfter!finished(10.seconds, state);
    if (work.hasError) { stderr.writeln(work.error); return 1; }
    state.operation = work.value;
    auto timer = loop.submitAfter!deadline(50.msecs, state);
    if (timer.hasError) { stderr.writeln(timer.error); return 1; }
    auto ran = loop.run();
    if (ran.hasError) { stderr.writeln(ran.error); return 1; }
    if (state.error || (state.result < 0 && state.result != -ECANCELED))
    { stderr.writeln("operation failed: ", state.error, " / ", state.result); return 1; }
    writeln("sleep returned: ", state.result == -ECANCELED ? "ECANCELED" : "ok");
    writeln("timed out: ", state.timedOut);
    writeln("cleaned up: ", state.cleaned);
    return 0;
}
sleep returned: ECANCELED
timed out: true
cleaned up: true

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

TCP and buffer ownership ​

The tutorial sends the five-byte message hello and prints the echoed frame. TCP is a byte stream: neither one write nor one readiness/completion notification is a message boundary. Accumulate partial receives and retain unsent suffixes, or use an API's documented all-bytes operation. Binding loopback port zero lets the OS reserve a port for this run without a port-selection race.

The foreign callbacks borrow buffers according to their library's contract. Copy data that must outlive a callback, or retain the documented owner. Event Horizon moves an owned buffer into the operation and returns ownership on completion. That lifetime discipline is not a promise of kernel or network zero-copy. A real protocol also needs framing, maximum message sizes and per-connection deadlines.

The callback tutorial sets up a local TCP pair synchronously, then drives all data transfers through OpSend and OpRecv. Its five-byte framing state handles short completions. Contexts and fixed buffers outlive the loop; teardown prevents callbacks from rearming. The contract example additionally tests async accept/connect and deliberately tiny pool buffers.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_tcp_echo"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import std.stdio : writeln;
import vibe.core.net : connectTCP, listenTCP, TCPConnection;
import vibe.core.sync : createManualEvent;

void main() @system
{
    auto finished = createManualEvent();
    Exception serverError;
    auto listener = listenTCP(0, (TCPConnection peer) nothrow @safe {
        scope(exit) finished.emit();
        scope(exit) peer.close();
        try
        {
            ubyte[5] frame;
            peer.read(frame[]); // Five-byte framing; all-mode handles short reads.
            peer.write(frame[]);
        }
        catch (Exception error) { serverError = error; }
    }, "127.0.0.1");
    scope(exit) listener.stopListening();
    auto client = connectTCP("127.0.0.1", listener.bindAddress.port);
    scope(exit) client.close();
    ubyte[5] reply;
    try
    {
        client.write(cast(const(ubyte)[]) "hello");
        client.read(reply[]);
    }
    finally
    {
        client.close(); // Unblock the server even if the exchange failed.
        finished.waitUninterruptible(0); // Includes an earlier completion emit.
    }
    if (serverError !is null) throw serverError;
    writeln("echoed: ", cast(const(char)[]) reply[]);
}
echoed: hello
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_tcp_echo"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.lifetime : move;
import std.stdio : writeln, stderr;
import sparkles.base.buffer : UniqueBuffer;
import sparkles.event_horizon;

// A five-byte frame: the helpers handle short transfers, not message framing.
IoResult!void echo(ref Listener listener) @system
{
    auto accepted = listener.accept();
    if (accepted.hasError) return ioErr!void(accepted.error);
    auto peer = accepted.value;
    scope(exit) peer.close();
    UniqueBuffer!(ubyte, 5) frame;
    frame.length = 5;
    auto received = peer.readExactly(move(frame));
    if (received.res.hasError) return received.res;
    return peer.sendAll(move(received.buf)).res;
}

int main() @system
{
    Listener listener; // Outlives the joined root and every server operation.
    scope(exit) listener.close();
    auto result = runApplication((ref RootScope root, ref Env env) {
        auto opened = env.net.listen(ipv4("127.0.0.1", 0));
        if (opened.hasError) return ioErr!void(opened.error);
        listener = opened.value;
        auto address = listener.localAddress();
        if (address.hasError) return ioErr!void(address.error);
        if (!root.spawn({
            auto served = echo(listener);
            if (served.hasError) root.fail(Cause!IoError.fromFailure(served.error));
        })) return ioOk(); // Rejection already records the scope failure.

        auto connected = env.net.connect(address.value);
        if (connected.hasError) return ioErr!void(connected.error);
        auto client = connected.value;
        scope(exit) client.close();
        UniqueBuffer!(ubyte, 5) frame;
        frame ~= cast(const(ubyte)[]) "hello";
        auto sent = client.sendAll(move(frame));
        if (sent.res.hasError) return sent.res;
        auto reply = client.readExactly(move(sent.buf));
        if (reply.res.hasError) return reply.res;
        writeln("echoed: ", cast(const(char)[]) reply.buf[]);
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
echoed: hello
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_tcp_echo"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.lifetime : move;
import core.stdc.errno : EIO;
import std.socket : TcpSocket, InternetAddress;
import std.stdio : writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion, OpRecv, OpSend;
import sparkles.event_horizon.buffer : Buf;

struct Transfer
{
    DefaultLoop* loop;
    bool* stopping;
    int fd;
    bool sending;
    size_t used;
    int error;
    ubyte[5] bytes;
}
void next(ref Transfer transfer) nothrow @nogc
{
    if (*transfer.stopping) return;
    auto window = Buf.fromForeign(transfer.bytes[transfer.used .. $], null);
    window.length = cast(uint) (transfer.bytes.length - transfer.used);
    if (transfer.sending)
    {
        auto sent = transfer.loop.submit!completed(OpSend(transfer.fd, move(window)), transfer);
        if (sent.hasError) transfer.error = sent.error.code;
    }
    else
    {
        auto received = transfer.loop.submit!completed(OpRecv(transfer.fd, move(window)), transfer);
        if (received.hasError) transfer.error = received.error.code;
    }
    if (transfer.error) { *transfer.stopping = true; transfer.loop.stop(); }
}
void completed(ref Transfer transfer, ref Completion done) nothrow @nogc
{
    if (*transfer.stopping) return; // Also prevents rearming during teardown.
    if (done.res <= 0)
    {
        transfer.error = done.res < 0 ? -done.res : EIO;
        *transfer.stopping = true;
        transfer.loop.stop();
        return;
    }
    transfer.used += done.res;
    if (transfer.used < transfer.bytes.length) next(transfer);
}
int main() @system
{
    // Synchronous loopback fixture setup. Data transfers use Tier-A callbacks.
    auto listener = new TcpSocket;
    scope(exit) listener.close();
    listener.bind(new InternetAddress("127.0.0.1", 0));
    listener.listen(1);
    auto client = new TcpSocket;
    scope(exit) client.close();
    client.connect(listener.localAddress);
    auto server = listener.accept();
    scope(exit) server.close();

    bool stopping;
    Transfer outgoing, incoming;
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) { stopping = true; loop.destroy(); }
    outgoing.loop = incoming.loop = &loop;
    outgoing.stopping = incoming.stopping = &stopping;
    outgoing.fd = cast(int) client.handle;
    incoming.fd = cast(int) server.handle;
    outgoing.sending = true;
    outgoing.bytes[] = cast(const(ubyte)[]) "hello";
    foreach (exchange; 0 .. 2)
    {
        next(outgoing);
        if (!outgoing.error) next(incoming);
        auto ran = loop.run(); // Fan-in for this five-byte exchange.
        if (ran.hasError) { stderr.writeln(ran.error); return 1; }
        if (outgoing.error || incoming.error)
        { stderr.writeln("transfer failed: ", outgoing.error, " / ", incoming.error); return 1; }
        outgoing.bytes = incoming.bytes;
        outgoing.used = incoming.used = 0;
        outgoing.fd = cast(int) server.handle;
        incoming.fd = cast(int) client.handle;
    }
    writeln("echoed: ", cast(const(char)[]) incoming.bytes[]);
    return 0;
}
echoed: hello

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

Queues, channels and backpressure ​

The shared payload contract is the ordered sequence 1 through 5, with sum 15. The flow-control contracts can differ. Both tutorials let a producer fill a capacity-two channel while a consumer drains it. The standalone EH contract separately proves that a third put parks and one freed slot permits exactly one additional put, without relying on a short sleep as evidence.

Read the foreign implementation before translating put: an unbounded queue does not acquire a capacity bound merely because the consumer is slow. A callback adapter must stop producing and arrange a wakeup when space returns; blocking an event-loop thread on an OS semaphore can stop unrelated connections. Event Horizon's Channel!(T, capacity) belongs to one scheduler and is not a drop-in cross-thread notification or broadcast mechanism.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_channel"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import std.stdio : writeln;
import vibe.core.channel : createChannel;
import vibe.core.concurrency : async;

void main() @system
{
    auto items = createChannel!(int, 2)();
    auto producer = async({
        scope(exit) items.close();
        foreach (value; 1 .. 6) items.put(value); // Parks while full.
        return 0;
    });
    int sum, next;
    while (items.tryConsumeOne(next)) sum += next; // Ends after close and drain.
    producer.getResult(); // Join and propagate errors.
    writeln("consumed: ", sum);
}
consumed: 15
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_channel"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.stdc.errno : EPIPE;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    Channel!(int, 2) items; // Outlives every producer/consumer operation.
    auto result = runApplication((ref RootScope root, ref Env env) {
        ref scheduler = currentScheduler();
        if (!root.spawn({
            scope(exit) items.close();
            foreach (value; 1 .. 6)
            {
                auto put = items.put(scheduler, value); // Parks when full.
                if (put.hasError) { root.fail(Cause!IoError.fromFailure(put.error)); return; }
            }
        })) return ioOk();
        int sum;
        for (;;)
        {
            auto next = items.take(scheduler); // Parks when empty.
            if (next.hasError)
            {
                if (next.error.code != EPIPE) return ioErr!void(next.error);
                break; // Closed and drained.
            }
            sum += next.value;
        }
        writeln("consumed: ", sum);
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
consumed: 15

Contract examples: vibe-core · event-horizon fibers.

Files and operation lifetime ​

The tutorials create an anonymous temporary file containing hello from a file\n. The vibe-core example sizes and reads a bounded span; the EH fiber helper reads through EOF with a byte limit. Both print the contents. Keep file and buffer ownership until all operations finish. Never classify a read failure as EOF.

The event-horizon example reads until EOF using offsets and an owned buffer. Its Linux io_uring implementation submits file operations to the kernel. This does not establish the same behavior for every backend: kqueue synthesizes completions over readiness and its regular-file worker-pool refinement is separate. Where the foreign example uses a worker adapter, it is explicitly application code, not evidence of a native filesystem capability.

The callback tutorial reads an anonymous temporary fixture through positioned OpRead operations until zero-byte EOF, using a fixed bound. Fixture creation and final close are synchronous setup/teardown. The contract version separately tests asynchronous open/close, missing-file errors and tiny pooled buffers. The fiber tutorial opens the file through env.fs and reads it with bounded readText; the file closes itself.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_file_read"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import std.conv : to;
import std.stdio : File, writeln;
import std.exception : enforce;
import vibe.core.file : FileMode, openFile;

void main() @system
{
    auto fixture = File.tmpfile();
    fixture.write("hello from a file\n");
    fixture.flush();
    auto file = openFile("/proc/self/fd/" ~ fixture.fileno.to!string, FileMode.read);
    scope(exit) file.close();
    ubyte[1024] bytes;
    const length = cast(size_t) file.size;
    enforce(length <= bytes.length, "file exceeds the example's bound");
    file.read(bytes[0 .. length]); // Default mode reads the whole requested span.
    writeln("read ", length, " bytes: ", cast(const(char)[]) bytes[0 .. length]);
}
read 18 bytes: hello from a file
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_file_read"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import std.file : remove, tempDir, write;
import std.path : buildPath;
import std.process : thisProcessID;
import std.conv : to;
import std.stdio : writeln, stderr;
import expected : andThen;
import sparkles.base.vfs : OpenMode, Rights, ambientAuthority, openRoot;
import sparkles.event_horizon : runApplication, RootScope, Env, ioErr, ioOk, readText;

int main() @system
{
    const name = "eh-tutorial-" ~ thisProcessID.to!string ~ ".txt";
    write(buildPath(tempDir, name), "hello from a file\n");
    scope (exit) remove(buildPath(tempDir, name));

    auto result = runApplication((ref RootScope root, ref Env env) {
        // `env.fs` opens a directory as a capability; the file is named in it.
        auto dir = openRoot!(Rights.readOnly)(&env.fs(), tempDir, ambientAuthority());
        if (dir.hasError)
            return ioErr!void(dir);
        auto file = dir.value.openFile!(OpenMode.read)(name);
        if (file.hasError)
            return ioErr!void(file);
        return readText(file.value, 1024).andThen!((text) {
            writeln("read ", text.length, " bytes: ", text);
            return ioOk();
        });
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
read 18 bytes: hello from a file
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_file_read"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.lifetime : move;
import core.stdc.errno : EFBIG;
import std.stdio : File, writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion, OpRead;
import sparkles.event_horizon.buffer : Buf;

struct Reader
{
    DefaultLoop* loop;
    int fd, error;
    bool stopping;
    size_t used;
    ubyte[128] bytes; // A bounded example, not an unbounded file loader.
}
void readNext(ref Reader reader) nothrow @nogc
{
    if (reader.used == reader.bytes.length) { reader.error = EFBIG; return; }
    auto window = Buf.fromForeign(reader.bytes[reader.used .. $], null);
    auto submitted = reader.loop.submit!readDone(OpRead(reader.fd, move(window), reader.used), reader);
    if (submitted.hasError) reader.error = submitted.error.code;
}
void readDone(ref Reader reader, ref Completion done) nothrow @nogc
{
    if (reader.stopping) return;
    if (done.res < 0) reader.error = -done.res;
    else if (done.res > 0) { reader.used += done.res; readNext(reader); }
    // Zero is EOF. A failed read is not EOF.
}
int main() @system
{
    auto fixture = File.tmpfile(); // Synchronous fixture setup; reads below are native callbacks.
    fixture.write("hello from a file\n");
    fixture.flush();
    Reader reader;
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) { reader.stopping = true; loop.destroy(); }
    reader.loop = &loop;
    reader.fd = fixture.fileno;
    readNext(reader);
    auto ran = loop.run();
    if (ran.hasError) { stderr.writeln(ran.error); return 1; }
    if (reader.error) { stderr.writeln("read failed: ", reader.error); return 1; }
    writeln("read ", reader.used, " bytes: ", cast(const(char)[]) reader.bytes[0 .. reader.used]);
    return 0;
}
read 18 bytes: hello from a file

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

Capturing a child process ​

The child writes different text to stdout and stderr, then exits with code 3. Both streams and the nonzero status are printed: a child command failing is not necessarily failure to launch or observe it. Root reaping is part of the operation's lifetime.

A general capture implementation must drain both pipes concurrently, even when one is quiet. Reading all stdout before stderr can deadlock when the stderr pipe fills. Tiny fixed fixtures do not prove a hand-written adapter safe for unbounded output. capture supplies the drain/reap coordination; adapters need their own output limits, cancellation and error handling.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_exec"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : seconds;
import std.stdio : writeln;
import vibe.core.concurrency : async;
import vibe.core.process : pipeProcess, Redirect;
import vibe.core.stream : IOMode;

string drain(Stream)(Stream stream)
{
    string text;
    ubyte[1024] bytes;
    while (!stream.empty)
        text ~= cast(const(char)[]) bytes[0 .. stream.read(bytes[], IOMode.once)];
    return text;
}
void main() @system
{
    auto pipes = pipeProcess(["sh", "-c", "echo out; echo err >&2; exit 3"], Redirect.all);
    pipes.stdin.close();
    auto stdout = async(() => drain(pipes.stdout));
    auto stderr = async(() => drain(pipes.stderr));
    const status = pipes.process.waitOrForceKill(5.seconds);
    stdout.task.join();
    stderr.task.join(); // Both streams must drain concurrently.
    writeln("stdout: ", stdout.getResult());
    writeln("stderr: ", stderr.getResult());
    writeln("exit code: ", status);
}
stdout: out

stderr: err

exit code: 3
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_exec"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    auto result = runApplication((ref RootScope root, ref Env env) {
        ProcessConfig config;
        config.stderrSpec = StdioSpec(StdioMode.pipe);
        auto captured = capture(currentScheduler(),
            ["sh", "-c", "echo out; echo err >&2; exit 3"], config);
        if (captured.hasError) return ioErr!void(captured.error);
        writeln("stdout: ", cast(const(char)[]) captured.value.stdout_[]);
        writeln("stderr: ", cast(const(char)[]) captured.value.stderr_[]);
        writeln("exit code: ", captured.value.status.code); // Nonzero exit is data.
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
stdout: out

stderr: err

exit code: 3

Contract examples: vibe-core · event-horizon fibers.

Streaming and termination ​

Streaming adds a state machine: accumulate bytes into lines, observe the root, request termination, finish drains, and reap. A pipe read can split a line or contain several lines, so splitting each chunk independently is insufficient. The two source tabs expose the cleanup machinery instead of hiding it behind a printed success message.

The foreign program's manual root termination is not process-tree containment. Event Horizon reports supervision outcome and reap provenance, but its process group/cgroup tiers are not an inescapable sandbox either: descendants can race the post-spawn cgroup migration. EOF may be forced at the drain limit. See the supervision caveats.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_spawn"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : seconds;
import std.stdio : writeln;
import std.string : indexOf;
import vibe.core.concurrency : async;
import vibe.core.process : pipeProcess, Redirect;
import vibe.core.stream : IOMode;

void main() @system
{
    auto pipes = pipeProcess(["sh", "-c", "echo one; echo two; exec sleep 30"], Redirect.stdout);
    auto reader = async({
        string pending;
        ubyte[128] bytes;
        while (!pipes.stdout.empty)
        {
            pending ~= cast(const(char)[]) bytes[0 .. pipes.stdout.read(bytes[], IOMode.once)];
            for (auto end = pending.indexOf('\n'); end >= 0; end = pending.indexOf('\n'))
            {
                const line = pending[0 .. end];
                writeln("line: ", line);
                if (line == "two") pipes.process.kill(); // TERM after readiness.
                pending = pending[end + 1 .. $];
            }
        }
        if (pending.length) writeln("line: ", pending);
        return 0;
    });
    const status = pipes.process.waitOrForceKill(5.seconds); // Bounded root wait.
    reader.getResult();
    writeln("exited: signaled by ", status < 0 ? -status : 0);
    writeln("end: stopped, reap: reaped");
}
line: one
line: two
exited: signaled by 15
end: stopped, reap: reaped
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_spawn"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    bool requestedStop, supervisionFailed;
    auto result = runApplication((ref RootScope root, ref Env env) {
        SupervisedProcessConfig config;
        config.terminateGrace = 500.msecs;
        config.process.stdinSpec = StdioSpec(StdioMode.nullDev);
        auto child = supervise(currentScheduler(),
            ["sh", "-c", "echo one; echo two; exec sleep 30"], config,
            null, (in ProcessEvent event) {
                if (event.kind == ProcessEventKind.line)
                {
                    auto line = cast(const(char)[]) event.line.bytes;
                    writeln("line: ", line);
                    if (line == "two") { requestedStop = true; root.cancel(); }
                }
                else if (event.kind == ProcessEventKind.exited)
                    writeln("exited: ", event.end, ", signaled by ",
                        event.status.signaled ? event.status.code : 0);
            });
        if (child.hasError) { supervisionFailed = true; return ioErr!void(child.error); }
        writeln("end: ", child.value.end, ", reap: ", child.value.reap);
        return ioOk();
    });
    // This example deliberately cancels its root after the second line.
    if (!result.hasError || (requestedStop && !supervisionFailed
        && result.error.kind == Cause!IoError.Kind.interrupt
        && result.error.interrupt.kind == InterruptKind.cancelled)) return 0;
    stderr.writeln(result.error);
    return 1;
}
line: one
line: two
exited: cancelled, signaled by 15
end: cancelled, reap: reaped

Contract examples: vibe-core · event-horizon fibers.

POSIX signals and loop notifications ​

The foreign tab uses sigwait in an ordinary worker and then delivers completion through its runtime bridge. Unlike calling an event method from a signal handler, this does not assume the event method is async-signal-safe. The signal is thread-directed for an isolated, reproducible fixture.

Install the signal receiver before sending SIGUSR1, then print the received signal. Linux signal masks and handler installation are process/thread integration decisions; they are not interchangeable with an ordinary callback queue. Keep signal-handler work async-signal-safe.

SignalFd receives masked POSIX signals through the event-horizon loop. A foreign notification primitive may only wake an owner loop from another thread. In particular, libasync's AsyncSignal is not a POSIX signal subscription; an OS signal bridge must supply that integration explicitly. These programs are Linux fixtures, not Windows signal portability examples.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_signal"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.sys.posix.signal : sigset_t, sigemptyset, sigaddset, SIGUSR1,
    pthread_sigmask, SIG_BLOCK, SIG_SETMASK, pthread_kill, sigwait;
import core.sys.posix.pthread : pthread_self;
import core.thread : Thread;
import std.exception : enforce;
import std.stdio : writeln;
import vibe.core.sync : createSharedManualEvent;

void main() @system
{
    auto completed = createSharedManualEvent();
    Exception failure;
    int received;
    // POSIX adapter: sigwait lives on an OS thread, not in a signal handler.
    auto worker = new Thread({
        scope(exit) completed.emit();
        try
        {
            sigset_t signals, previous;
            enforce(sigemptyset(&signals) == 0 && sigaddset(&signals, SIGUSR1) == 0);
            enforce(pthread_sigmask(SIG_BLOCK, &signals, &previous) == 0);
            scope(exit) enforce(pthread_sigmask(SIG_SETMASK, &previous, null) == 0);
            enforce(pthread_kill(pthread_self(), SIGUSR1) == 0);
            enforce(sigwait(&signals, &received) == 0);
        }
        catch (Exception error) { failure = error; }
    });
    worker.start();
    completed.wait(0); // The waiting fiber parks.
    worker.join();
    if (failure !is null) throw failure;
    writeln("got signal ", received == SIGUSR1 ? "SIGUSR1" : "other");
}
got signal SIGUSR1
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_signal"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.stdc.errno : errno;
import core.sys.posix.signal : SIGUSR1, kill;
import core.sys.posix.unistd : getpid;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    SignalFd signals; // Block before starting runtime worker threads.
    auto opened = SignalFd.create(signals, [SIGUSR1]);
    if (opened.hasError) { stderr.writeln(opened.error); return 1; }
    auto result = runApplication((ref RootScope root, ref Env env) {
        if (kill(getpid(), SIGUSR1) != 0) return ioErr!void(errno, OpKind.none);
        auto received = signals.nextSignal(currentScheduler());
        if (received.hasError) return ioErr!void(received.error);
        writeln("got signal ", received.value == SIGUSR1 ? "SIGUSR1" : "other");
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
got signal SIGUSR1

Contract examples: vibe-core · event-horizon fibers.

Retries and ordinary errors ​

The synthetic operation fails transiently twice, then succeeds. The tutorial prints the successful attempt; the standalone contract checks the exact count and result.

Retry policy needs a retryable-error decision and a finite budget. Vibe-core's loop catches only the example's TransientFailure; other exceptions propagate. EH's fiber policy retries ordinary I/O failures and preserves interruption and defects as separate channels. Neither shape makes repeated side effects safe unless the operation is idempotent.

The callback version handles EAGAIN with 5 ms then 10 ms backoff and a finite attempt budget. Admission and timer-completion errors are reported separately, not mistaken for retryable dependency failures.

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_vibe_retry"
    dependency "vibe-core" version="2.14.0"
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import std.stdio : writeln;
import vibe.core.core : sleep;

class TransientFailure : Exception
{
    this() @safe { super("try again"); }
}
void main() @system
{
    foreach (attempt; 1 .. 6)
    {
        try
        {
            if (attempt < 3) throw new TransientFailure;
            writeln("succeeded on attempt ", attempt);
            break;
        }
        catch (TransientFailure error)
        {
            if (attempt == 5) throw error;
            sleep((5 << (attempt - 1)).msecs);
        }
    }
}
succeeded on attempt 3
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_retry"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import core.stdc.errno : EAGAIN;
import std.stdio : writeln, stderr;
import sparkles.event_horizon;

int main() @system
{
    auto result = runApplication((ref RootScope root, ref Env env) {
        int attempts;
        enum policy = exponential(5.msecs) & recurs(4);
        auto tried = retry(root, env.clock, policy, () {
            if (++attempts < 3) return ioErr!int(EAGAIN, OpKind.none);
            return ioOk(attempts);
        });
        if (tried.hasError) root.fail(tried.error);
        else writeln("succeeded on attempt ", tried.value);
        return ioOk();
    });
    if (result.hasError) stderr.writeln(result.error);
    return result.hasError ? 1 : 0;
}
succeeded on attempt 3
d
#!/usr/bin/env dub
/+ dub.sdl:
    name "tutorial_eh_callback_retry"
    dependency "sparkles:event-horizon" path="../../../../../.."
    platforms "linux"
    buildType "checked" {
        buildOptions "optimize" "inline" "debugInfo"
    }
+/
import core.time : msecs;
import core.stdc.errno : EAGAIN;
import std.stdio : writeln, stderr;
import sparkles.event_horizon.loop : DefaultLoop;
import sparkles.event_horizon.op : Completion;
import sparkles.event_horizon.errors : IoResult, OpKind, ioOk, ioErr;

struct State { DefaultLoop* loop; int attempts, error; bool stopping; }
IoResult!int request(int attempt) @safe nothrow @nogc
    => attempt < 3 ? ioErr!int(EAGAIN, OpKind.none) : ioOk(42);

void attempt(ref State state, ref Completion done) nothrow @nogc
{
    if (state.stopping) return;
    if (done.res < 0) { state.error = done.res; return; }
    auto result = request(++state.attempts);
    if (!result.hasError) return;
    if (result.error.code != EAGAIN || state.attempts >= 5)
    { state.error = -result.error.code; return; }
    auto timer = state.loop.submitAfter!attempt((5 << (state.attempts - 1)).msecs, state);
    if (timer.hasError) state.error = -timer.error.code;
}

int main() @system
{
    State state;
    DefaultLoop loop;
    auto started = DefaultLoop.create(loop);
    if (started.hasError) { stderr.writeln(started.error); return 1; }
    scope(exit) { state.stopping = true; loop.destroy(); }
    state.loop = &loop;
    auto first = loop.submitAfter!attempt(0.msecs, state);
    if (first.hasError) { stderr.writeln(first.error); return 1; }
    auto ran = loop.run();
    if (ran.hasError) { stderr.writeln(ran.error); return 1; }
    if (state.error) { stderr.writeln("request failed: ", state.error); return 1; }
    writeln("succeeded on attempt ", state.attempts);
    return 0;
}
succeeded on attempt 3

Contract examples: vibe-core · event-horizon fibers · event-horizon callbacks.

Errors and migration decisions ​

BoundaryMigration decision
Ordinary operation failureCheck the foreign status/exception and the event-horizon IoResult; do not silently treat failure as success or EOF.
Task failureDecide who observes the error and who joins remaining work. Scope outcomes and defects are distinct from ordinary I/O errors.
Buffer lifetimeRetain borrowed data until the foreign operation finishes; recover moved buffers from event-horizon completions.
Cross-thread workUse a documented cross-thread bridge. Scheduler-local channels do not replace it.
Missing native primitiveTreat a Phobos/POSIX adapter as application integration, not as a feature supplied by the networking library.

Next steps ​

Start with one independently owned connection or operation. Preserve its framing, error handling and shutdown behavior before changing scheduler topology. The Node.js comparison provides another fully executable view of the same concerns, including explicit retries. The execution guide records platform and containment limits.