Ship

Book · Timers

Timers

Draft

Park a fiber until a Clock deadline without blocking the runtime.

On this page · 4 sections

Sleeping parks one fiber

Clock.sleep(duration) parks the current fiber until the provided Clock reaches the deadline. Other fibers keep running; the host thread waits only when every fiber is parked.

Clock.virtual(start) is the deterministic provider for tests. Its time stands still while any fiber can run, then jumps straight to the earliest pending deadline, so a test observes exact elapsed time without waiting.

Timers run on the native scheduler. The browser runtime does not implement Clock yet.

type TimerProbe = {
  mut events: Vector(String);
}

let recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit = fn(probeprobe: Book.Concurrency.Timers.TimerProbe, eventevent: String) {
  probe.events := Vector.pushpush: pure fn!('a)(items: Vector(String), item: String) -> Vector(String)(probeprobe: Book.Concurrency.Timers.TimerProbe.eventsevents: Vector(String), eventevent: String);
};

let epochepoch: Instant = must Instant.parse_rfc3339parse_rfc3339: pure fn(string: String) -> Instant, exn [ :parse(String) ]("2026-01-01T00:00:00Z""2026-01-01T00:00:00Z": String);

Sleepers Wake In Deadline Order

test✓
  let probeprobe: Book.Concurrency.Timers.TimerProbe: TimerProbe @ global = TimerProbe { events = []; };
  provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
    let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
    let slowslow: Async.Task(Unit) = spawn {
      Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int));
      recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "slow""slow": String);
    };
    let fastfast: Async.Task(Unit) = spawn {
      Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int));
      recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "fast""fast": String);
    };
    recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "running""running": String);
    await slowslow: Async.Task(Unit);
    await fastfast: Async.Task(Unit);
    expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int);
    expect ✓Clock.nownow: fn!() -> Instant() == must Instant.addadd: pure fn(instant: Instant, duration: Duration) -> Instant, exn [ :overflow ](epochepoch: Instant, Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int));
  };
  expect ✓probeprobe: Book.Concurrency.Timers.TimerProbe.eventsevents: Vector(String) == ["running""running": String, "fast""fast": String, "slow""slow": String];

A zero or negative duration still parks the fiber: every other runnable fiber gets a turn before it resumes.

Zero Sleep Yields

test✓
  let probeprobe: Book.Concurrency.Timers.TimerProbe: TimerProbe @ global = TimerProbe { events = []; };
  provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
    let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
    let otherother: Async.Task(Unit) = spawn {
      recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "other""other": String);
    };
    Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.zerozero: pure fn!() -> Duration());
    recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "resumed""resumed": String);
    await otherother: Async.Task(Unit);
    expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.zerozero: pure fn!() -> Duration();
  };
  expect ✓probeprobe: Book.Concurrency.Timers.TimerProbe.eventsevents: Vector(String) == ["other""other": String, "resumed""resumed": String];

Interruption cancels the timer

Interrupting a sleeping fiber removes its deadline and runs its cleanup. The canceled deadline never advances virtual time.

Interrupt Cancels Sleep

test✓
  let probeprobe: Book.Concurrency.Timers.TimerProbe: TimerProbe @ global = TimerProbe { events = []; };
  provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
    let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
    let ownerowner: Async.Task(Int) = spawn {
      let _sleeper_sleeper: Async.Task(Unit) = spawn {
        defer recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "cleaned""cleaned": String);
        Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.hourshours: pure fn!(value: Int) -> Duration(11: Int));
        recordrecord: fn(probe: Book.Concurrency.Timers.TimerProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Timers.TimerProbe, "woke""woke": String);
      };
      Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int));
      77: Int
    };
    expect ✓await ownerowner: Async.Task(Int) == 77: Int;
    expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.secondsseconds: pure fn!(value: Int) -> Duration(11: Int);
  };
  expect ✓probeprobe: Book.Concurrency.Timers.TimerProbe.eventsevents: Vector(String) == ["cleaned""cleaned": String];

Deadlines race work

A deadline is ordinary concurrent work. The first child to complete a Completion wins. Returning from the race fiber interrupts the losing child, so its timer never fires.

let first_replyfirst_reply: fn(reply_after: Duration @ global, deadline: Duration @ global) -> String = fn(reply_afterreply_after: Duration, deadlinedeadline: Duration) {
  let racerace: Async.Task(String) = spawn {
    let firstfirst: Completion(String) = Completion.makemake: pure fn!('a)() -> Completion(String)();
    let _deadline_deadline: Async.Task(Unit) = spawn {
      Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(deadlinedeadline: Duration);
      Completion.completecomplete: pure fn!('a)(completion: Completion(String), value: String @ global) -> Unit(firstfirst: Completion(String), "deadline""deadline": String);
    };
    let _reply_reply: Async.Task(Unit) = spawn {
      Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(reply_afterreply_after: Duration);
      Completion.completecomplete: pure fn!('a)(completion: Completion(String), value: String @ global) -> Unit(firstfirst: Completion(String), "reply""reply": String);
    };
    await firstfirst: Completion(String)
  };
  await racerace: Async.Task(String)
};

Deadline Races Reply

test✓
  provide { Clock = Clock.virtualvirtual: pure fn!(start: Instant) -> Service.Provider(Clock)(epochepoch: Instant); } {
    let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
    expect ✓first_replyfirst_reply: fn(reply_after: Duration @ global, deadline: Duration @ global) -> String(Duration.secondsseconds: pure fn!(value: Int) -> Duration(22: Int), Duration.secondsseconds: pure fn!(value: Int) -> Duration(55: Int)) == "reply""reply": String;
    expect ✓first_replyfirst_reply: fn(reply_after: Duration @ global, deadline: Duration @ global) -> String(Duration.secondsseconds: pure fn!(value: Int) -> Duration(99: Int), Duration.secondsseconds: pure fn!(value: Int) -> Duration(55: Int)) == "deadline""deadline": String;
    expect ✓Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant) == Duration.secondsseconds: pure fn!(value: Int) -> Duration(77: Int);
  };

System time

Clock.SystemClock uses the host's monotonic clock. An idle scheduler waits for the earliest deadline instead of spinning, and still observes host cancellation while it waits.

System Clock Sleeps

test✓
  provide { Clock = Clock.SystemClockSystemClock: service Clock; } {
    let startstart: Clock.MonotonicInstant = Clock.markmark: fn!() -> Clock.MonotonicInstant();
    Clock.sleepsleep: fn!(duration: Duration) -> Unit, eff Clock(Duration.zerozero: pure fn!() -> Duration());
    expect ✓Duration.comparecompare: pure fn!(left: Duration, right: Duration) -> Int(
      Clock.elapsed_sinceelapsed_since: fn!(mark: Clock.MonotonicInstant) -> Duration(startstart: Clock.MonotonicInstant),
      Duration.zerozero: pure fn!() -> Duration(),
    ) >= 00: Int;
  };