Alerts: Spaceport's Event System
Alerts are Spaceport's powerful event-driven hook system that allows your application code to respond to events
throughout the request lifecycle, database operations, and custom application events. By annotating static methods
with @Alert, you can hook into dozens of built-in events or create your own custom event flows.
Think of Alerts as Spaceport's central nervous system—they connect your application logic to every significant event that occurs, from HTTP requests hitting your server to documents being saved in the database. This declarative approach helps keeps your code organized and makes it easy to see exactly what happens when events fire.
# Quick Reference
Use this table for fast lookup of common event patterns:
| Pattern | Example | When It Fires | Result Type |
|---|---|---|---|
on [route] hit |
on /api/users hit |
Any HTTP request to route | HttpResult |
on [route] [METHOD] |
on /api/users POST |
Specific HTTP method to route | HttpResult |
~on [regex] hit |
~on /users/([^/]*) hit |
Regex match (captures in r.matches) |
HttpResult |
on page hit |
on page hit |
Every HTTP request | HttpResult |
on document saved |
on document saved |
After any document save | Result |
on socket data |
on socket data |
WebSocket message received | SocketResult |
on initialize |
on initialize |
Application startup | Result |
Priority reminder: Higher numbers run first. Default is 0. Use negative numbers for fallback/cleanup handlers.
# Core Concepts
The Alerts system is built on a few key concepts that work together to provide a flexible, performant event system.
## Alert Annotations
The @Alert annotation marks a static method as an event handler. When an event with a matching name is invoked
anywhere in Spaceport, your method will be called automatically.
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult
class MyRouter {
@Alert('on /api/users hit')
static _handleUsers(HttpResult r) {
// Writing a Groovy Map automatically serializes to JSON
r.writeToClient([ 'users': [ 'alice', 'bob' ]])
}
}
Key Requirements:
- The method must be static
- The method must accept exactly one parameter (the Result context object, or a subclass)
- The annotation value is the event string to listen for
## Event Strings
Event strings identify which event an alert should respond to. Spaceport uses a consistent naming convention for built-in events, but you can invoke custom events with any string you choose.
Note: All event string matching is case insensitive. This applies to both literal strings and regex patterns. For example,on /API/Users hitwill matchon /api/users hit.
Some Built-in Event Patterns:
on initialize- Application startup (source modules loaded)on deinitialize- Before hot reload cleanup (debug mode only)on [route] hit- Any HTTP request to a routeon [route] [METHOD]- HTTP request with specific method (POST, PUT, DELETE, etc.)on page hit- Any HTTP page request (fires for every request)on document created- A new document is createdon document saved- A document is saved to the databaseon socket connect- WebSocket connection establishedon socket data- WebSocket message received
See Built-in Events Reference for the complete list.
## The Result Object
Every alert handler receives a Result object (or a specialized subclass) that contains:
- Context data about the event (request details, document info, etc.)
- Control flags like
cancelledto stop further processing - Match data from regex-based event strings (if applicable)
The Result object is both an input (providing context) and an output (allowing you to signal and mutate outcomes).
## Alert Priority
When multiple alerts listen to the same event, they execute in priority order (highest first). This allows you to control the sequence of execution for middleware-style patterns.
@Alert(value = 'on page hit', priority = 100)
static _securityCheck(HttpResult r) {
// Runs first - high priority
if (!r.client.isAuthenticated()) {
r.setRedirectUrl('/login')
r.cancelled = true // Stop other alerts
}
}
@Alert('on page hit') // Default priority = 0
static _logRequest(HttpResult r) {
// Runs after security check, if not cancelled
println "Request to ${ r.context.target }."
}
@Alert(value = 'on page hit', priority = -100)
static _fallbackHandler(HttpResult r) {
// Runs last - negative priority
// Good for 404 handlers or cleanup
if (!r.called) {
r.setStatus(404)
r.writeToClient('Not Found')
}
}
Priority Guidelines:
| Priority Range | Use Case |
|---|---|
| 100+ | Security checks, authentication |
| 50-99 | Request preprocessing, CORS |
| 1-49 | Business logic middleware |
| 0 (default) | Standard route handlers |
| -1 to -49 | Post-processing, logging |
| -50 to -100 | Fallback handlers, 404 pages |
## Error Handling Behavior
When an alert handler throws an exception, Spaceport catches the error, prints the stack trace, and continues processing remaining alerts. This means:
- One failing alert won't crash other handlers
- Errors are logged but don't halt the request pipeline
- You should implement your own error handling for critical operations
@Alert('on /api/data hit')
static _mightFail(HttpResult r) {
try {
// Your risky operation
def result = SomeExternalService.fetch()
r.writeToClient(result)
} catch (Exception e) {
// Handle gracefully instead of letting it propagate
r.setStatus(500)
r.writeToClient(['error': 'Service unavailable'])
r.cancelled = true // Stop further processing
}
}
# Basic Usage
Let's start with some common patterns for using alerts in your application. While alerts can be used for a wide variety of purposes, these examples cover some typical use cases that you'll certainly encounter.
## Application Lifecycle
Hook into startup and shutdown events to initialize resources, transform assets, or perform cleanup.
Spaceport provides four lifecycle events that fire in a specific sequence:
| Event | When It Fires | Use Case |
|---|---|---|
on initialize |
After source modules are loaded | Initialize resources, create databases |
on initialized |
After all on initialize handlers complete |
Start background tasks, confirm startup |
on deinitialize |
Before hot reload cleanup begins (debug mode) | Prepare for shutdown, stop accepting work |
on deinitialized |
After cleanup completes (debug mode) | Release external resources, confirm shutdown |
import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
class AppLifecycle {
static boolean running = false
@Alert('on initialize')
static _startup(Result r) {
println "Phase 1: Initializing resources..."
// Ensure required databases exist
if (!Spaceport.main_memory_core.containsDatabase('users')) {
Spaceport.main_memory_core.createDatabase('users')
}
}
@Alert('on initialized')
static _startupComplete(Result r) {
println "Phase 2: All initialization complete, starting services..."
// Safe to start background tasks now
running = true
SomeExternalSystem.initialize()
}
@Alert('on deinitialize')
static _prepareShutdown(Result r) {
println "Phase 3: Preparing for hot reload..."
// Stop accepting new work
running = false
}
@Alert('on deinitialized')
static _shutdownComplete(Result r) {
println "Phase 4: Cleanup complete, releasing resources..."
// Release external connections
SomeExternalSystem.shutdown()
}
}
Note: Thedeinitializeanddeinitializedevents only fire in debug mode during hot reloads. In production, they won't fire on normal shutdown.
## Handling HTTP Routes
The most common use of alerts is routing HTTP requests. Use the on [route] hit pattern for any request, or
on [route] [METHOD] for specific HTTP methods. Note the case sensitivity of the method name—always use uppercase
here (GET, POST, DELETE, etc). Both patterns are invoked in the same queue, so priority controls execution order
whether specifying a method or not.
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult
class ProductRouter {
@Alert('on /system/ping hit')
static _ping(HttpResult r) {
r.writeToClient('pong!')
}
// Handle GET /products
@Alert('on /products GET')
static _listProducts(HttpResult r) {
// Your application logic to fetch products
def products = fetchAllProducts()
// Spaceport's HttpResult offers a way to write the response to the client
r.writeToClient(products)
}
// Handle POST /products
@Alert('on /products POST')
static _createProduct(HttpResult r) {
// Extract data from the request using Spaceport's context
def name = r.context.data.name
def price = r.context.data.getNumber('price')
// Your application logic to create the product
def product = createProduct(name, price)
// Use Spaceport's HttpResult to set a specific status and write a JSON response
r.setStatus(201)
r.writeToClient(['id': product._id])
}
// Handle DELETE /products/123
@Alert('~on /products/([^/]*) DELETE')
static _deleteProduct(HttpResult r) {
def productId = r.matches[0] // Captured from regex by Spaceport
if (deleteProduct(productId))
r.setStatus(204) // No content
else
r.setStatus(404) // Not found
}
}
## Global Request Middleware
Use on page hit to run code for every HTTP request, perfect for logging, authentication, or adding common headers.
Use the context available to determine the request details with a finer grain. When deciding whether to use on page hit
or a more specific route, even a catch-all like ~on /(.*) hit, know that on page hit has its own priority queue and
that it will fire after the on [route] hit and on [route] [METHOD] alerts. This allows you to implement global middleware
that runs consistently regardless of route matching.
class Middleware {
@Alert(value = 'on page hit', priority = 50)
static _cors(HttpResult r) {
// Use addResponseHeader to append headers, or setResponseHeader to overwrite
r.setResponseHeader('Access-Control-Allow-Origin', '*')
r.setResponseHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE')
}
@Alert('on page hit')
static _logging(HttpResult r) {
def start = System.currentTimeMillis()
def method = r.context.method // GET, POST, etc.
println "${ new Date() }: Processed ${ method } request to ${ r.context.target } for ${ r.client?.userID ?: 'anonymous' }"
}
}
See Routing for more details on handling HTTP requests with alerts.
## Document Lifecycle Hooks
React to database events to implement audit logs, cache invalidation, or derived data updates.
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document
class AuditLog {
@Alert('on document created')
static _logCreation(Result r) {
// Spaceport provides the document and context
def doc = r.context.doc
println "Document created: ${doc._id} in ${r.context.database}"
}
@Alert('on document saved')
static _invalidateCache(Result r) {
def doc = r.context.doc
// Your caching system
YourCacheSystem.remove(doc._id)
}
}
# Regex-Based Event Matching
Alert strings that start with ~ are treated as regular expressions, allowing you to capture dynamic route parameters
or create flexible event patterns.
Important: All regex matching is case insensitive.
## Capturing Route Parameters
The most common use of regex alerts is capturing URL path segments.
class ArticleRouter {
// Match: /articles/123, /articles/my-post-slug, etc.
// Using [^/]* to capture only the segment (not including slashes)
@Alert('~on /articles/([^/]*) hit')
static _viewArticle(HttpResult r) {
// Check if we got a match
if (r.matches.isEmpty()) return
def articleId = r.matches[0]
def article = Article.fetchById(articleId)
if (!article) {
r.setStatus(404)
r.writeToClient("Article not found")
return
}
new Launchpad().assemble(['article.ghtml']).launch(r)
}
// Match: /users/alice/posts/123
@Alert('~on /users/([^/])/posts/([^/]) hit')
static _viewUserPost(HttpResult r) {
def username = r.matches[0]
def postId = r.matches[1]
// Your application logic here
}
}
Regex Capture Group Patterns:
| Pattern | Matches | Use Case |
|---|---|---|
([^/]*) |
Any characters except / |
Single path segment |
(.*) |
Any characters including / |
Multiple path segments |
(\d+) |
Digits only | Numeric IDs |
([a-zA-Z0-9-]+) |
Alphanumeric and hyphens | URL slugs |
Example: Understanding the difference
// Using [^/]* - captures only "123"
@Alert('~on /users/([^/]*) hit')
// Matches: /users/123 → r.matches[0] = "123"
// Does NOT match: /users/123/posts (no match, different pattern)
// Using .* - captures everything after /users/
@Alert('~on /users/(.*) hit')
// Matches: /users/123 → r.matches[0] = "123"
// Matches: /users/123/posts → r.matches[0] = "123/posts"
// Matches: /users/123/posts/456 → r.matches[0] = "123/posts/456"
Important Notes:
- Captured groups are available in
r.matchesas a list of strings - Always check
r.matches.isEmpty()orr.matches.size()before accessing - The regex matches against the full event string (e.g.,
on /users/123 hit) - Spaces in the pattern are automatically treated as
\s(whitespace)
## Pattern Matching for Custom Events
You can also use regex for your own custom event hierarchies.
class NotificationHandler {
// Match any notification type
@Alert('~on notification\\.([^.]*)')
static _handleNotification(Result r) {
if (r.matches.isEmpty()) return
def notificationType = r.matches[0] // 'email', 'sms', 'push', etc.
println "Handling ${notificationType} notification"
// ... dispatch to specific handler
}
}
// Elsewhere in your code:
Alerts.invoke('on notification.email', [ message: 'Hello!' ])
Alerts.invoke('on notification.sms', [ message: 'Alert!' ])
# Result Types and Context Objects
Spaceport provides specialized Result classes for different event types. These classes extend the base Result and add
helper methods specific to their context.
## HttpResult (HTTP Requests)
Used for all HTTP-related alerts. Provides methods to manipulate the response.
@Alert('on /download hit')
static _downloadFile(HttpResult r) {
// Access request data from Spaceport's context
def filename = r.context.data.file
def userAgent = r.context.headers.'User-Agent'
// Use Spaceport's HttpResult API to set response properties
r.setContentType('application/pdf; charset=UTF-8')
r.setResponseHeader('Cache-Control', 'max-age=3600')
// Add cookies using Spaceport's cookie methods
r.addSecureResponseCookie('last-download', filename)
// Your application logic to locate the file
def file = new File("/downloads/${filename}")
// Use Spaceport's convenience methods to send the file
r.markAsAttachment(filename)
r.writeToClient(file) // Automatically handles file streaming
}
Available Context Properties:
| Property | Type | Description |
|---|---|---|
request |
HttpServletRequest |
Raw servlet request object |
response |
HttpServletResponse |
Raw servlet response object |
method |
String |
HTTP method (GET, POST, etc.) |
target |
String |
Request path (e.g., /api/users) |
time |
Long |
Request timestamp (epoch milliseconds) |
cookies |
Map |
Request cookies as a map |
headers |
Map |
Request headers as a map |
data |
Cargo |
Query parameters (GET) or form data (POST) |
dock |
Cargo |
Session-specific storage for this client |
client |
Client |
Authenticated client object (if logged in) |
Helper Methods:
| Method | Description |
|---|---|
setStatus(Integer) |
Set HTTP status code (200, 404, etc.) |
setContentType(String) |
Set Content-Type header |
addResponseHeader(name, value) |
Add a response header (allows duplicates) |
setResponseHeader(name, value) |
Set/replace a response header |
addResponseCookie(name, value) |
Add a cookie (7-day expiry, root path) |
addSecureResponseCookie(name, value) |
Add a secure, httpOnly cookie |
addResponseCookie(Cookie) |
Add a cookie with full control |
removeResponseCookie(name) |
Remove a cookie (sets max-age to 0) |
setRedirectUrl(String) |
Redirect to a URL (302) |
writeToClient(String) |
Write string response |
writeToClient(String, Integer) |
Write string with status code |
writeToClient(Map) |
Write JSON response (auto-sets content type) |
writeToClient(List) |
Write JSON array response |
writeToClient(File) |
Write file response (auto-detects content type) |
writeToClient(File, String) |
Write file with explicit content type |
markAsAttachment(String) |
Set Content-Disposition for download |
authorize(Closure) |
Run authorization check, returns boolean |
ensure(Closure) |
Run validation/setup closure |
getClient() |
Get the authenticated Client object |
See the HttpResult API Reference for the complete list.
## SocketResult (WebSocket Events)
Used for WebSocket-related alerts. Provides methods to send data back through the socket.
Note: WebSocket writes are asynchronous. ThewriteToRemote()methods usesendStringByFuture()internally, meaning they return immediately and the actual send happens in the background.
@Alert('on socket data')
static _handleMessage(SocketResult r) {
// Spaceport provides the parsed message data
def message = r.context.data.message
def sessionId = r.context.session.remote.inetSocketAddress
// Get the authenticated client (if any) via Spaceport
def client = r.client
// Your application logic to process the message
def response = YourMessageProcessor.handle(message, client)
// Send response through the WebSocket (async, non-blocking)
r.writeToRemote(['response': 'Message received', 'echo': message])
// Or close the connection using Spaceport's API
if (message == 'goodbye') {
r.close('Client requested disconnect')
}
}
Available Context Properties:
| Property | Type | Description |
|---|---|---|
request |
UpgradeRequest |
Original HTTP upgrade request |
session |
Session |
WebSocket session object |
handler |
SocketHandler |
Handler instance managing this socket |
handlerId |
String |
ID of the handler (for routing) |
time |
Long |
Event timestamp |
headers |
Map |
Request headers from upgrade |
cookies |
Map |
Cookies from upgrade request |
data |
Map |
Parsed JSON data from the client |
dock |
Cargo |
Session-specific storage |
client |
Client |
Authenticated client object |
Helper Methods:
| Method | Description |
|---|---|
getHandler() |
Get the SocketHandler instance |
getClient() |
Get the Client associated with this socket |
writeToRemote(String) |
Send a string message (async) |
writeToRemote(Map) |
Send a JSON message (async, pretty-printed) |
close() |
Close the socket connection |
close(String reason) |
Close with status 1000 and a reason message |
## Base Result (Custom Events)
For custom events or simpler built-in events, you'll receive the base Result class.
@Alert('on user registered')
static _sendWelcomeEmail(Result r) {
def user = r.context.user
def email = r.context.email
// Send email logic...
// You can cancel further processing if needed
if (!emailSent) {
r.cancelled = true
}
}
Base Result Properties:
| Property | Type | Description |
|---|---|---|
context |
Object/Map |
Event-specific context data |
called |
boolean |
Set to true when this alert fires |
cancelled |
boolean |
Set to true to stop further alert processing |
matches |
List |
Captured groups from regex event strings |
# Advanced Patterns
## Creating Custom Events
You can invoke your own custom events anywhere in your application to create decoupled, event-driven architectures. This pattern allows different parts of your system to react to domain events without tight coupling.
The Flow:
- Your core business logic performs an operation and invokes a custom event
- Spaceport broadcasts this event to all registered alert handlers
- Multiple independent handlers can react to the same event
- Each handler implements its own specific concern (email, inventory, analytics, etc.)
import spaceport.computer.Alerts
class OrderProcessor {
static void processOrder(Order order) {
// Your main business logic
order.status = 'processed'
order.save()
// Invoke a custom event - Spaceport notifies all listeners
Alerts.invoke('on order processed', [
order: order,
timestamp: System.currentTimeMillis()
])
}
}
// Multiple handlers can react independently to the same event
class EmailNotifier {
@Alert('on order processed')
static _sendConfirmation(Result r) {
def order = r.context.order
YourEmailService.send(order.customerEmail, "Order #${order.id} confirmed")
}
}
class InventoryManager {
@Alert('on order processed')
static _updateInventory(Result r) {
def order = r.context.order
order.items.each { item ->
YourInventorySystem.decrementStock(item.productId, item.quantity)
}
}
}
class AnalyticsTracker {
@Alert('on order processed')
static _trackSale(Result r) {
def order = r.context.order
YourAnalytics.trackEvent('sale', order.total)
}
}
## Broadcasting Multiple Events
Use Alerts.invokeAll() to broadcast multiple event strings in a single pass while respecting priority order. This is useful when an action should trigger multiple event types.
import spaceport.computer.Alerts
class OrderProcessor {
static void processOrder(Order order) {
order.status = 'processed'
order.save()
// Invoke multiple events in a single pass
// All handlers run in priority order across both event types
def result = Alerts.invokeAll([
'on order processed',
'on order.status.changed',
"on order.${order.type}.processed" // Dynamic event name
], [
order: order,
timestamp: System.currentTimeMillis()
])
if (result.cancelled) {
// Some handler cancelled processing
order.status = 'on-hold'
order.save()
}
}
}
## Using resultType for Custom Result Classes
For complex custom events, you can create your own Result subclass with domain-specific helper methods.
Why use a custom Result class?
- Encapsulate common operations that multiple handlers need
- Add type-safe accessors for your domain objects
- Include domain-specific validation or mutation methods
- Make handler code cleaner and more expressive
Step 1: Create your custom Result class
import spaceport.computer.alerts.results.Result
class OrderResult extends Result {
OrderResult(Object context) {
super(context)
}
// Type-safe accessor for the order
Order getOrder() {
return context.order as Order
}
// Type-safe accessor for the customer
Customer getCustomer() {
return context.customer as Customer
}
// Domain-specific operation
void markAsFailed(String reason) {
order.status = 'failed'
order.failureReason = reason
order.save()
this.cancelled = true // Stop further alert processing
}
// Validation helper
boolean isHighValueOrder() {
return order.total > 1000
}
}
Step 2: Specify your class when invoking
def context = [
order: myOrder,
customer: customer,
resultType: OrderResult // Tell Spaceport to use your custom class
]
def result = Alerts.invoke('on order submitted', context)
Step 3: Use in your handlers
@Alert(value = 'on order submitted', priority = 100)
static _validateOrder(OrderResult r) {
// Use your custom helper methods
if (r.order.total > r.customer.creditLimit) {
r.markAsFailed('Exceeds credit limit')
// No need to manually set cancelled - markAsFailed handles it
return
}
if (r.isHighValueOrder()) {
// Flag for manual review
r.order.requiresReview = true
}
}
@Alert('on order submitted')
static _processOrder(OrderResult r) {
// This only runs if validation passed (wasn't cancelled)
r.order.status = 'processing'
r.order.save()
}
## Cancelling Alert Chains
Set cancelled = true on the Result to prevent subsequent alerts from executing. This is useful for authentication,
authorization, or validation logic.
class AuthMiddleware {
@Alert(value = 'on page hit', priority = 100)
static _requireAuth(HttpResult r) {
// Skip auth for public routes
if (r.context.target.startsWith('/public/')) return
if (!r.client?.isAuthenticated()) {
r.setRedirectUrl('/public/login')
r.cancelled = true // Stop processing this request
}
}
}
class ProtectedRoutes {
// This will only run if auth check passes
@Alert('on /admin/dashboard hit')
static _showDashboard(HttpResult r) {
// Only authenticated users reach here
new Launchpad().assemble(['admin-dashboard.ghtml']).launch(r)
}
}
## Multi-Stage Processing Pipelines
Use priorities and shared context to build processing pipelines.
The Flow:
- High-priority alert validates the input (priority 30)
- If validation passes, mid-priority alert processes the data (priority 20)
- Low-priority alert saves the result (priority 10)
- Each stage can access results from previous stages via
r.context - Any stage can set
r.cancelled = trueto abort the pipeline
class ImageUploadPipeline {
@Alert(value = 'on /api/upload POST', priority = 30)
static _validateUpload(HttpResult r) {
def file = r.context.data.file
if (!YourImageValidator.isValidType(file)) {
r.setStatus(400)
r.writeToClient(['error': 'Invalid file type'])
r.cancelled = true
return
}
// Store validation result for next stage
r.context.validated = true
}
@Alert(value = 'on /api/upload POST', priority = 20)
static _processImage(HttpResult r) {
if (!r.context.validated) return
def file = r.context.data.file
def processed = YourImageProcessor.resizeAndOptimize(file)
// Store processed image for final stage
r.context.processedImage = processed
}
@Alert(value = 'on /api/upload POST', priority = 10)
static _saveImage(HttpResult r) {
if (!r.context.processedImage) return
def url = YourStorageSystem.save(r.context.processedImage)
r.writeToClient(['url': url])
}
}
# Built-in Events Reference
## HTTP Events
| Event String | Result Type | Description |
|---|---|---|
on page hit |
HttpResult |
Fires for every HTTP request |
on / hit |
HttpResult |
Any HTTP request to root path |
on [route] hit |
HttpResult |
Any HTTP request to specific route |
on [route] [METHOD] |
HttpResult |
Specific HTTP method to route |
Examples:
on /api/users hit- Any request to /api/userson /api/users POST- POST /api/users~on /articles/([^/]) hit- Any request to /articles/ with capture~on /users/([^/])/posts/([^/]) DELETE- DELETE with multiple captures
## WebSocket Events
| Event String | Result Type | Description |
|---|---|---|
on socket connect |
SocketResult |
WebSocket connection opened |
on socket data |
SocketResult |
Data received from client |
on socket [handler-id] |
SocketResult |
Routed to specific handler |
on socket closed |
SocketResult |
WebSocket connection closed |
## Document Events
| Event String | Result Type | Context Properties | Description |
|---|---|---|---|
on document created |
Result |
id (String): Document IDdatabase (String): Database namedoc (Document): The documentclassType (Class): Document class |
New document created |
on document save |
Result |
doc (Document): The document being saved |
Before document save (can modify) |
on document saved |
Result |
doc (Document): The saved document |
After document saved successfully |
on document modified |
Result |
type (String): Document typeaction (String): Action performeddocument (Document): The documentoperation (Object): Operation details |
Document modified in database |
on document remove |
Result |
type (String): Document typedocument (Document): Document to delete |
Before document deleted |
on document removed |
Result |
type (String): Document typedocument (Document): Deleted documentoperation (Object): Operation details |
After document deleted |
on document conflict |
Result |
Conflict details (revision info) | Save conflict detected |
## Application Lifecycle Events
| Event String | Result Type | When It Fires | Use Case |
|---|---|---|---|
on initialize |
Result |
After source modules are loaded | Create databases, initialize resources |
on initialized |
Result |
After all on initialize handlers complete |
Start background tasks, confirm startup |
on deinitialize |
Result |
Before hot reload cleanup (debug mode only) | Stop accepting work, prepare for shutdown |
on deinitialized |
Result |
After cleanup completes (debug mode only) | Release external resources |
## Authentication Events
| Event String | Result Type | Context Properties | Description |
|---|---|---|---|
on client auth |
Result |
user_id (String): User identifierclient (Client): Authenticated client |
Client authenticated successfully |
on client auth failed |
Result |
user_id (String): Attempted user IDexists (boolean): Whether user exists |
Authentication attempt failed |
# Common Gotchas
Watch out for these common mistakes and behaviors:
1. Event strings are case insensitive
Both literal and regex matching ignore case. on /API/Users hit matches on /api/users hit.
2. Spaces in regex become \s automatically
Don't double-escape spaces in your patterns. Write ~on /users/(.) hit, not ~on /users/(.)\\s+hit.
3. Errors in handlers don't stop other handlers
Exceptions are caught, printed, and processing continues. Add try/catch if you need guaranteed error handling.
4. WeakReferences mean GC can remove your handlers
If your handler class gets garbage collected, the alert stops working. Keep references to important handler classes.
5. r.called reflects whether ANY handler ran, not just yours
Check specific state you set, not the generic called flag, when building pipelines.
6. .matches may be empty even for regex alerts
Always check r.matches.isEmpty() or r.matches.size() before accessing indices.
7. Default priority is 0, not 1
If you don't specify priority, your handler runs after positive priorities and before negative ones.
# Performance Considerations
## Alert Registration Performance
- Alerts are registered once at startup (or during hot reload)
- Registration list is sorted by priority, so invocation is fast
- Regex patterns are compiled once at registration time
- Uses WeakReferences internally to allow garbage collection
## Invocation Performance
- String matching is very fast (case-insensitive equality check)
- Regex matching has some overhead, but is optimized with pre-compilation
- Consider priority carefully: high-priority alerts run first for every match
## Best Practices
- Use specific event strings when possible instead of broad catches with
on page hit - Minimize regex complexity for frequently-invoked events
- Return early in alert handlers when conditions aren't met
- Use priority to short-circuit unnecessary processing with
r.cancelled = true - Avoid heavy computation in high-frequency alerts (like
on page hit) - Use
[^/]instead of.when capturing single path segments - Check
r.matchesbounds before accessing to avoid IndexOutOfBoundsException
# Common Patterns and Recipes
## Basic Authentication Guard
class AuthGuard {
@Alert(value = 'on /(.*) hit', priority = 100)
static _checkAuth(HttpResult r) {
def publicPaths = ['/login', '/register', '/public', '/assets']
// Skip auth for public paths
if (publicPaths.any { r.context.target.startsWith(it) }) {
return
}
// Require authentication for everything else
if (!r.client?.hasPermission('authenticated')) {
r.setRedirectUrl('/login')
r.cancelled = true
}
}
}
## Request Logging
class RequestLogger {
@Alert('on page hit')
static _logRequest(HttpResult r) {
def method = r.context.method
def path = r.context.target
def ip = r.context.request.remoteAddr
def user = r.client?.userID ?: 'anonymous'
println "[${new Date()}] ${method} ${path} | ${user} | ${ip}"
}
}
## Automatic Cache Invalidation
class CacheInvalidator {
@Alert('on document saved')
static _invalidate(Result r) {
def doc = r.context.doc
// Clear the specific document cache
YourCacheSystem.remove("doc:${doc._id}")
// Clear any aggregate caches that might include this document
if (doc.type == 'article') {
YourCacheSystem.remove('article:list')
YourCacheSystem.remove('article:recent')
}
}
}
## Error Page Handling
class ErrorHandler {
@Alert(value = 'on page hit', priority = -100) // Very low priority - runs last
static _handle404(HttpResult r) {
// Only handle if no other handler responded
if (!r.called) {
r.setStatus(404)
new Launchpad().assemble(['errors/404.ghtml']).launch(r)
}
}
}
## Document Audit Trail
class AuditTrail {
@Alert('on document modified')
static _logChange(Result r) {
def auditEntry = Document.getNew('audit-log')
auditEntry.type = 'audit'
auditEntry.fields.documentId = r.context.document._id
auditEntry.fields.documentType = r.context.type
auditEntry.fields.action = r.context.action
auditEntry.fields.timestamp = System.currentTimeMillis()
auditEntry.save()
}
}
## Safe Regex Route Handling
class SafeRouteHandler {
@Alert('~on /api/items/([^/]*)/details hit')
static _getItemDetails(HttpResult r) {
// Always validate matches exist
if (r.matches.isEmpty()) {
r.setStatus(400)
r.writeToClient(['error': 'Invalid route'])
return
}
def itemId = r.matches[0]
// Validate the captured value
if (!itemId || itemId.length() > 100) {
r.setStatus(400)
r.writeToClient(['error': 'Invalid item ID'])
return
}
// Proceed with valid ID
def item = ItemService.findById(itemId)
if (!item) {
r.setStatus(404)
r.writeToClient(['error': 'Item not found'])
return
}
r.writeToClient(item.toMap())
}
}
# Troubleshooting
## Alert Not Firing
Symptoms: Your annotated method isn't being called.
Checklist:
- Is the method
static? - Does it have exactly one parameter?
- Is the class in a directory scanned by the Source Loader?
- Is the event string exactly correct? (spaces matter, case doesn't)
- For HTTP routes, is another alert setting
r.cancelled = truefirst? - Check the console for compilation errors
- Is your class being garbage collected? (Keep a reference)
## Wrong Result Type
Symptoms: ClassCastException or missing methods on result object.
Solution: Match your parameter type to the event type:
- HTTP events →
HttpResult - WebSocket events →
SocketResult, andResultfor open/close events - Document events →
Result - Custom events →
Result(or your own subclass)
## Regex Not Matching
Symptoms: Regex alert handler never fires.
Checklist:
- Did you prefix the event string with
~? - Remember spaces become
\sautomatically—don't double-escape - Use
[^/]to capture single path segments,.for multiple - Test your regex against the full event string (e.g.,
on /users/123 hit) - Check if
r.matchesis populated correctly (and not empty)
## Priority Confusion
Symptoms: Alerts firing in unexpected order.
Remember:
- Higher numbers = higher priority (run first)
- Default priority is
0 - Negative priorities run last
- Same priority = undefined order (don't rely on it)
## Matches Array Empty
Symptoms: r.matches[0] throws IndexOutOfBoundsException.
Solution: Always check before accessing:
@Alert('~on /users/([^/]*) hit')
static _handleUser(HttpResult r) {
if (r.matches.isEmpty()) {
r.setStatus(400)
return
}
def userId = r.matches[0]
// ... proceed safely
}
# Summary
The Alerts system is the backbone of event-driven programming in Spaceport. By understanding how to:
- Use
@Alertannotations to hook into events - Work with different Result types and their context data
- Leverage priorities and cancellation for control flow
- Create custom events for your own domain logic
- Apply regex patterns for dynamic routing
- Handle errors gracefully
...you can build sophisticated, maintainable applications that respond elegantly to every significant event in your system. While Spaceport has some built-in Alerts for common scenarios, the true power lies in your ability to define and react to the events that matter most to your application's unique needs.
# See Also
- Source Modules: Where to place your alert-annotated classes
- HTTP Result Reference: Complete API for HTTP responses
- Documents: Understanding document lifecycle events
- Routing Patterns: Deep dive into HTTP routing with alerts
- Launchpad: Rendering responses in your alert handlers
SPACEPORT DOCS