SPACEPORT DOCS

Cargo: The Universal Data Container

Cargo is a powerful and flexible data structure designed to manage application state, from simple counters to complex, nested data hierarchies. At its core, Cargo is a super-charged wrapper around a standard Groovy Map, providing a rich API for data manipulation, state management, and seamless integration with other Spaceport systems like Documents and Launchpad.

Think of a Cargo object as a universal container. It can act as a simple value holder, a nested map, a counter, a toggle, or a set. This versatility makes it an indispensable tool for handling form data, user sessions, application settings, and reactive UI components.

This document will start by exploring Cargo in its simplest form—as a local object—before moving on to its more advanced features like persistence and reactivity.

# Core Concepts: Using Cargo as a Local Object

The easiest way to understand Cargo is to use it as a local variable within your source modules. It provides a clean, fluent API for creating, reading, updating, and deleting data without needing any external connections.

## Construction

You can create a Cargo object in several ways, depending on your initial data needs.

// Creates an empty Cargo object, ready to hold data.
  def userData = new Cargo()
// Creates a Cargo object holding a single integer value.
  def pageCounter = new Cargo(0)

  // Creates a Cargo object holding a single string value.
  def statusMessage = new Cargo('Initialized.')
// Creates a Cargo object pre-populated with key-value pairs.
  def settings = new Cargo([
      darkMode: false,
      notifications: true,
      volume: 75
  ])

## Basic Getters and Setters

Cargo provides intuitive methods for reading and writing data. It can operate as a single-value container or as a map-like structure with nested nodes.

### Single-Value Operations

When a Cargo object is treated as a single-value holder, the get() and set() methods work on an implicit internal property.

def counter = new Cargo(10)

// Get the current value
def currentValue = counter.get() // Returns 10

// Set a new value
counter.set(11)
println counter.get() // Prints 11

### Map-Like Operations and Deep Nodes

The real power of Cargo shines when managing structured data. You can set and retrieve values at any depth using a .-separated path. If the path doesn't exist, Cargo creates it for you on the fly.

def user = new Cargo()

// Set simple key-value pairs
user.set('username', 'jeremy')
user.set('level', 99)

// Set a deeply nested value. The 'profile' and 'prefs' nodes are created automatically.
user.set('profile.prefs.theme', 'dark')

// Get the values back
def username = user.get('username') // Returns 'jeremy'
def theme = user.get('profile.prefs.theme') // Returns 'dark'

### Property Access with Dot Notation

For a more idiomatic Groovy experience, you can also use standard dot notation to access and assign properties. This makes your code cleaner and more readable.

def user = new Cargo()

// Set values using dot notation
user.username = 'jeremy'
user.profile.prefs.theme = 'dark'

// Get values using dot notation
println user.username                 // Prints 'jeremy'
println user.profile.prefs.theme      // Prints 'dark'

// Each node in the path is also a Cargo object
println user.profile.class.name       // Prints spaceport.computer.memory.virtual.Cargo

# Cargo in Action: Counters, Toggles, and Sets

Beyond basic key-value storage, Cargo includes specialized methods for common state management patterns.

## As a Counter

You can easily increment or decrement numeric values, making Cargo perfect for counters, scores, or inventory management.

def stats = new Cargo([ pageViews: 100, likes: 5 ])

// Increment the pageViews by 1
stats.inc('pageViews') // pageViews is now 101

// Increment likes by a specific amount
stats.inc('likes', 10) // likes is now 15

// Decrement pageViews
stats.dec('pageViews') // pageViews is now 100

For single-value Cargo objects, you can omit the path:

def counter = new Cargo(0)
counter.inc() // Value is now 1

## As a Toggle

You can flip the boolean state of a property with the toggle() method.

def settings = new Cargo([ darkMode: false ])

settings.toggle('darkMode')
println settings.get('darkMode') // Prints true

settings.toggle('darkMode')
println settings.get('darkMode') // Prints false

## As a Set

Cargo can manage a unique list of items, which is useful for things like tags, favorites, or user roles.

def article = new Cargo()

// Add items to the 'tags' list. Duplicates are ignored.
article.addToSet('tags', 'groovy')
article.addToSet('tags', 'spaceport')
article.addToSet('tags', 'groovy') // This one is ignored

