Events
ARO is fundamentally event-driven. Feature sets respond to events rather than being called directly. This chapter explains how events work and how to build event-driven applications.
Event-Driven Architecture
In ARO, feature sets are triggered by events, not called directly:
┌─────────────────────────────────────────────────────────────┐
│ Event Bus │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTPRequest ───► (listUsers: User API) [via operationId] │
│ │
│ FileCreated ───► (Process: FileCreated Handler) │
│ │
│ ClientConnected ─► (Handle: ClientConnected Handler) │
│ │
└─────────────────────────────────────────────────────────────┘
Event Types
HTTP Events (Contract-First)
ARO uses contract-first HTTP development. Routes are defined in openapi.yaml, and feature sets are named after operationId values:
openapi.yaml:
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users:
get:
operationId: listUsers
post:
operationId: createUser
/users/{id}:
get:
operationId: getUser
handlers.aro:
(* Triggered by GET /users - matches operationId *)
(listUsers: User API) {
<Retrieve> the <users> from the <repository>.
<Return> an <OK: status> with <users>.
}
(* Triggered by POST /users *)
(createUser: User API) {
<Extract> the <data> from the <request: body>.
<Create> the <user> with <data>.
<Return> a <Created: status> with <user>.
}
(* Triggered by GET /users/123 *)
(getUser: User API) {
<Extract> the <id> from the <pathParameters: id>.
<Retrieve> the <user> from the <repository> where id = <id>.
<Return> an <OK: status> with <user>.
}
File System Events
Triggered by file system changes:
(* File created *)
(Process New File: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Read> the <content> from the <file: path>.
<Process> the <result> from the <content>.
<Return> an <OK: status> for the <processing>.
}
(* File modified *)
(Reload Config: FileModified Handler) {
<Extract> the <path> from the <event: path>.
if <path> is "./config.json" then {
<Read> the <config> from the <file: path>.
<Publish> as <app-config> <config>.
}
<Return> an <OK: status> for the <reload>.
}
(* File deleted *)
(Log Deletion: FileDeleted Handler) {
<Extract> the <path> from the <event: path>.
<Log> the <message> for the <console> with "File deleted: ${path}".
<Return> an <OK: status> for the <logging>.
}
Socket Events
Triggered by TCP connections:
(* Client connected *)
(Handle Connection: ClientConnected Handler) {
<Extract> the <client-id> from the <event: connectionId>.
<Extract> the <address> from the <event: remoteAddress>.
<Log> the <message> for the <console> with "Client connected: ${address}".
<Return> an <OK: status> for the <connection>.
}
(* Data received *)
(Process Data: DataReceived Handler) {
<Extract> the <data> from the <event: data>.
<Extract> the <connection> from the <event: connection>.
<Process> the <response> from the <data>.
<Send> the <response> to the <connection>.
<Return> an <OK: status> for the <processing>.
}
(* Client disconnected *)
(Handle Disconnect: ClientDisconnected Handler) {
<Extract> the <client-id> from the <event: connectionId>.
<Log> the <message> for the <console> with "Client disconnected: ${client-id}".
<Return> an <OK: status> for the <cleanup>.
}
Handling Events
Handler Naming
Event handlers include "Handler" in the business activity:
(Feature Name: EventName Handler)
Examples:
(Index Content: FileCreated Handler) { ... }
(Reload Config: FileModified Handler) { ... }
(Echo Data: DataReceived Handler) { ... }
(Log Connection: ClientConnected Handler) { ... }
Accessing Event Data
Use <Extract> to get event data:
(Process Upload: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Extract> the <filename> from the <event: filename>.
<Read> the <content> from the <file: path>.
<Transform> the <processed> from the <content>.
<Store> the <processed> into the <processed-repository>.
<Return> an <OK: status> for the <processing>.
}
Multiple Handlers
Multiple handlers can respond to the same event:
(* Handler 1: Log the file *)
(Log Upload: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Log> the <message> for the <console> with "File uploaded: ${path}".
<Return> an <OK: status> for the <logging>.
}
(* Handler 2: Index the file *)
(Index Upload: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Read> the <content> from the <file: path>.
<Store> the <index-entry> into the <search-index>.
<Return> an <OK: status> for the <indexing>.
}
(* Handler 3: Notify admin *)
(Notify Upload: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Send> the <notification> to the <admin-channel>.
<Return> an <OK: status> for the <notification>.
}
All handlers execute independently when the event is emitted.
Built-in Events
Application Events
| Event | When Triggered |
|---|---|
ApplicationStarted |
After Application-Start completes |
ApplicationStopping |
Before Application-End runs |
File Events
| Event | When Triggered |
|---|---|
FileCreated |
File created in watched directory |
FileModified |
File modified in watched directory |
FileDeleted |
File deleted in watched directory |
FileRenamed |
File renamed in watched directory |
Socket Events
| Event | When Triggered |
|---|---|
ClientConnected |
TCP client connects |
DataReceived |
Data received from client |
ClientDisconnected |
TCP client disconnects |
Long-Running Applications
For applications that need to stay alive to process events (servers, file watchers, etc.), use the <Keepalive> action:
(Application-Start: File Watcher) {
<Log> the <startup: message> for the <console> with "Starting file watcher...".
(* Start watching a directory *)
<Watch> the <directory: "./uploads"> as <file-monitor>.
(* Keep the application running to process file events *)
<Keepalive> the <application> for the <events>.
<Return> an <OK: status> for the <startup>.
}
The <Keepalive> action:
- Blocks execution until a shutdown signal is received (SIGINT/SIGTERM)
- Allows the event loop to process incoming events
- Enables graceful shutdown with Ctrl+C
Best Practices
Keep Handlers Focused
(* Good - single responsibility *)
(Log File Upload: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
<Log> the <message> for the <console> with "Uploaded: ${path}".
<Return> an <OK: status> for the <logging>.
}
(* Avoid - too many responsibilities *)
(Handle File: FileCreated Handler) {
(* Don't do logging, indexing, notifications, and analytics in one handler *)
}
Handle Events Idempotently
Events may be delivered multiple times:
(Process File: FileCreated Handler) {
<Extract> the <path> from the <event: path>.
(* Check if already processed *)
<Retrieve> the <existing> from the <processed-files> where path = <path>.
if <existing> is not empty then {
(* Already processed - skip *)
<Return> an <OK: status> for the <idempotent>.
}
(* Process file *)
<Read> the <content> from the <file: path>.
<Transform> the <processed> from the <content>.
<Store> the <processed> into the <processed-files>.
<Return> an <OK: status> for the <processing>.
}
Next Steps
- Application Lifecycle - Startup and shutdown events
- HTTP Services - Contract-first HTTP routing
- File System - File system events