Events
defining, subscribing to & emitting events
Functions
The Events module gives Arturo a lightweight publish/subscribe system. You define an :event, register one or more handlers with on, and fire it with emit. Handlers run on the next dispatcher tick, keeping things predictable and free of reentrancy surprises. The same machinery powers built-in lifecycle signals and task callbacks.
Key Concepts
- An
:eventis created witheventand identified by name onsubscribes a handler;emitfires the event- Handlers receive an optional payload via
.with: offunsubscribes - all handlers, or a single one by id- Built-in events cover process lifecycle (
CtrlC,BeforeExit, …) - Tasks are events too:
onaccepts a:taskfor done/failed callbacks
Note
Unlike channels (which are identified by reference), events are keyed by name. Twoevent 'progresscalls refer to the same event - so a handler registered in one place fires for anemitanywhere else.
Basic Usage
Defining & Subscribing
DataReady: event 'dataReady
; bind the payload to a literal symbol with .with:
on.with:'payload DataReady [
print ["got:" payload]
]
; fire it - handler runs on the next dispatcher tick
emit.with: #[user: "alice"] DataReady
Important
Hyphens don't survive the parser as event names - use'dataReady, not'data-ready.
Unsubscribing
; remove every handler for an event
off DataReady
; or keep a handle and remove just one
id: on.id DataReady [print "x"] ; returns an :integer id
off id
; fire-and-forget: run a handler exactly once
on.once DataReady [print "fires once"]
Built-in Events
Arturo ships a handful of lifecycle events you can hook into directly:
on CtrlC [ print "shutting down..." ]
on BeforeExit [ print "bye" ] ; runs at natural exit
on SigTerm [ print "term" ] ; POSIX; then quit(143)
on SigHup [ print "hup" ] ; POSIX; then quit(129)
Caution
BeforeExitruns at the program's natural end and drains any pending events first.SigTerm/SigHuphandlers run in an async-signal context - keep them short and side-effect-light.
Common Patterns
Task Callbacks
Because a :task is event-shaped, you can attach callbacks instead of blocking on wait:
t: do.async [pause 1000 42]
on.done.with:'r t [print ["ok:" r]]
on.failed.with:'e t [print ["fail:" e]]
on.cancelled.with:'r t [print ["cancel:" r]] ; r is :null
on.finished.with:'r t [print ["any:" r]] ; fires on any outcome
wait t ; handlers fire synchronously before wait returns
Tip
Task callbacks fire synchronously the moment the task settles - sowait tsees them run before it returns. Plain useremit, by contrast, schedules handlers for the next tick. The asymmetry is intentional.
Cross-process Events
Events bridge the parent ↔ do.async.isolated boundary in both directions. A child can report progress back to the parent:
E: event 'progress
on.with:'p E [print ["progress:" p]]
t: do.async [
pause 50
emit.with: 25 event 'progress ; child → parent
pause 50
emit.with: 100 event 'progress
"done"
]
print wait t ; progress: 25 / progress: 100 / done
…and the parent can push to a child that's driving its own dispatcher:
t: do.async [
on.with:'msg event 'tick [...]
wait do.async [pause 200] ; pumps the inbound queue
]
emit.with: "hello" event 'tick ; reaches the child
Warning
Built-in events stay local: anemit CtrlCfrom a child does not reach the parent'sCtrlChandler. Only user-defined events cross the process boundary, and payloads are limited to round-trippable values (integers, strings, blocks, dictionaries, logicals, null).