println article.get('tags') // Prints ['groovy', 'spaceport']

// Check if an item exists in the set
boolean hasTag = article.contains('tags', 'spaceport') // Returns true

// Remove an item from the set
article.takeFromSet('tags', 'groovy')
println article.get('tags') // Prints ['spaceport']

# Advanced Data Manipulation

Cargo provides several other utility methods for managing the data within it, from removing data to checking for its existence.

## Checking for Existence

Before trying to access a value, you can check if it exists and is not empty using the exists() method. This is useful for conditional logic.

def user = new Cargo([
    name: 'Jeremy',
    profile: [
        avatar: 'avatar.png'
    ],
    roles: [] // An empty list
])

user.exists('name')             // Returns true
user.exists('profile.avatar')   // Returns true
user.exists('profile.bio')      // Returns false (key does not exist)
user.exists('roles')            // Returns false (key exists but the list is empty)

## Adding and Removing Data

You can precisely manage the contents of your Cargo object.

def tasks = new Cargo()

// Use setNext to add items with numeric keys
tasks.setNext('Write documentation') // Sets key '1'
tasks.setNext('Review code')         // Sets key '2'

println tasks.get('1') // Prints 'Write documentation'

// Delete a specific task
tasks.delete('1')
println tasks.exists('1') // Prints false

// Clear all tasks
tasks.clear()
println tasks.isEmpty() // Prints true

## Working with Lists

For paths that contain lists, you can use append() and remove() to manage their contents without retrieving and re-setting the entire list.

def user = new Cargo()
user.set('permissions', ['read'])

// Append a single item to the list
user.append('permissions', 'write')

// Append multiple items
user.append('permissions', ['comment', 'delete'])

println user.get('permissions') // Prints ['read', 'write', 'comment', 'delete']

// Remove an item from the list
user.remove('permissions', 'delete')
println user.get('permissions') // Prints ['read', 'write', 'comment']

# Type-Safe Getters in Practice

When you receive data from a form submission or a database, it often arrives as strings or generic objects. Cargo's type-safe getters ensure you can work with this data reliably without manual parsing and error handling.

Imagine you have a Cargo object populated with data from a client-side request.

## Example: Displaying Product Details in Launchpad

This example shows how to use a Cargo object to hold product information and safely render it in a Launchpad template (.ghtml).

Groovy Logic (in a Source Module or Launchpad Element)

// This Cargo object might be populated from a database or API call.
// Notice the values are strings, just like they might be from an HTTP request.
def productData = new Cargo([
    name: 'Spaceport Rocket',
    stock: "15",
    price: "1299.95",
    onSale: "true",
    rating: "4.7",
    features: "Fuel Efficient, Reusable, Self-Landing"
])

Launchpad Template (product-details.ghtml)

<%
    // Assume 'productData' is passed into the template, or otherwise made accessible here.
%>

<div class="product">
    <h1>${ productData.getString('name') }</h1>

    /// Use getInteger to safely get the stock count for logic
    <% if (productData.getInteger('stock') > 0) { %>
        /// Use getNumber to handle floating-point values like currency
        <p class="price">Price: ${ productData.getNumber('price').money() }</p>
    <% } else { %>
        <p class="out-of-stock">Out of Stock</p>
    <% } %>

    /// Use getBool to reliably check a flag
    <% if (productData.getBool('onSale')) { %>
        <span class="sale-banner">ON SALE!</span>
    <% } %>

    /// Use getRoundedInteger for display purposes
    <p>Rating: ${ productData.getRoundedInteger('rating') } out of 5 stars</p>

    <div class="features">
        <h3>Features</h3>
        <ul>
            /// getList can parse a comma-separated string into a List
            <% for (feature in productData.getList('features')) { %>
                <li>${ feature }</li>
            <% } %>
        </ul>
    </div>
</div>

This template demonstrates how the type-safe getters (getString, getInteger, getNumber, getBool, getList, getRoundedInteger) allow you to work with the data in its correct type, preventing common errors and making the template logic clean and robust.

# Handling Default Values

