-
Notifications
You must be signed in to change notification settings - Fork 0
Guide Repositories
Repositories provide persistent in-memory storage that survives across HTTP requests and event handlers. Unlike regular variables which are scoped to a single feature set execution, repositories maintain state for the lifetime of the application.
In ARO, each HTTP request creates a fresh execution context. Variables defined in one request aren't available in another:
(* This won't work - count resets on each request *)
(GET /count: Counter API) {
Create the <count> with 0.
Compute the <new-count> from <count> + 1.
Return an <OK: status> with <new-count>.
}
Repositories solve this by providing shared storage:
(POST /increment: Counter API) {
Retrieve the <counts> from the <counter-repository>.
Compute the <current> from <counts: length>.
Store the <current> into the <counter-repository>.
Return an <OK: status> with <current>.
}
(GET /count: Counter API) {
Retrieve the <counts> from the <counter-repository>.
Compute the <total> from <counts: length>.
Return an <OK: status> with { count: <total> }.
}
Repository names must end with -repository. This is how ARO distinguishes repositories from regular variables:
(* These are repositories *)
<user-repository>
<message-repository>
<order-repository>
<session-repository>
(* These are NOT repositories - just regular variables *)
<users>
<messages>
<user-data>
The naming convention:
- Makes repositories visually distinct in code
- Enables automatic persistence by the runtime
- Follows ARO's self-documenting code philosophy
Use the Store action to save data to a repository:
Store the <data> into the <name-repository>.
All of these are equivalent:
Store the <user> into the <user-repository>.
Store the <user> in the <user-repository>.
Store the <user> to the <user-repository>.
Repositories use list-based storage. Each store operation appends to the list:
(* First request *)
Store the <user1> into the <user-repository>.
(* Repository: [user1] *)
(* Second request *)
Store the <user2> into the <user-repository>.
(* Repository: [user1, user2] *)
(* Third request *)
Store the <user3> into the <user-repository>.
(* Repository: [user1, user2, user3] *)
(postMessage: Chat API) {
Extract the <data> from the <request: body>.
Extract the <text> from the <data: message>.
Extract the <author> from the <data: author>.
Create the <message> with {
text: <text>,
author: <author>,
timestamp: now
}.
Store the <message> into the <message-repository>.
Return a <Created: status> with <message>.
}
Use the Retrieve action to fetch data from a repository:
Retrieve the <items> from the <name-repository>.
- Returns a list of all stored items
- Returns an empty list
[]if the repository is empty or doesn't exist - Never throws an error for missing repositories
(getMessages: Chat API) {
Retrieve the <messages> from the <message-repository>.
Return an <OK: status> with { messages: <messages> }.
}
Use where to filter results:
(getUserById: User API) {
Extract the <id> from the <pathParameters: id>.
Retrieve the <user> from the <user-repository> where id = <id>.
Return an <OK: status> with <user>.
}
Use specifiers to retrieve a single item from a repository:
(* Get the most recently stored item *)
Retrieve the <message> from the <message-repository: last>.
(* Get the first stored item *)
Retrieve the <message> from the <message-repository: first>.
(* Get by numeric index - 0 = most recent *)
Retrieve the <latest> from the <message-repository: 0>.
Retrieve the <second-latest> from the <message-repository: 1>.
Numeric indices count from most recently added (0 = newest, 1 = second newest, etc.).
This is useful when you only need one item, like the latest message in a chat:
(getLatestMessage: Chat API) {
Retrieve the <message> from the <message-repository: last>.
Return an <OK: status> with { message: <message> }.
}
If the repository is empty, an empty string is returned.
Repositories are scoped to their business activity. Feature sets with the same business activity share repositories:
(* Same business activity: "Chat API" *)
(* These share the same <message-repository> *)
(postMessage: Chat API) {
Store the <message> into the <message-repository>.
Return a <Created: status>.
}
(getMessages: Chat API) {
Retrieve the <messages> from the <message-repository>.
Return an <OK: status> with <messages>.
}
(deleteMessage: Chat API) {
(* Same repository as above *)
Retrieve the <messages> from the <message-repository>.
(* ... *)
}
(* Business activity: "Chat API" *)
(postMessage: Chat API) {
Store the <msg> into the <message-repository>.
}
(* Business activity: "Admin API" - DIFFERENT repository! *)
(postAuditLog: Admin API) {
(* This <message-repository> is separate from Chat API's *)
Store the <log> into the <message-repository>.
}
This scoping:
- Prevents accidental data leakage between domains
- Allows reuse of generic repository names
- Enforces domain boundaries
(Application-Start: Simple Chat) {
Log "Starting Simple Chat..." to the <console>.
Start the <http-server> for the <contract>.
Keepalive the <application> for the <events>.
Return an <OK: status> for the <startup>.
}
(* GET /status - Return the last message *)
(getStatus: Simple Chat API) {
Retrieve the <message> from the <message-repository: last>.
Return an <OK: status> with { message: <message> }.
}
(* POST /status - Store a new message *)
(postStatus: Simple Chat API) {
Extract the <message> from the <body: message>.
Store the <message> into the <message-repository>.
Return a <Created: status> with { message: <message> }.
}
# Post a message
curl -X POST http://localhost:8080/status \
-H 'Content-Type: application/json' \
-d '{"message":"Hello!"}'
# Response: {"message":"Hello!"}
# Post another message
curl -X POST http://localhost:8080/status \
-H 'Content-Type: application/json' \
-d '{"message":"World!"}'
# Response: {"message":"World!"}
# Get the last message
curl http://localhost:8080/status
# Response: {"message":"World!"}Use the Delete action with a where clause to remove items from a repository:
Delete the <user> from the <user-repository> where id = <userId>.
(deleteUser: User API) {
Extract the <userId> from the <pathParameters: id>.
Delete the <user> from the <user-repository> where id = <userId>.
Return an <OK: status> with { deleted: <userId> }.
}
The deleted item(s) are bound to the result variable (user in this example).
Repository observers are feature sets that automatically react to repository changes. They receive access to both old and new values, enabling audit logging, synchronization, and reactive patterns.
Create an observer by naming your feature set's business activity as {repository-name} Observer:
(Audit Changes: user-repository Observer) {
Extract the <changeType> from the <event: changeType>.
Extract the <newValue> from the <event: newValue>.
Extract the <oldValue> from the <event: oldValue>.
Log <changeType> to the <console>.
Return an <OK: status> for the <audit>.
}
Observers receive an event with the following fields:
| Field | Type | Description |
|---|---|---|
repositoryName |
String | The repository name (e.g., "user-repository") |
changeType |
String | "created", "updated", or "deleted" |
entityId |
String? | ID of the changed entity (if available) |
newValue |
Any? | The new value (nil for deletes) |
oldValue |
Any? | The previous value (nil for creates) |
timestamp |
Date | When the change occurred |
Observers are triggered for three types of changes:
- created: New item stored (no previous value existed with matching ID)
- updated: Existing item modified (matched by ID)
-
deleted: Item removed using
Deleteaction
Observers fire only for actual changes to the repository:
| Change Type | Trigger Condition | Observer Fires? |
|---|---|---|
created |
New entry stored (value didn't exist) | Yes |
updated |
Existing entry modified (matched by ID) | Yes |
deleted |
Entry removed via Delete action |
Yes |
| duplicate | Same value stored again | No |
Important: Storing a duplicate value is a no-op — the repository ignores it and no observer event fires. This provides automatic deduplication without manual checks.
(* First store: observer fires with changeType = "created" *)
Store the <url> into the <crawled-repository>.
(* Second store of same URL: no observer fires *)
Store the <url> into the <crawled-repository>.
A common anti-pattern is combining Store with Emit in the same handler:
(* Anti-pattern: Store + Emit *)
Store the <url> into the <crawled-repository>.
Emit a <ProcessUrl: event> with { url: <url> }. (* Redundant! *)
Prefer observers instead. Handlers that store data shouldn't also emit events for the same logical action — that's what repository observers are for:
(Queue URL: QueueUrl Handler) {
(* Just store — observer handles the rest *)
Store the <url> into the <crawled-repository>.
Return an <OK: status>.
}
(Process New URLs: crawled-repository Observer) {
(* Only fires for genuinely new URLs *)
Extract the <url> from the <event: newValue>.
Emit a <ProcessUrl: event> with { url: <url> }.
Return an <OK: status>.
}
This pattern provides:
- Separation of concerns: Handlers store, observers react
- Automatic deduplication: Observers only fire for new entries
- Extensibility: Add more observers without changing the handler
(Track User Changes: user-repository Observer) {
Extract the <changeType> from the <event: changeType>.
Extract the <entityId> from the <event: entityId>.
Compare the <changeType> equals "updated".
Extract the <oldName> from the <event: oldValue: name>.
Extract the <newName> from the <event: newValue: name>.
Compute the <message> from "User " + <entityId> + " renamed from " + <oldName> + " to " + <newName>.
Log <message> to the <console>.
Return an <OK: status> for the <tracking>.
}
You can have multiple observers for the same repository:
(* Audit logging observer *)
(Log All Changes: user-repository Observer) {
Extract the <changeType> from the <event: changeType>.
Log <changeType> to the <console>.
Return an <OK: status>.
}
(* Email notification observer *)
(Notify Admin: user-repository Observer) {
Extract the <changeType> from the <event: changeType>.
Compare the <changeType> equals "deleted".
Send the <notification> to the <admin-email>.
Return an <OK: status>.
}
Repositories persist for the lifetime of the application:
- Created when first accessed
- Survive across all HTTP requests
- Cleared when application restarts
Repositories are in-memory only:
- Data is lost when the application stops
- No external database required
- Fast and simple for prototyping
For persistent storage, use a database integration (future ARO feature).
(* Good - clear what's stored *)
<user-repository>
<pending-order-repository>
<session-token-repository>
(* Avoid - too generic *)
<data-repository>
<stuff-repository>
(* Good - separate repositories for different concepts *)
<user-repository>
<order-repository>
<product-repository>
(* Avoid - mixing concepts *)
<everything-repository>
Store simple, serializable data:
(* Good - simple object *)
Create the <user> with {
id: <id>,
name: <name>,
email: <email>
}.
Store the <user> into the <user-repository>.
(* Avoid - complex nested structures *)
Store the <entire-request-context> into the <debug-repository>.
Fundamentals
- The Basics
- Feature Sets
- Actions
- Variables
- Type System
- Control Flow
- Error Handling
- Computations
- Dates
- Concurrency
Runtime & Events
I/O & Communication
Advanced