Timers take a Duration

timeout and with-deadline take a Duration, made with milliseconds,
seconds, minutes or hours; within takes one too.

🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-5-5)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-20 18:07:05 +02:00
parent 3f8c13cc2f
commit cefa2313ad

View file

@ -955,7 +955,7 @@ Functions that may suspend execution—whether waiting on a channel, sleeping, o
@codeblock[#:lang "bjolang"]|{
(: think (-bjo-> int int))
(defbjo (think n)
(sync (timeout 50)) ;; wait 50 ms, without holding up a thread
(sync (timeout (milliseconds 50))) ;; wait 50 ms, without holding up a thread
(* n n))
}|
@ -1035,29 +1035,31 @@ Crucially, calling @code{(chan-recv seat)} does not perform the receive immediat
|---------------------------+-------------------------------------------------------------|
| @code{(chan-recv ch)} | Receives a value from channel @code{ch} |
| @code{(chan-send ch v)} | Sends @code{v} on channel @code{ch} |
| @code{(timeout ms)} | Fires after @code{ms} milliseconds |
| @code{(timeout d)} | Fires after the @code{Duration} @code{d} |
| @code{(choose e1 e2 ...)} | Fires when the first of the supplied events occurs |
| @code{(wrap e f)} | Fires when @code{e} occurs, applying @code{f} to its value |
}
A @code{Duration} is a length of time, made with @code{milliseconds}, @code{seconds}, @code{minutes} or @code{hours}: @code{(timeout (seconds 5))}. It has its unit in its type, so a timer cannot be given a bare number, and one too long for its range raises rather than wrapping round.
With these combinators, deadlines and timeouts become simple functions that compose over any event. For instance, we can enforce a response timeout on our card players:
@codeblock[#:lang "bjolang"]|{
(: within (-> int (Event %a) (Event (Option %a))))
(defun (within ms ev)
(: within (-> Duration (Event %a) (Event (Option %a))))
(defun (within d ev)
(choose (wrap ev #(Some &))
(wrap (timeout ms) (fun (u) None))))
(wrap (timeout d) (fun (u) None))))
(: slow-player (-bjo-> (Chan string) void))
(defbjo (slow-player seat)
(sync (timeout 200))
(sync (timeout (milliseconds 200)))
(sync (chan-send seat "7♣")))
(defbjo (main)
(def seat (make-chan))
(spawn (slow-player seat))
(println (sync (within 50 (chan-recv seat)))) ;; None
(println (sync (within 500 (chan-recv seat)))) ;; (Some 7♣)
(println (sync (within (milliseconds 50) (chan-recv seat)))) ;; None
(println (sync (within (milliseconds 500) (chan-recv seat)))) ;; (Some 7♣)
0)
}|
@ -1097,14 +1099,14 @@ The top-level @code{main} function acts as an implicit root scope, which is why
|---------------------------------------+------------------------------------------------------------|
| @code{(with-scope body ...)} | Waits for child fibers and finalizes open file ports |
| @code{(with-cancel (cancel) body ...)} | Binds a @code{cancel} procedure to signal child fibers |
| @code{(with-deadline ms body ...)} | Cancels the scope automatically after @code{ms} ms |
| @code{(with-deadline d body ...)} | Cancels the scope automatically after the duration @code{d} |
| @code{(with-shield body ...)} | Shields cleanup operations from parent cancellation |
}
@codeblock[#:lang "bjolang"]|{
(: fetch (-bjo-> string void))
(defbjo (fetch name)
(sync (timeout 30))
(sync (timeout (milliseconds 30)))
(println #"fetched ${name}"))
(: count-up (-bjo-> (Chan int) void))
@ -1115,7 +1117,7 @@ The top-level @code{main} function acts as an implicit root scope, which is why
(defbjo (main)
;; Both are fetched when this returns, or the deadline raises.
(with-deadline 1000
(with-deadline (seconds 1)
(spawn (fetch "a"))
(spawn (fetch "b")))
(println "both done")
@ -1249,7 +1251,7 @@ If a transaction raises an exception, it writes nothing, and @code{atomically} r
@codeblock[#:lang "bjolang"]|{
;; A card from the pile, or None after 100 ms.
(sync (choose (wrap (atomically-evt (fun (tx) (take-top tx pile))) Some)
(wrap (timeout 100) (fun (_) None))))
(wrap (timeout (milliseconds 100)) (fun (_) None))))
}|
Inside a transaction, @code{(tx-or-else tx a b)} runs @code{a}, and if @code{a} retries, undoes its writes and runs @code{b}. If both retry, the whole transaction waits for a change to anything either of them read.