///|
/// The central event dispatcher for agent lifecycle events.
///
/// `EventTarget` implements an observer pattern where multiple listeners can
/// subscribe to receive events. Events are queued and processed asynchronously,
/// ensuring that event emission is always non-blocking.
///
/// # Architecture
///
/// ```text
/// emit() ──▶ [Queue] ──▶ start() ──▶ [Listener 1]
/// ──▶ [Listener 2]
/// ──▶ [Listener N]
/// ```
///
/// # Threading Model
///
/// - `emit()` is synchronous and non-blocking (enqueues event)
/// - `start()` runs an async event loop that dispatches to listeners
/// - Listeners are invoked sequentially for each event
struct EventTarget {
uuid : @uuid.Generator
clock : &@clock.Clock
// TODO: Consider adding a `Closed` event variant instead of using `Event?`.
// This would make the termination signal explicit in the type system rather
// than using `None` as a sentinel value.
queue : @aqueue.Queue[Event]
listeners : Array[async (Event) -> Unit]
}
///|
/// Creates a new `EventTarget` with an empty listener list.
///
/// The event target uses an unbounded queue to ensure `emit()` never blocks.
/// Remember to call `start()` in a background task to begin processing events.
///
/// # Returns
///
/// A new `EventTarget` instance ready to receive listeners and events.
///
/// # Example
///
/// ```moonbit no-check
/// let emitter = EventTarget::new()
/// emitter.add_listener(async fn(event) { println(event) })
/// // Start in background
/// spawn(() => emitter.start())
/// ```
pub fn EventTarget::new(
uuid? : @uuid.Generator,
clock? : &@clock.Clock = @clock.epoch,
) -> EventTarget raise {
let uuid = match uuid {
None => @uuid.generator(@rand.chacha8())
Some(uuid) => uuid
}
{ uuid, clock, queue: @aqueue.Queue::new(kind=Unbounded), listeners: [] }
}
///|
/// Emits an event to be processed by all registered listeners.
///
/// This method enqueues the event for asynchronous processing. The event will
/// be dispatched to all listeners when `start()` processes it from the queue.
///
/// # Parameters
///
/// - `event`: The `Event` to emit.
///
/// # Behavior
///
/// - **Non-blocking**: Returns immediately after enqueuing.
/// - **Order-preserving**: Events are processed in FIFO order.
///
/// # Panics
///
/// Aborts if the queue is full (should not happen with unbounded queue).
///
/// # Example
///
/// ```moonbit no-check
/// emitter.emit(PreConversation)
/// emitter.emit(TokenCounted(1500))
/// emitter.emit(PostConversation)
/// ```
pub fn EventTarget::emit(
self : EventTarget,
desc : EventDesc,
id? : @uuid.Uuid = self.uuid.v4(),
) -> Unit {
let success = self.queue.try_put({ id, created: self.clock.now(), desc }) catch {
_ => abort("Event queue is closed, cannot emit event")
}
guard success else { abort("Event queue is full, cannot emit event") }
}
///|
/// Registers an async listener function to receive events.
///
/// Listeners are called sequentially for each event in the order they were
/// registered. Each listener receives every event emitted after registration.
///
/// # Parameters
///
/// - `f`: An async function that takes an `Event` and returns `Unit`.
///
/// # Example
///
/// ```moonbit no-check
/// emitter.add_listener(async fn(event) {
/// match event {
/// PostToolCall(call, result~, rendered~) => {
/// // Log tool call results
/// println("Tool \(call.name) completed")
/// }
/// AssistantMessage(usage~, message~) => {
/// // Track token usage
/// if usage is Some(u) {
/// total_tokens += u.total_tokens
/// }
/// }
/// _ => ()
/// }
/// })
/// ```
pub fn EventTarget::add_listener(
self : EventTarget,
f : async (Event) -> Unit,
) -> Unit {
self.listeners.push(f)
}
///|
/// Starts the event processing loop.
///
/// This async function runs continuously, waiting for events from the queue
/// and dispatching them to all registered listeners. It blocks until a `None`
/// sentinel is received (via `close()`).
///
/// # Behavior
///
/// - **Blocking**: Waits for events when queue is empty.
/// - **Sequential dispatch**: Listeners are called one at a time per event.
/// - **Terminates**: Exits when `close()` sends the termination signal.
///
/// # Usage
///
/// Should typically be spawned as a background task:
///
/// ```moonbit no-check
/// @async.with_task_group((group) => {
/// group.spawn_bg(() => { emitter.start() }, no_wait=true)
/// // ... rest of the application
/// })
/// ```
pub async fn EventTarget::start(self : EventTarget) -> Unit {
while true {
let event = self.queue.get()
let errors = []
for listener in self.listeners {
listener(event) catch {
error => errors.push(error)
}
}
if errors is [error, ..] {
raise error
}
}
}
///|
/// Immediately processes all pending events in the queue.
///
/// Unlike `start()`, this method does not wait for new events. It processes
/// all currently queued events and returns, making it useful for ensuring
/// all events are handled before a checkpoint.
///
/// # Behavior
///
/// - **Non-blocking on empty**: Returns immediately if queue is empty.
/// - **Draining**: Processes all pending events.
/// - **Does not terminate**: Does not affect the `start()` loop.
///
/// # Example
///
/// ```moonbit no-check
/// // Ensure all events are processed before saving state
/// emitter.flush()
/// save_checkpoint()
/// ```
/// CR: `listener` is suspendable, should we collcect all events
/// and then call listeners outside of the loop to avoid racing?
/// assuming listerns can take a long time
pub async fn EventTarget::flush(self : EventTarget) -> Unit {
while true {
let event = self.queue.try_get() catch { _ => None }
guard event is Some(event) else { break }
for listener in self.listeners {
listener(event)
}
}
}