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 keeps your code organized and makes it easy to see exactly what happens when events fire.

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

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

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

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result

class AppLifecycle {
    
    static boolean running
    
    @Alert('on initialize')
    static _startup(Result r) {
        println "Application starting up..."
        
        // Spaceport's API to ensure required databases exist
        if (!Spaceport.main_memory_core.containsDatabase('users')) {
            Spaceport.main_memory_core.createDatabase('users')
        }
        
        // Modify global state
        running = true
        
        // Initialize your own application systems
        // (could be connection pools, caches, scheduled tasks, etc.)
        SomeExternalSystem.initialize()
    }
    
    @Alert('on deinitialized')
    static _shutdown(Result r) {
        println "Application shutting down for hot reload..."
        
        // 'on deinitialize' is called before hot reload in debug mode,
        // 'on deinitialized' is called after cleanup.
        running = false
        
        // You could imagine the need to 
        SomeExternalSystem.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).

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
        // (could be from Documents, a View, or any other data source)
        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
        // Since this is a POST, it's likely form data or JSON payload
        // If it was a GET, it could be query parameters
        def name = r.context.data.name
        def price = r.context.data.getNumber('price') // Complete with data helpers
        
        // Your application logic to create the product
        // (could save to a Document, call an external API, etc.)
        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
        
        // (Your application logic to delete the product)
        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.

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.
        // This could also use Spaceport's built-in logging (Command.log), or your own system
        println "${ start.time() }: Processed ${ method } request to ${ r.context.target } for ${ r.client.userID ?: 'anonymous' }"
    }
}

## 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
        
        // Your logging implementation (could write to a log, send to a service, etc.)
        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 (could be a Map in the Spaceport Store, 
        // or something external like Redis, Memcached, Cloudflare, etc.)
        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.

## 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.
    @Alert('~on /articles/(.*) hit')
    static _viewArticle(HttpResult r) {
        // Spaceport captures the route parameter via regex
        def articleId = r.matches[0]
        
        // Your application logic to fetch the article
        // (this could be a custom Document class method, a database query, etc.)
        def article = Article.fetchById(articleId)
        if (!article) {
            // Use Spaceport's HttpResult to send a 404
            r.setStatus(404)
            r.writeToClient("Article not found")
            return
        }
        
        // Render the article using Launchpad now that the 
        // middleware and data fetching is done
        new Launchpad().assemble(['article.ghtml']).launch(r)
    }
    
    // Match: /users/alice/posts/123
    @Alert('~on /users/(.)/posts/(.) hit')
    static _viewUserPost(HttpResult r) {
        // Spaceport captures multiple groups
        def username = r.matches[0]
        def postId = r.matches[1]
        
        // Your application logic here
        // ...
    }
}

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) {
        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 a few 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)

Common 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
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
setRedirectUrl(String) Redirect to a URL (302)
writeToClient(String) Write string response
writeToClient(Map) Write JSON response (auto-sets content type)
writeToClient(File) Write file response (auto-detects content type)
markAsAttachment(String) Set Content-Disposition for download

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.

@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)
    
    // Use Spaceport's SocketResult to send response through the WebSocket
    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
writeToRemote(Map) Send a JSON message
close() Close the socket connection
close(String reason) Close with a reason message

## Base Result (Custom Events)

For custom events or simpler built-in events, you'll receive the base Result class. This has fewer helper methods but provides the core functionality.

@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: 1. Your core business logic performs an operation and invokes a custom event 2. Spaceport broadcasts this event to all registered alert handlers 3. Multiple independent handlers can react to the same event 4. Each handler implements its own specific concern (email, inventory, analytics, etc.)

This creates a flexible plugin-like architecture where you can add new behaviors without modifying existing code.

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
        // Your email service integration
        YourEmailService.send(order.customerEmail, "Order #${order.id} confirmed")
    }
}

class InventoryManager {
    @Alert('on order processed')
    static _updateInventory(Result r) {
        def order = r.context.order
        // Your inventory management logic
        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
        // Your analytics service integration
        YourAnalytics.trackEvent('sale', order.total)
    }
}

## Using resultType for Custom Result Classes

For complex custom events, you can create your own Result subclass with domain-specific helper methods. This pattern lets you encapsulate common operations and make your alert handlers more readable.

The Flow:

import spaceport.computer.alerts.results.Result

// Custom Result class with domain-specific helper methods
class OrderResult extends Result {
    OrderResult(Object context) {
        super(context)
    }
    
    // Helper to get the order with proper type
    Order getOrder() {
        return context.order as Order
    }
    
    // Domain-specific operation that multiple handlers might need
    void markAsFailed(String reason) {
        getOrder().status = 'failed'
        getOrder().failureReason = reason
        getOrder().save()
        this.cancelled = true // Stop further alert processing
    }
}

// Invoking with a custom result type
def context = [
    order: myOrder,
    customer: customer,
    resultType: OrderResult  // Tell Spaceport to use your custom class
]

def result = Alerts.invoke('on order submitted', context)

