Custom Commands Design (rooibos)
This document describes the architectural design and guiding principles of custom commands in rooibos. It is intended for contributors, architects, and AI agents working on the codebase.
Core Abstractions
Custom commands extend Rooibos with user-defined side effects: WebSockets, gRPC, database polling, background workers. The architecture provides four key components:
| Component | Purpose |
|---|---|
Command::Custom |
Mixin for command identification |
Command::Outlet |
Messaging gateway for result delivery |
Command::CancellationToken |
Cooperative cancellation mechanism |
Command.custom { ... } |
Wrapper giving callables unique identity |
Guiding Design Principles
1. Messaging Gateway over Raw Queue
Commands produce messages. Those messages cross threads. Without abstraction, queue manipulation scatters across the codebase.
The Outlet wraps the internal queue with a domain-specific API. Commands call out.put(:tag, data) instead of managing queue details. Debug mode validates Ractor-shareability automaticallyâcommands cannot accidentally push unshareable data.
| Aspect | Raw Queue | Outlet |
|---|---|---|
| Ractor safety | Manual | Automatic |
| Error messages | Generic | Contextual |
| API | queue << Ractor.make_shareable([...]) |
out.put(:tag, data) |
| Pattern | Implementation detail | Messaging Gateway |
This implements the Messaging Gateway pattern from Enterprise Integration Patterns. The Outlet is a Facade (Gang of Four) over the queue, and acts as a Channel Adapter translating command results into update function messages.
2. Cooperative Cancellation over Thread Termination
Long-running commands block the event loop. WebSocket listeners, database pollers, and streaming connections run indefinitely until stopped. Stopping them abruptly corrupts state.
Thread#kill terminates immediately. Mutexes may deadlock. Resources may leak. Database transactions may abort mid-write.
The CancellationToken signals cancellation requests. Commands check token.canceled? periodically and stop at safe points. Cleanup code runs. Resources release. Transactions commit.
| Aspect | Thread#kill |
CancellationToken |
|---|---|---|
| Cleanup | None (immediate termination) | Command controls cleanup |
| Resource safety | May corrupt state | Clean shutdown |
| Mutexes | May deadlock | Released properly |
| Pattern | Forceful | Cooperative |
Configurable grace periods accommodate different cleanup needs:
-
0.5seconds â Quick HTTP abort, minimal cleanup -
2.0seconds â Default, suitable for most commands -
5.0seconds â WebSocket close handshake with remote server -
Float::INFINITYâ Wait indefinitely for cooperative exit (database transactions)
3. Command Identity via Object Reference
The runtime tracks active commands by their object identity. When the update function returns Command.cancel(handle), the runtime looks up that exact object in its registry and signals its token.
Class-based commands get unique identity automaticallyâeach MyCommand.new produces a distinct object. Reusable lambdas and procs share identity. Dispatch them twice, and cancellation would affect both.
Command.custom(callable) wraps a callable in a unique container. Each call produces a distinct handle. Store it in your model. Cancel it later by returning Command.cancel(handle).
4. Automatic Error Propagation
Commands run in threads. Exceptions bubble up silently. The update function never sees them. Backtraces written to STDERR corrupt the TUI display.
The runtime catches unhandled exceptions and pushes Command::Error to the queue. This is automaticâcommands do not rescue exceptions unless they want custom handling. The update function pattern-matches on Command::Error and reacts appropriately.
This mirrors the sentinel pattern used for Command::Exit and Command::Cancel.
Error categorization: We use a single Command::Error sentinel for all command exceptions. Distinguishing âframework bugsâ vs âuser bugsâ at the sentinel level adds complexity with marginal benefit. Instead, exception classes provide the signal:
| Exception Class | Source |
|---|---|
RatatuiRuby::Error::Invariant |
Framework validation (debug mode) |
RatatuiRuby::Error::Internal |
Framework bugs |
ArgumentError, RuntimeError, etc. |
User code |
The update function can pattern-match on error_msg.exception.class if it needs to distinguish sources.
5. Ractor Readiness
This design is forward-compatible with Rubyâs Ractor-based parallelism.
Whatâs already shareable:
| Pattern | Shareable? | Why |
|---|---|---|
| Module-constant lambda | â Yes |
self is the frozen module |
| Closure-free block | â Yes | No captured variables |
Data.define command |
â Yes | Immutable by definition |
| Frozen class instance | â Yes | Deeply frozen |
Debug mode validation:
In debug mode, Rooibos validates Ractor shareability at dispatch time. Ractor.shareable?(command) catches most issues. The Outletâs put method validates messages before pushing them to the queue.
Why Thread dispatch is Ractor-safe:
Commands execute in Threads within the main Ractor. Only messages (via Outlet) cross the shareability boundary. The CancellationToken stays within its Threadânever shared across Ractors. The @active_commands hash is local to the Runtime (main Ractor).
When Ruby evolves to true Ractor parallelism, this design upgrades transparently: commands are already validated as shareable, so they could be sent to worker Ractors without code changes.
Pattern Lineage
This design implements several established patterns from the software architecture literature.
Design Patterns (Gang of Four)
| Component | Pattern |
|---|---|
| Custom Mixin | Command â Encapsulates a request as an object |
| Outlet | Facade â Simplified interface to a complex subsystem |
| Runtime | Mediator â Coordinates command dispatch and message routing |
| Update Function | State â Behavior changes based on internal state |
Enterprise Integration Patterns (Hohpe & Woolf)
| Component | Pattern |
|---|---|
| Outlet | Messaging Gateway â Wraps message channel access |
| Outlet | Channel Adapter â Connects applications to messaging systems |
| Queue | Point-to-Point Channel â Single consumer receives each message |
Cancellation Patterns
| Platform | Mechanism | Relationship |
|---|---|---|
| .NET | CancellationToken |
Direct inspiration for API design |
| Go | context.Context |
Cooperative cancellation via ctx.Done()
|
| JavaScript | AbortController |
Web Fetch API cancellation |
| Java | Thread.interrupt() |
Flag-based cooperative cancellation |
| Kotlin | Job.cancel() |
Coroutine cancellation |
Prior Art
The Elm Architecture (TEA)
| Framework | Language | Notes |
|---|---|---|
| Elm | Elm | Original TEA; Cmd + Sub primitives |
| BubbleTea | Go | TUI implementation; âSubscriptions are Loopsâ philosophy |
| Iced | Rust | GUI TEA with Command + Subscription |
| Miso | Haskell | Web TEA with Effect + Sub |
| Bolero | F# | Web TEA with Cmd + Sub |
Redux Ecosystem
| Library | Pattern | Mapping |
|---|---|---|
| Redux Thunk | Raw dispatch access | Direct queue access (rejected) |
| Redux Saga |
put() effect dispatches actions |
Outlet#put â adopted |
| Redux Observable | RxJS Observables | RxRuby (rejected for complexity) |
| redux-loop | Elm-style Cmd | Recursive commands |
Redux Sagaâs put() is the direct inspiration for out.put(). The Saga patternâlong-running processes that listen for actions and dispatch new onesâmaps directly to Rooibosâs custom commands.
Ruby Ecosystem
| Library | Pattern | Relationship |
|---|---|---|
| Observable (stdlib) | Observer pattern | Similar push semantics, lacks thread-safety |
| Concurrent Ruby | Actor, Promises | More complex than needed |
| Celluloid | Actor mailboxes | Inspiration for internal terminology |
| Sidekiq | Background jobs | Similar âfire command, receive resultâ model |
Key Influences Summary
| Influence | What We Adopted |
|---|---|
| Elm | MVU architecture, Cmd/Msg pattern |
| BubbleTea | âSubscriptions are Loopsâ philosophy |
| Redux Saga |
put() for message dispatch |
| .NET CancellationToken | Cooperative cancellation with grace periods |
| EIP Messaging Gateway | Outlet as infrastructure abstraction |
| Go context.Context | Cancellation propagation pattern |
Further Reading
External Resources
-
Elm Guide â Official Elm documentation covering commands and effects
-
BubbleTea â Go TUI framework based on The Elm Architecture
-
Redux Saga â Saga pattern and
put()effect documentation -
Enterprise Integration Patterns â Messaging patterns reference by Hohpe & Woolf
Academic References
-
Gamma, Helm, Johnson, Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
-
Command, Facade, Mediator, State patterns
-
Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley.
-
Gateway pattern
-
Hohpe, G. & Woolf, B. (2003). Enterprise Integration Patterns. Addison-Wesley.
-
Messaging Gateway, Message Channel, Point-to-Point Channel, Channel Adapter
-
Joshi, U. (2023). Patterns of Distributed Systems. Addison-Wesley.
-
Write-Ahead Log, Thread Pool