← Back to Documentation

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:

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