Then in your alert handlers, you get the custom type with its helper methods:

@Alert('on order submitted')
static _validateOrder(OrderResult r) {  // Type is OrderResult, not Result
    // 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
    }
}

## 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. This pattern demonstrates how multiple alerts can collaborate on the same request, each handling a specific stage of processing.

The Flow: 1. High-priority alert validates the input (priority 30) 2. If validation passes, mid-priority alert processes the data (priority 20) 3. Low-priority alert saves the result (priority 10) 4. Each stage can access results from previous stages via r.context 5. Any stage can set r.cancelled = true to abort the pipeline

This approach keeps each stage focused and testable, while maintaining a clear sequence of operations.

class ImageUploadPipeline {
    
    @Alert(value = 'on /api/upload POST', priority = 30)
    static _validateUpload(HttpResult r) {
        def file = r.context.data.file
        
        // Your validation logic
        if (!YourImageValidator.isValidType(file)) {
            r.setStatus(400)
            r.writeToClient(['error': 'Invalid file type'])
            r.cancelled = true // Stop the pipeline
            return
        }
        
        // Store validation result for next stage to use
        r.context.validated = true
    }
    
    @Alert(value = 'on /api/upload POST', priority = 20)
    static _processImage(HttpResult r) {
        // Only proceed if validation passed
        if (!r.context.validated) return
        
        def file = r.context.data.file
        
        // Your image processing logic (resize, optimize, watermark, etc.)
        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) {
        // Only proceed if processing completed
        if (!r.context.processedImage) return
        
        // Your storage logic (S3, local filesystem, CDN, etc.)
        def url = YourStorageSystem.save(r.context.processedImage)
        
        // Send response to client using Spaceport's HttpResult
        r.writeToClient(['url': url])
    }
}

# Built-in Events Reference

## HTTP Events

Event String Result Type Context Description
on page hit HttpResult HTTP request context Fires for every HTTP request
on / hit HttpResult HTTP request context GET request to root path
on [route] hit HttpResult HTTP request context GET request to specific route
on [route] [METHOD] HttpResult HTTP request context Specific HTTP method to route

Examples: - on /api/users hit - GET /api/users - on /api/users POST - POST /api/users - ~on /articles/(.) hit - GET /articles/ with capture - ~on /users/(.)/posts/(.) DELETE - DELETE with multiple captures

## WebSocket Events

Event String Result Type Context Description
on socket connect SocketResult Socket lifecycle context WebSocket connection opened
on socket data SocketResult Socket message context Data received from client
on socket [handler-id] SocketResult Socket message context Routed to specific handler
on socket closed SocketResult Socket lifecycle context WebSocket connection closed

## Document Events

Event String Result Type Context Properties Description
on document created Result id, database, doc, classType New document created
on document save Result doc Before document save (can modify)
on document saved Result doc After document saved successfully
on document modified Result type, action, document, operation Document modified in database
on document remove Result type, document Before document deleted
on document removed Result type, document, operation After document deleted
on document conflict Result Document conflict details Save conflict detected

## Application Lifecycle Events

Event String Result Type Context Description
on initialize Result Empty map Application startup complete
on initialized Result Empty map All initialization complete
on deinitialize Result Empty map Before hot reload (debug mode)
on deinitialized Result Empty map After cleanup before reload

## Authentication Events

Event String Result Type Context Properties Description
on client auth Result user_id, client Client authenticated successfully
on client auth failed Result user_id, exists Authentication attempt failed

# Performance Considerations

## Alert Registration Performance

## Invocation Performance

## Best Practices

1. Use specific event strings when possible instead of broad catches with on page hit 2. Minimize regex complexity for frequently-invoked events 3. Return early in alert handlers when conditions aren't met 4. Use priority to short-circuit unnecessary processing with r.cancelled = true 5. Avoid heavy computation in high-frequency alerts (like on page hit)

# Common Patterns and Recipes

## Basic Authentication Guard

class AuthGuard {
    
    @Alert(value = 'on page 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
        
        println "[${new Date()}] ${method} ${path} | ${user} | ${ip}"
    }
}

## Automatic Cache Invalidation

This pattern shows how to automatically clear caches when documents change, keeping your cached data fresh. The Alert system makes this seamless—whenever a document is saved, your cache logic runs without any coupling to the save logic.

class CacheInvalidator {
    
    @Alert('on document saved')
    static _invalidate(Result r) {
        // Spaceport provides the document that was saved
        def doc = r.context.doc
        
        // Your caching implementation (could be a Map, Redis, Memcached, etc.)
        // 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')
        }
    }
}

Note: Spaceport includes a Document caching system that is covered in the Document Documentation, but often other external caches are used in real-world applications. Imagine integrating with Redis, Memcached, or even in-memory caches specific to your application logic. This pattern demonstrates how Alerts can help maintain cache coherency effortlessly.

## Error Page Handling

class ErrorHandler {

    @Alert(value = 'on page hit', priority = -100) // Very low priority
    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()
    }
}

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

# 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