Packages

events

Announce named events inside one program and run every listener registered for them, in order.

import "std:events"

std:events lets one part of a program announce that something happened and another part react, without either knowing about the other. Events are named by strings: any code can listen for a name and any code can emit it. It works inside one program, across threads, and is not a network or message-queue tool.

Listen and emit#

events.on(name, fn) registers a listener. events.emit(name, args...) calls every listener of that name with the arguments, in the order they were added, and returns when they have all run:

main.osl
import "std:events"

events.on("greet", def(string name) -> (
  log "hello " ++ name
))

events.emit("greet", "ada")
events.emit("greet", "bob")
Click the code to edit it

Listeners run on the thread that calls emit, so a slow listener slows the emitter. Emitting an event nobody listens to does nothing.

More than one listener#

Each registration is called, so several parts of a program can react to the same event. events.count(name) says how many listeners an event has:

main.osl
import "std:events"

events.on("order", def(string item) -> (
  log "billing: " ++ item
))
events.on("order", def(string item) -> (
  log "shipping: " ++ item
))

log events.count("order")
events.emit("order", "book")
Click the code to edit it

Listening once#

events.once(name, fn) registers a listener that removes itself just before its first call:

main.osl
import "std:events"

events.once("ready", def() -> (
  log "ready fired"
))

log events.count("ready")
events.emit("ready")
events.emit("ready")
log events.count("ready")
Click the code to edit it

Removing a listener#

events.off(name, fn) removes the earliest registration of fn for that event, whether it was added with on or once, and says whether there was one to remove. To remove a listener you need to keep the function in a variable or give it a name:

main.osl
import "std:events"

def audit(string what) -> (
  log "audit: " ++ what
)

events.on("save", audit)
events.emit("save", "first")
log events.off("save", audit)
events.emit("save", "second")
log events.off("save", audit)
Click the code to edit it

The same function added twice is two listeners and is called twice. Each off removes one of them. Listeners added or removed while an event is being emitted take effect from the next emit.

When a listener throws#

An error thrown by a listener propagates to the code that called emit, and the listeners after it are not called. Use try around emit if one bad listener shouldn't stop the caller:

main.osl
import "std:events"

events.on("job", def() -> (
  throw error("JobError", "first listener failed")
))
events.on("job", def() -> (
  log "never reached"
))

auto outcome = try(events.emit("job"))
log outcome.isErr()
log outcome.unwrapErr().message
Click the code to edit it

Events across threads#

Listeners can be added, removed and emitted from any thread. A program can emit from several async functions and collect what listeners saw. Shared data is safe by default, and the order the threads reach emit is not fixed, so sort before printing:

main.osl
import "std:events"

string[] seen = []

events.on("finished", def(string name) -> (
  seen.append(name)
))

async work(string name, int ms) string (
  thread.sleep(ms)
  events.emit("finished", name)
  return name
)

auto jobs = [work("compile", 30), work("lint", 10), work("test", 20)]
void thread.waitAll(jobs)

log seen.sort()
Click the code to edit it

A small event bus#

Wrapping the event names in a class keeps the strings in one place and gives callers typed methods instead of bare names:

main.osl
import "std:events"

class Cart (
  string[] items = []

  def add(string item) (
    self.items.append(item)
    events.emit("cart:changed", self.items.len)
  )
)

events.on("cart:changed", def(int count) -> (
  log "cart now has " ++ count ++ " items"
))

auto cart = Cart()
cart.add("book")
cart.add("pen")
Click the code to edit it

A listener's parameters aren't checked against what emit sends. A listener that expects an int but is emitted with nothing fails with a type error, and one that is given extra arguments ignores them, so keep each event's arguments the same everywhere.

Quick check

events.once("ready", f) is registered, then events.emit("ready") is called twice. How many times does f run?

Your turn

Too hot

Register a listener for "temp" that logs too hot: and the temperature, but only when it is above 30. The two emits are already written.

main.osl
Ctrl Enter runs
Expected output
too hot: 35

API reference#

5 members, generated from the std:events source of OSL 9.3.0.

Functions

#events.count(string event) int

count returns how many listeners event has.

#events.emit(string event, any ...args)

emit calls every listener of event with args, in the order they were added, on the calling thread, and returns when they have all run. Listeners added or removed during the emit take effect from the next one. An error a listener throws propagates to the caller of emit and the remaining listeners are not called.

#events.off(string event, any fn) boolean

off removes the earliest registration of fn for event, whether added with on or once, and reports whether there was one to remove. Throws TypeError when fn isn't a function.

#events.on(string event, any fn)

on adds fn as a listener for event. Every registration is called, in the order added, each time the event is emitted, so adding a function twice calls it twice. Throws TypeError when fn isn't a function.

#events.once(string event, any fn)

once adds fn as a listener that is removed just before its first call. Throws TypeError when fn isn't a function.