When dealing with potentially incomplete data, providing default values is essential for preventing errors and ensuring predictable behavior. Cargo offers two distinct methods for this.

## getDefaulted(): Read with Write-Back

The getDefaulted(path, defaultValue) method is perfect for initializing data. It behaves as follows:

1. It checks for a value at the given path. 2. If the value exists, it returns it. 3. If the value does not exist, it writes the defaultValue to that path and then returns it.

This "set-if-not-exists" behavior is extremely useful for setting up default configurations or user profiles the first time they are accessed.

def userSettings = new Cargo()

// The 'theme' key doesn't exist, so getDefaulted sets it to 'light' and returns 'light'.
def theme = userSettings.getDefaulted('theme', 'light')

// Now the key exists.
println userSettings.get('theme') // Prints 'light'

// On the next call, it simply returns the existing value.
def theme2 = userSettings.getDefaulted('theme', 'dark') // Will NOT overwrite
println theme2 // Prints 'light'

## getOrDefault(): Read-Only Fallback

The getOrDefault(path, valueOtherwise) method provides a safe, read-only fallback.

Use this when you need a temporary default for a calculation or display without altering the original data state.

def requestData = new Cargo([ item: 'rocket' ])

// The 'quantity' key doesn't exist, so getOrDefault returns the fallback value '1'.
def quantity = requestData.getOrDefault('quantity', 1)

println quantity // Prints 1

// The original Cargo object remains unchanged.
println requestData.exists('quantity') // Prints false

# Iterating and Querying Cargo

Cargo provides a rich set of methods for inspecting, querying, and transforming its data, making it easy to work with collections of information. These methods allow you to treat a Cargo object much like a standard Groovy collection.

## Core Inspection Methods

These methods allow you to inspect the basic structure and contents of a Cargo object.

def users = new Cargo([
    user1: [ name: 'Jeremy', level: 99 ],
    user2: [ name: 'Ollie', level: 50 ]
])

println users.size() // Prints 2
println users.keys() // Prints ['user1', 'user2']

// values() returns a list of Cargo objects
println users.values().first().getClass().name // prints spaceport.computer.memory.virtual.Cargo

// entrySet() gives you both key and value
users.entrySet().each { entry ->
    println "Key: ${entry.key}, Name: ${entry.value.name}"
}

// map() converts it to a standard Map
def userMap = users.map()
println userMap.getClass().name // Prints java.util.LinkedHashMap

## Finding Specific Data

You can search for entries within a Cargo object using closures.

def products = new Cargo([
    prodA: [ name: 'Rocket', category: 'transport', stock: 10 ],
    prodB: [ name: 'Laser', category: 'weapon', stock: 0 ],
    prodC: [ name: 'Shield', category: 'defense', stock: 5 ],
    prodD: [ name: 'Blaster', category: 'weapon', stock: 25 ]
])

// Find the key for the first item with 0 stock
def outOfStockKey = products.findKey { it.stock == 0 } // Returns 'prodB'

// Find all keys for items in the 'weapon' category
def weaponKeys = products.findAllKeys { it.category == 'weapon' } // Returns ['prodB', 'prodD']

// Count how many items have stock > 0
def inStockCount = products.count { it.stock > 0 } // Returns 3

## Sorting and Pagination

Cargo includes built-in helpers for sorting and paginating its contents, which is ideal for displaying large data sets.

def players = new Cargo([
    p1: [ name: 'Zoe', score: 1500 ],
    p2: [ name: 'Alex', score: 2500 ],
    p3: [ name: 'Chloe', score: 1200 ]
])

// Sort players by score, descending
def sortedPlayers = players.sort { a, b -> b.score <=> a.score }
sortedPlayers.each { println it.name } // Prints Alex, Zoe, Chloe

// Paginate the results, showing 2 per page
def firstPage = players.paginate(1, 2)
println firstPage.size() // Prints 2

# Leveraging Groovy's Power

Because Cargo is built on standard Groovy principles, it seamlessly integrates with Groovy's powerful collection methods. Many of these methods are enhanced by Spaceport's Class Enhancements to work intelligently with Cargo.

Here’s a brief overview of how you can use these familiar methods.

