SPACEPORT DOCS

Source Modules in Spaceport

Source modules are Groovy classes that form the core logic of your Spaceport application. They are dynamically loaded by Spaceport using a classpath-like system, allowing you to organize your application code into modular, reusable components.

If you're not already familiar with Alerts, you'll see a bunch of uses within the examples below. Interacting with Spaceport's event system is primarily done through Alerts—static methods annotated to handle specific events, such as HTTP requests or lifecycle events. See the Alerts documentation for a deep-dive into how to work with Alerts in your application.

# Source Module Loading

Using the Spaceport Manifest, Spaceport's Source Loader automatically discovers and compiles the modules inside the given source paths, handling on-the-spot compilation and class-loading of your Groovy code to make it available to your running application. This system provides similar functionality to Java's classpath mechanism but with the added benefits of dynamic reloading and Groovy's powerful features.

Key characteristics of source modules:

# Module Structure and Organization

Source modules are organized in the modules/ directory within your Spaceport project by default, with a hierarchical structure that reflects a typical application package-based architecture. Without additional configuration, Spaceport will scan the modules/ directory, but you can also specify different or additional paths in your config.spaceport manifest file. A basic structure for an application with a few modules and submodules might look like:

/[SPACEPORT_ROOT]
  modules/
    Router.groovy
    AssetHandler.groovy
    TrafficManager.groovy
    utils/
      Helper.groovy
      FileUtils.groovy
    documents/
      SubscriptionDocument.groovy
      ArticleDocument.groovy

## Naming Conventions

Since source modules are typically designed specifically for your application, the naming conventions can be flexible. Without a strict requirement to add a typical 'com.name' package prefix that might be a familiar practice with typical Java and Groovy development, your package structure can be slim and sleek. However, it's still good practice to use clear, descriptive names that reflect the module's purpose. For example: TrafficManager, Router, ArticleDocument, etc.

If you're developing a library of reusable modules that might be shared across multiple projects, consider using a more traditional package naming convention to avoid naming conflicts, such as com.yourcompany.yourlibrary, allowing a developer to easily drop your source module structure right into their project.

## Basic Module Example

Here's a simple source module that handles HTTP requests. Since it's in the root folder of modules/, it doesn't need a package declaration. Alerts like these provide a quick and effective entry point for your application.

import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult

class Router {
    
    @Alert('on / hit')
    static _root(HttpResult r) {
        // Return a simple HTML response
        r.writeToClient('<h1>Hello, world! From Spaceport.</h1>')
    }
    
    @Alert('on /api/status hit')
    static _status(HttpResult r) {
        // Return a JSON status response
        r.writeToClient([ 'status' : 'ok', 'timestamp' : new Date() ])
    }
    
}

# Module Loading Process

Spaceport follows a specific process when loading source modules:

If any errors occur during compilation or loading of your source modules, Spaceport logs the errors and continues running, allowing you to fix issues without crashing the entire application.

Source Modules are dynamically loaded and reloaded, but you may also find it useful or necessary to load Groovy classes at startup, before the source module loading process, that your source modules can leverage. See the Ignition Script Documentation for more information on how to set up ignition scripts that run before your source modules are loaded.

# Common Module Patterns

Since source modules are just plain Groovy, you can structure them in whichever way suits your application's needs. Spaceport allows for a fairly unopinionated approach to how you organize your code, applying all the principles of computer science and software engineering that you already know and love. There are a couple of common patterns that emerge in Spaceport applications, however.

## Static Modules

Static source modules are useful for handling routing and other global application logic. Think of this type as a globally available class where methods and state are static and managed at the application level. You'll miss out on some of the goodness of being an Object, but the good news is there's no need to instantiate the class with the new keyword, and it comes pre-loaded by default. In this example, the other classes and instances in your application can reach hitCounter by using TrafficManager.hitCounter, and you can still use @Alert annotations to hook into the Spaceport event system.

import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.alerts.results.HttpResult
import spaceport.computer.memory.virtual.Cargo

class TrafficManager {
    
    static Integer hitCounter = 0

    @Alert('on page hit')
    static _count(HttpResult r) {
        // Handle all requests to the server, but don't mutate the response
        hitCounter++ // Increment the hit counter
    }

    @Alert('on /api/stats hit')
    static _getStats(HttpResult r) {
        // Return the current hit count as JSON
        r.writeToClient([ 'hits' : hitCounter ])
    }
    
}

## Instance Modules

Instance source modules are useful for encapsulating related functionality and state within an object-oriented design. These modules can be instantiated as needed, allowing for multiple instances with their own state. This pattern is particularly useful for managing resources or handling specific tasks that require their own context. In this example, you can create multiple Message, each with its own content and author.

class Message {
    
    String content
    String author
    
    Message(String content, String author) {
        this.content = content
        this.author = author
    }
    
