Why Kit Doesnβt Need Outlet
Context: The
unified-draft.mdspecifiesTea::Command::Outletfor message passing from custom commands to the Tea runtime. This document explains why Kit (the component-based runtime) does not need this abstraction.
The Core Insight: Immediate-Mode Rendering
RatatuiRubyβs Engine is immediate-mode:
βEach frame, your code describes the entire UI from scratch. The Engine draws it and immediately forgets it, holding no application state between renders.β
This means Kit walks the entire component tree and calls render every frame. Components donβt need to notify anyone of state changesβthe next frame automatically sees updated state.
Tea vs Kit: Different Problems
| Concern | Tea | Kit |
|---|---|---|
| State model | Immutable (Ractor-safe) | Mutable (encapsulated) |
| State location | Single Model in runtime | Distributed in components |
| Async results | Message β Queue β Update | Callback β mutate state |
| Render trigger | After update function | Every frame |
| Re-render notification | Implicit (update always renders) | Not needed (always renders) |
WebSocket Example: Tea vs Kit
Tea: Outlet Required
class WebSocketCommand include Tea::Command::Custom def call(out, token) ws = WebSocket::Client.new(@url) ws.on_message { |msg| out.put(:ws, :message, data: msg) } ws.connect until token.cancelled? sleep 1 end ws.close end end def update(msg, model) case msg in [:ws, :message, data:] model.with(messages: model.messages + [data]) end end
Tea needs Outlet because: 1. Command runs in a separate thread 2. Model is immutableβcanβt mutate from callback 3. Runtime must receive messages to call update
Kit: Direct Mutation
class WebSocketTab include Kit::Component def initialize(url:) @url = url @messages = [] end def mount @ws = WebSocket::Client.new(@url) @ws.on_message { |msg| @messages << msg } @ws.connect_async end def unmount @ws&.close end def render(frame, area) frame.render_widget(tui.list(items: @messages.last(10)), area) end end
Kit doesnβt need Outlet because: 1. Component owns the WebSocket directly 2. State is mutableβcallbacks mutate @messages 3. Next frameβs render sees updated state automatically
Why Each Tea Concept Is Unnecessary in Kit
Outlet (Message Gateway)
Tea: Routes messages from command thread β runtime queue β update function.
Kit: Not needed. Callbacks mutate component state directly. Immediate-mode rendering sees changes next frame.
CancellationToken
Tea: Runtime signals command to stop cooperatively, since commands run in spawned threads tracked by the runtime.
Kit: Not needed. Components have unmount lifecycle hook. Component stops its own resources:
def unmount @ws&.close @polling_thread&.kill end
Ractor Safety
Tea: Messages cross thread boundaries and must be Ractor-shareable for future Ruby 4.0 compatibility.
Kit: Not needed. Components are mutable by design. State stays within the component. No Ractor isolation required.
Thread Tracking
Tea: Runtime tracks spawned command threads to ensure clean shutdown.
Kit: Not needed. Each component tracks its own resources. Tree traversal during shutdown calls unmount on each component.
What Kit DOES Need
1. Lifecycle Hooks
module Kit::Component def mount # Called when component enters tree end def unmount # Called when component leaves tree end end
These replace Teaβs command spawning and cancellation.
2. Thread Safety for Complex Mutations
For simple mutations (array append, boolean toggle), Rubyβs GIL is sufficient.
For complex mutations:
def mount @mutex = Mutex.new @ws = WebSocket::Client.new(@url) @ws.on_message do |msg| @mutex.synchronize { @messages << msg } end end def render(frame, area) messages = @mutex.synchronize { @messages.dup } # render with messages end
3. Error Handling
Components should rescue and surface errors:
def mount @ws = WebSocket::Client.new(@url) @ws.on_error { |e| @error = e.message } rescue => e @error = e.message end
Comparison Table
| Abstraction | Tea | Kit | Why Different |
|---|---|---|---|
| Outlet | β Required | β Not needed | Kit mutates directly |
| CancellationToken | β Required | β Not needed | Kit has unmount
|
| Ractor safety | β Required | β Not needed | Kit is mutable |
| Thread tracking | β Runtime tracks | β Not needed | Components self-manage |
| Lifecycle hooks | β Not applicable | β Required | Kit needs mount/unmount |
| Mutex | β Uses Outlet | β If complex | Kit needs manual locking |
Architectural Summary
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β ENGINE β β (Immediate-mode, renders every frame) β βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ€ β TEA β KIT β β β β β Immutable Model β Mutable Components β β Commands spawn threads β Components own resources β β Outlet sends messages β Callbacks mutate state β β Runtime tracks threads β unmount cleans up β β Ractor-safe required β GIL + Mutex sufficient β β β β β NEEDS OUTLET β DOESN'T NEED OUTLET β βββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββ
Conclusion
Outlet is Tea-specific. It solves the problem of getting async results into an immutable, unidirectional data flow.
Kitβs paradigmβmutable components with immediate-mode renderingβmakes Outlet unnecessary:
-
Callbacks mutate state β No message routing needed
-
Render every frame β No change notification needed
-
unmounthook β No external cancellation needed -
Components own resources β No runtime tracking needed
The Outlet stays in Tea::Command::Outlet. Kit needs no equivalent.