// Closure gets the value by default
  products.each { product -> println product.name }
  // Or, get both key and value
  products.each { key, product -> println "${key}: ${product.name}" }
// Get a list of all product names
  def names = products.collect { it.name } // Returns ['Rocket', 'Laser', 'Shield', 'Blaster']
// Find the first product in the 'weapon' category
  def firstWeapon = products.find { it.category == 'weapon' }
  println firstWeapon.name // Prints 'Laser'
// Is any product out of stock?
  boolean hasOutOfStock = products.any { it.stock == 0 } // Returns true
// Create a simple HTML list of product names
  def productHtml = "<ul>" + products.combine { "<li>${it.name}</li>" } + "</ul>"

# Global State with the Spaceport Store

While Cargo is excellent as a local object, its capabilities expand significantly when used to manage shared, application-wide state. This is achieved by connecting it to the Spaceport Store.

The Spaceport Store (Spaceport.store) is a global, in-memory, thread-safe ConcurrentHashMap that is available everywhere in your application. It's the perfect place to store data that needs to be shared across different requests, user sessions, or source modules, but doesn't need to be permanently saved to the database. Think of it as a central hub for your application's live operational data.

## Cargo.fromStore()

To simplify working with the store, Cargo provides a static helper method: Cargo.fromStore(name). This method acts as a singleton provider for named Cargo objects.

This ensures that any part of your application asking for Cargo.fromStore('hitCounter') will always receive the same, single object to work with.

## Use Case: Application-Wide Hit Counter

Imagine you want to track the total number of page hits across your entire application. Using Cargo.fromStore() makes this trivial.

Source Module: TrafficManager.groovy

import spaceport.computer.memory.virtual.Cargo

class TrafficManager {
    
    // Get the global hit counter Cargo from the store.
    // This will create it on the first run.
    static Cargo globalStats = Cargo.fromStore('globalStats')

    @Alert('on page hit')
    static _countHit(HttpResult r) {
        // Any time a page is hit, increment the counter.
        // This is the same instance, no matter where it's called from.
        globalStats.inc('totalHits')
    }
}

Source Module: Api.groovy

import spaceport.computer.memory.virtual.Cargo

class Api {

    @Alert('on /api/stats hit')
    static _getStats(HttpResult r) {
        // Retrieve the same global stats object from the store.
        def stats = Cargo.fromStore('globalStats')
        
        // Return the current count as JSON
        r.writeToClient([ 'totalApplicationHits': stats.getInteger('totalHits') ])
    }
}

Why do this?

# Persistent State with Documents

For data that needs to be permanent and survive server restarts, you can mirror a Cargo object to a Spaceport Document. This creates a live link between your in-memory Cargo object and a document in your CouchDB database.

When a Cargo is mirrored, any changes you make to it are automatically and asynchronously saved to the corresponding Document. This combines the convenience of the Cargo API with the power of persistent database storage.

## Cargo.fromDocument()

The static helper method Cargo.fromDocument(document) establishes this link. You pass it a Document instance, and it returns a Cargo object that is directly tied to the cargo property of that document.

## Use Case: Managing Article Metadata

This is a powerful use case for mirrored Cargo objects. While the core content of an article (like its title and body) might be stored in formal Document properties, Cargo is perfect for managing the flexible metadata associated with it, such as view counts, tags, and likes.

Source Module: ArticleHandler.groovy

// Assume ArticleDocument extends spaceport.computer.memory.physical.Document
import documents.ArticleDocument
import spaceport.computer.memory.virtual.Cargo

class ArticleHandler {

    // This alert fires when a user views an article page
    @Alert('~on /articles/(.*) hit')
    static _viewArticle(HttpResult r) {
        // 1. Fetch the article's document from the database
        def articleDoc = ArticleDocument.fetchById(r.matches[0])
        if (!articleDoc) return r.notFound()

        // 2. Get a Cargo object mirrored to that document's metadata
        def articleMeta = Cargo.fromDocument(articleDoc)

        // 3. Update the metadata using the Cargo API
        articleMeta.inc('viewCount')
        articleMeta.addToSet('tags', 'recently-viewed')

        // There's no need to call articleDoc.save()!
        // The changes to 'articleMeta' are automatically persisted to the database.

        // Now, render the article page...
        r.writeToClient("Viewing '${articleDoc.title}'. Views: ${articleMeta.get('viewCount')}")
    }