    String getSummary() {
        return "${author}: ${content.take(20)}${content.length() > 20 ? '...' : ''}"
    }
    
}

Using standard OOP principles, and some client-server communication patterns using Spaceport's Alert system, you could then create a MessageHandler module that manages a collection of Message instances, and exposes methods to create and list messages, like in the example below.

import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult

class MessageHandler {
    
    static List<Message> messages = []
    
    @Alert('on /api/messages GET')
    static _listMessages(HttpResult r) {
        // Return a list of message summaries
        r.writeToClient([ 'summaries' : messages.collect { it.getSummary() } ])
    }
    
    @Alert('on /api/messages/create POST')
    static _createMessage(HttpResult r) {
        // Create a new message from request parameters
        def content = r.context.data.'content' ?: 'No content'
        def author  = r.context.data.'author' ?: 'Anonymous'
        def message = new Message(content, author)
        messages << message
        r.writeToClient([ 'status' : 'created', 'author' : message.author, 'message' : message.content ])
    }
    
}

Instance modules can still leverage static methods and properties for shared functionality, but the primary focus is on instance-specific behavior and data. This type of module is a core part of building OOP-style applications.

## Document Modules

Spaceport has a built-in document system that allows you to define data models as Groovy classes. These document modules interact seamlessly with CouchDB, providing a structured way to manage your application's database layer, acting as an ORM-like system with class-specific behavior. See more on Documents for a deep-dive into working with persistent data in Spaceport.

package documents

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.physical.Document

class ArticleDocument extends Document {
    
    @Alert('on initialize')
    static void _init(Result r) {
        // Ensure the 'articles' database exists
        if (!Spaceport.main_memory_core.containsDatabase('articles'))
             Spaceport.main_memory_core.createDatabase('articles')
    }
    
    static ArticleDocument fetchById(String id) {
        // Leverage the built-in getIfExists method from the Document base class
        return getIfExists(id, 'articles') as ArticleDocument
    }
    
    // Let Spaceport know which additional properties to store in the document
    def customProperties = [ 'title', 'body', 'tags' ]
    
    // Default values for properties
    String title = 'Untitled'
    
    void setTitle(String title) {
        // Sanitize using .clean() to prevent XSS attacks
        this.title = title.clean()
    }
    
    String body = 'No content, yet.'
    
    void setBody(String body) {
        // Allow some basic HTML only, but still sanitize
        body.cleanType = 'basic'
        this.body = body.clean()
    }
    
    List<String> tags = []
    
    void addTag(String tag) {
        this.tags << tag.clean()
    }
    
    void removeTag(String tag) {
        this.tags.remove(tag)
    }
    
}
If you're already familiar with Groovy you'll be familiar with the extra methods on common class types, but you might notice a couple of additional features in the example above. The clean() method is a built-in Class extension in Spaceport that sanitizes strings to prevent XSS attacks, and the cleanType property allows you to specify different levels of HTML sanitization. Spaceport has a multitude of enhancements that you can find in the Class Enhancements Documentation.

## Utility Modules

Utility modules provide shared functionality that can be used across different parts of your application. These modules typically contain static methods and are organized in a way that makes them easy to find and use. They are kind of like static modules, but focused on providing helper functions rather than application-wide state or behavior.

class Helper {

    static String generateRandomString(int length) {
        def chars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
        // .combine() and .random() are additional Spaceport Class Enhancements
        return (1..length).combine { chars.random() }
    }
    
    static boolean isProbablyValidEmail(String email) {
        def emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
        return email ==~ emailPattern
    }
    
}

# Debugging and Hot Reloading

One of the most powerful features of source modules is hot reloading in debug mode. The following process plays out when hot reloading:

## Enabling Debug Mode

To enable hot reloading, set the debug flag to true in your config.spaceport file. If running with --no-manifest, debug mode is on by default, which means that if your application is ready for production, you should explicitly set up a manifest and explicitly set debug to false to disable hot reloading and improve performance and security.

# In config.spaceport
debug: true

## Development Workflow

With debug mode enabled, hot reloading allows your development workflow to become:

## Production Deployments

For production deployments, it's recommended to disable debug mode to enhance performance and security. This prevents the overhead of file monitoring and dynamic recompilation, ensuring a stable and efficient runtime environment. With debug mode off, source modules are loaded once at startup and remain static until the server is restarted, providing a more predictable and secure application state.

Disabling debug mode also reduces security risks, as it prevents runtime file modifications from affecting the app.

## Debug-Mode Development Considerations

While hot reloading is a powerful feature, there are some considerations to keep in mind during development with regards to state management and application behavior. For example, static variables in static modules will be reset on reload, so any state that needs to persist across reloads should be stored in the Spaceport Store.

