SPACEPORT DOCS

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:

## 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 hit will match on /api/users hit.

Some Built-in Event Patterns:

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:

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:

@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: The deinitialize and deinitialized events 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:

## 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. The writeToRemote() methods use sendStringByFuture() 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:

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?

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:

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:

## 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 ID
database (String): Database name
doc (Document): The document
classType (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 type
action (String): Action performed
document (Document): The document
operation (Object): Operation details
Document modified in database
on document remove Result type (String): Document type
document (Document): Document to delete
Before document deleted
on document removed Result type (String): Document type
document (Document): Deleted document
operation (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 identifier
client (Client): Authenticated client
Client authenticated successfully
on client auth failed Result user_id (String): Attempted user ID
exists (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

## Invocation Performance

## Best Practices

# 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:

## Wrong Result Type

Symptoms: ClassCastException or missing methods on result object.

Solution: Match your parameter type to the event type:

## Regex Not Matching

Symptoms: Regex alert handler never fires.

Checklist:

## Priority Confusion

Symptoms: Alerts firing in unexpected order.

Remember:

## 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:

...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