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:
- Groovy-based: Fully leverage Groovy's dynamic capabilities and seamless Java interoperability
- Dynamically loaded: Compiled and loaded at runtime by Spaceport's Source Loader
- Hot-reloadable: In debug mode, changes are automatically detected and modules are reloaded
- Classpath-like: Similar organization and loading mechanism to Java's classpath system
- Modular: Encourages clean separation of concerns and reusable code
# 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:
- Discovery: Scans configured paths for
.groovyfiles - Compilation: Compiles Groovy source files into bytecode
- Loading: Loads compiled classes into the runtime
- Registration: Registers annotated methods (like
@Alert) with appropriate handlers - Initialization: Fires an
on initializealert - Hot-reload: (Debug mode) Monitors files for changes and recompiles as needed, repeating this process
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. Theclean()method is a built-in Class extension in Spaceport that sanitizes strings to prevent XSS attacks, and thecleanTypeproperty 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:
- File Change Detection: A source module has been modified, deleted, or created. Wait for a brief debounce period to
- ensure the file write is complete and multiple changes can be batched together.
- Deinitialization: Fires an
on deinitializealert, allowing modules to clean up resources or serialize data as needed using the Spaceport Store. - Initialization: Replays the loading process above for all source modules for a fresh start. Deserialize any necessary state from the Spaceport Store to maintain continuity.
## 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:
- Edit a source module file
- Save the file
- Spaceport automatically detects the change
- Module is recompiled and reloaded, there is no need to restart the server in most cases
- Test the changes immediately in your application
## 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:
- Check The Console: Review Spaceport's console for any compilation or runtime errors related to your source modules.
- Validate Annotations: Ensure that your Alert methods are correctly annotated with
@Alert, and make sure you've included aResultparameter in the method signature. - File Paths: Verify that your source modules are located in the correct directories as specified in your manifest.
- Syntax Errors: Look for any syntax errors in your Groovy code that might prevent compilation.
- Imports: Ensure all necessary classes are imported at the top of your source module files.
- Restart Spaceport: If all else fails, try restarting the Spaceport server to clear any potential state issues, taking note of the Debug-Mode Development Considerations above.
# Next Steps
Now that you understand how source modules work, explore these related topics to build more sophisticated Spaceport applications:
- Alerts: Master the event system that powers source module routing and lifecycle hooks
- Routing: Dive deeper into HTTP request handling, URL patterns, and advanced routing techniques
- Cargo: Learn about reactive data structures and persistent storage for managing application state
- Documents: Work with CouchDB through Spaceport's ORM-like interface for data persistence
- Launchpad: Build dynamic user interfaces with server-side rendering and integrate with your source modules
- Class Enhancements: Discover Groovy extensions that make web development easier
- Scaffolds: Understand project organization patterns and best practices for structuring larger applications
- Ignition Scripts: Load code and initialize resources before source modules are loaded
SPACEPORT DOCS