Using the example from above, if you want to maintain the hitCounter value across reloads, you could modify the TrafficManager module to save and load the counter from the Spaceport Store using Spaceport's Cargo system which has a tie-in to the Spaceport Store, but you could also use standard Maps, Lists, or other Objects as needed, putting them directly into the Spaceport.store ConcurrentHashMap. See the Cargo Documentation for details on its persistent storage options.

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.HttpResult
import spaceport.computer.alerts.results.Result
import spaceport.computer.memory.virtual.Cargo

class TrafficManager {
    
    static Cargo hitCounter = Cargo.fromStore('hitCounter')

    @Alert('on page hit')
    static _count(HttpResult r) {
        // Handle all requests to the server, but don't mutate the response
        hitCounter.inc() // Increment the hit counter
    }

    @Alert('on /api/stats hit')
    static _getStats(HttpResult r) {
        // Return the current hit count as JSON
        r.writeToClient([ 'hits' : hitCounter.get() ])
    }
    
}

You'll notice that the hitCounter is now a Cargo object that automatically persists its value in the Spaceport Store, allowing it to survive module reloads and server restarts. It's also still in scope of the TrafficManager, even though it's also persisting in the Store. This pattern can be applied to any stateful data that needs to persist across development iterations. Other approaches could include serializing the state of the object, storing it into the store on a on deinitialize alert, and deserializing it back on an on initialize alert.

Consider a more complex example where you have a GamePlayer class that maintains player state. You could implement it like this using a Self-Registering Class pattern.

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import java.util.concurrent.CopyOnWriteArrayList

class GamePlayer {

    // Maintain a thread-safe list of all players using a static variable
    static List<GamePlayer> players = new CopyOnWriteArrayList<>()

    String name
    Integer score

    GamePlayer(String name, Integer score = 0) {
        this.name = name
        this.score = score
        // Register the player
        players << this
    }

    void increaseScore(int points) {
        score += points
    }

}

On a hot reload, players will be lost since the static list is reset, but you can persist them in the Store on deinitialization and restore them on initialization.

import spaceport.Spaceport
import spaceport.computer.alerts.Alert
import spaceport.computer.alerts.results.Result
import java.util.concurrent.CopyOnWriteArrayList

class GamePlayer {

    @Alert('on initialized')
    static void _init(Result r) {
        // Load players from the store if they exist
        Spaceport.store.get('gamePlayers')?.each {
                // Use whichever method of deserialization makes sense for your data
                players << new GamePlayer(it.name, it.score)
            }
        }
        // Clear the store
        Spaceport.store.remove('gamePlayers')
    }

    @Alert('on deinitialized')
    static void _deinit(Result r) {
        // Save players to the store using an appropriate serialization method
        Spaceport.store.put('gamePlayers', players.collect { [ name: it.name, score: it.score ] })
    }

    static List<GamePlayer> players = new CopyOnWriteArrayList<>()

    String name
    Integer score

    GamePlayer(String name, Integer score = 0) {
        this.name = name
        this.score = score
        // Register the player
        players << this
    }

    void increaseScore(int points) {
        score += points
    }

}

This pattern ensures that player state is preserved across hot reloads, allowing for a smoother development experience without losing important data.

Another consideration is that if you are holding on to instances of a class in variables that don't get cleared on a hot reload, those instances will be retained across reloads which can lead to unexpected behavior if the class definition has changed. In such cases, be mindful of how you manage instance state and consider re-instantiating objects if necessary to ensure they align with the updated class definition. Here's an example of this gotcha, imagining that we hadn't implemented the serialization pattern above, and we had a Game class that managed game logic and interacted with GamePlayer instances.

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

class Game {
    
    static boolean isRunning = true
    
    @Alert('on initialize')
    static void _init(Result r) {
        // Create a game loop
        Thread.start {
            while (isRunning) {
                // Increase a Player's score every second
                Spaceport.store.gamePlayers.each { GamePlayer player ->
                    player.increaseScore(100)
                }
                sleep(1000) // Wait for 1 second
            }
        }
    }
    
    @Alert('on deinitialize')
    static void _deinit(Result r) {
        isRunning = false // Stop the game loop, the Thread will end naturally
    }
    
}

In this example, the Game class starts a thread that continuously increases the score of all players. If you make changes to the GamePlayer class and hot reload, the existing instances in Spaceport.store.gamePlayers will still reference the old class definition, which will throw a ClassCastException when you try to call increaseScore on them. The new class definition of Game will expect instances of the updated GamePlayer, but the old instances are still lingering.

Since Groovy is a dynamic language, it's possible in the above scenario to refer to player as def player instead of GamePlayer player, which would allow the code to continue working even if the class definition has changed, but be aware that this could lead to some unexpected behavior if the class structure has changed significantly. The recommended approach is to re-instantiate objects after a hot reload to ensure they align with the updated class definition if your application is even remotely complex.

# Troubleshooting

If you encounter issues with source modules not loading or behaving as expected, consider the following troubleshooting steps:

# Next Steps

Now that you understand how source modules work, explore these related topics to build more sophisticated Spaceport applications: