# events

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

This is the Markdown version of https://osl-lang.org/packages/events. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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:

```osl
import "std:events"

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

events.emit("greet", "ada")
events.emit("greet", "bob")
```

Output:

```text
hello ada
hello bob
```

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:

```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")
```

Output:

```text
2
billing: book
shipping: book
```

## Listening once

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

```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")
```

Output:

```text
1
ready fired
0
```

## 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:

```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)
```

Output:

```text
audit: first
true
false
```

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:

```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
```

Output:

```text
true
first listener failed
```

## 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:

```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()
```

Output:

```text
["compile","lint","test"]
```

## 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:

```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")
```

Output:

```text
cart now has 1 items
cart now has 2 items
```

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.

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

- [ ] Twice.
- [x] Once.
- [ ] Never, because `once` needs a second argument.

Answer: A `once` listener removes itself just before its first call.

### Exercise: 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.

Starting code:

```osl
import "std:events"

events.emit("temp", 25)
events.emit("temp", 35)
```

It should print:

```text
too hot: 35
```

Hint 1: `events.on("temp", def(int degrees) -> ( ... ))` goes before the emits.

Hint 2: Put an `if degrees > 30 ( ... )` inside the listener.

A solution:

```osl
import "std:events"

events.on("temp", def(int degrees) -> (
  if degrees > 30 (
    log "too hot: " ++ degrees
  )
))

events.emit("temp", 25)
events.emit("temp", 35)
```

## API reference

## API reference

Every member of `std:events`, generated from the source of OSL.

### 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.