    // This alert fires when a user "likes" an article
    @Alert('~on /articles/(.*)/like POST')
    static _likeArticle(HttpResult r) {
        def articleDoc = ArticleDocument.fetchById(r.matches[0])
        if (!articleDoc) return r.notFound()
        
        def articleMeta = Cargo.fromDocument(articleDoc)

        // Increment the likes and record who liked it
        articleMeta.inc('likes')
        articleMeta.addToSet('likedBy', r.context.data.getString('userId'))
        
        r.writeToClient([ status: 'ok', likes: articleMeta.getInteger('likes') ])
    }
}

Why do this?

* Automatic Persistence: It dramatically simplifies database interactions. You work with a normal Cargo object, and Spaceport handles saving it to CouchDB in the background. * Data Synchronization: It keeps your in-memory state object in sync with the database, ensuring data integrity. * Flexible Document Schemas: You can easily manage complex, nested data inside your Documents using the clean Cargo API without having to manually manipulate maps and lists.

# Reactive State with Launchpad

The single most important feature of Cargo is its role as a reactive data source in Launchpad templates. When you modify a Cargo object on the server during a server action, Launchpad can automatically detect the change and push updates to the client's browser without a full page reload.

This is accomplished using Launchpad's server-reactive syntax: ${{ ... }}.

When you wrap a Cargo call in these special brackets, Launchpad creates a subscription. If that Cargo object is later modified by a server action (like an on-click event), Launchpad will re-evaluate the expression and send the new result to the client, updating the UI in real-time.

## Use Case: A Real-Time Counter

Here’s how you can build a simple counter component where the state is managed entirely on the server by a Cargo object, and the UI updates reactively.

Launchpad Template (counter.ghtml)

<%
    // Get a Cargo object from the store to hold our counter's state.
    // Using the store makes the state persist across different requests.
    def counter = Cargo.fromStore('myCounter')
%>

<div class="counter-widget">
    <span>
        Current Count:
        /// This creates a reactive subscription to the counter Cargo.
        /// When counter changes, only this part of the DOM will be updated.
        ${{ counter.counter.getDefaulted('value', 0) }}
    </span>

    /// This button triggers a server action that modifies the Cargo object
    <button on-click=${_ { counter.inc('value') }}>+</button>
</div>

What's Happening?

This creates a rich, interactive experience with server-side state management and minimal boilerplate.

# Other Utilities and Advanced Usage

Here are a few other methods and behaviors that round out Cargo's feature set.

## Groovy Truthiness

Cargo objects can be evaluated as booleans, which is known as "Groovy Truth". An empty Cargo is false, while a Cargo with any data in it is true.

def userPrefs = new Cargo()
if (userPrefs) {
    // This code will not run
}

userPrefs.set('theme', 'dark')
if (userPrefs) {
    // This code will run
}

## Type Coercion with asType

You can seamlessly convert a Cargo object into other common data structures using the as keyword.

def data = new Cargo([a: 1, b: 2])

// Convert to a Map
Map myMap = data as Map

// Convert to a List (contains the values)
List myValues = data as List // Returns [1, 2]

## Operator Overloading

For single-value Cargo objects used as counters, you can use Groovy's standard ++ and -- operators.

def counter = new Cargo(5)

counter++
println counter.get() // Prints 6

counter--
println counter.get() // Prints 5

## Other Convenience Methods

# Summary: The Role of Cargo

As we've seen, Cargo is far more than a simple data map. It's a versatile tool that adapts to your application's needs, seamlessly transitioning between four key roles: a temporary data holder, a global state manager, an automatic persistence layer, and a reactive UI driver. By understanding these roles, you can write cleaner, more powerful, and more maintainable Spaceport applications.

## Key Takeaways

## Next Steps

With a solid understanding of Cargo, you are now ready to see how it integrates with the other powerful systems in Spaceport. Explore the following documentation to continue building dynamic, data-driven applications: