rooibos 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +9 -5
  3. data/.builds/ruby-3.3.yml +9 -5
  4. data/.builds/ruby-3.4.yml +9 -5
  5. data/.builds/ruby-4.0.0.yml +9 -5
  6. data/AGENTS.md +1 -1
  7. data/CHANGELOG.md +57 -0
  8. data/README.md +2 -2
  9. data/README.rdoc +374 -0
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +1 -1
  12. data/doc/best_practices/forms_and_validation.md +20 -0
  13. data/doc/best_practices/http_workflows.md +20 -0
  14. data/doc/best_practices/index.md +26 -0
  15. data/doc/best_practices/lists_and_tables.md +20 -0
  16. data/doc/best_practices/modal_dialogs.md +20 -0
  17. data/doc/best_practices/no_stateful_widgets.md +184 -0
  18. data/doc/best_practices/orchestration.md +20 -0
  19. data/doc/best_practices/streaming_data.md +20 -0
  20. data/doc/contributors/design/commands_and_outlets.md +1 -1
  21. data/doc/contributors/documentation_plan.md +616 -0
  22. data/doc/contributors/documentation_stub_audit.md +112 -0
  23. data/doc/contributors/documentation_style.md +275 -0
  24. data/doc/contributors/e2e_pty.md +168 -0
  25. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +70 -0
  26. data/doc/contributors/specs/file_browser.md +789 -0
  27. data/doc/contributors/specs/file_browser_stories.md +774 -0
  28. data/doc/contributors/specs/tutorials_to_stories.rb +167 -0
  29. data/doc/contributors/todo/scrollbar.md +118 -0
  30. data/doc/contributors/tutorial_old/01_project_setup.md +20 -0
  31. data/doc/contributors/tutorial_old/02_hello_world.md +24 -0
  32. data/doc/contributors/tutorial_old/03_adding_state.md +26 -0
  33. data/doc/contributors/tutorial_old/06_organizing_your_code.md +20 -0
  34. data/doc/contributors/tutorial_old/07_your_first_command.md +21 -0
  35. data/doc/contributors/tutorial_old/08_the_preview_pane.md +20 -0
  36. data/doc/contributors/tutorial_old/09_loading_states.md +20 -0
  37. data/doc/contributors/tutorial_old/10_testing_your_app.md +20 -0
  38. data/doc/contributors/tutorial_old/11_polish_and_refine.md +20 -0
  39. data/doc/contributors/tutorial_old/12_going_further.md +20 -0
  40. data/doc/contributors/tutorial_old/index.md +20 -0
  41. data/doc/essentials/commands.md +20 -0
  42. data/doc/essentials/index.md +31 -0
  43. data/doc/essentials/messages.md +21 -0
  44. data/doc/essentials/models.md +21 -0
  45. data/doc/essentials/shortcuts.md +19 -0
  46. data/doc/essentials/the_elm_architecture.md +24 -0
  47. data/doc/essentials/the_runtime.md +21 -0
  48. data/doc/essentials/update_functions.md +20 -0
  49. data/doc/essentials/views.md +22 -0
  50. data/doc/getting_started/for_go_developers.md +16 -0
  51. data/doc/getting_started/for_python_developers.md +16 -0
  52. data/doc/getting_started/for_rails_developers.md +17 -0
  53. data/doc/getting_started/for_ratatui_ruby_developers.md +17 -0
  54. data/doc/getting_started/for_react_developers.md +17 -0
  55. data/doc/getting_started/index.md +52 -0
  56. data/doc/getting_started/install.md +20 -0
  57. data/doc/getting_started/quickstart.md +9 -45
  58. data/doc/getting_started/ruby_primer.md +19 -0
  59. data/doc/getting_started/why_rooibos.md +20 -0
  60. data/doc/index.md +79 -11
  61. data/doc/scaling_up/async_patterns.md +20 -0
  62. data/doc/scaling_up/command_composition.md +20 -0
  63. data/doc/scaling_up/custom_commands.md +21 -0
  64. data/doc/scaling_up/fractal_architecture.md +20 -0
  65. data/doc/scaling_up/index.md +30 -0
  66. data/doc/scaling_up/message_routing.md +20 -0
  67. data/doc/scaling_up/ractor_safety.md +20 -0
  68. data/doc/scaling_up/testing.md +21 -0
  69. data/doc/troubleshooting/common_errors.md +20 -0
  70. data/doc/troubleshooting/debugging.md +21 -0
  71. data/doc/troubleshooting/index.md +23 -0
  72. data/doc/troubleshooting/performance.md +20 -0
  73. data/doc/tutorial/01_project_setup.md +44 -0
  74. data/doc/tutorial/02_hello_world.md +45 -0
  75. data/doc/tutorial/03_static_file_list.md +44 -0
  76. data/doc/tutorial/04_arrow_navigation.md +47 -0
  77. data/doc/tutorial/05_real_files.md +45 -0
  78. data/doc/tutorial/06_safe_refactoring.md +21 -0
  79. data/doc/tutorial/07_red_first_tdd.md +26 -0
  80. data/doc/tutorial/08_file_metadata.md +42 -0
  81. data/doc/tutorial/09_text_preview.md +44 -0
  82. data/doc/tutorial/10_directory_tree.md +42 -0
  83. data/doc/tutorial/11_pane_focus.md +40 -0
  84. data/doc/tutorial/12_sorting.md +41 -0
  85. data/doc/tutorial/13_filtering.md +43 -0
  86. data/doc/tutorial/14_toggle_hidden.md +41 -0
  87. data/doc/tutorial/15_text_input_widget.md +43 -0
  88. data/doc/tutorial/16_rename_files.md +42 -0
  89. data/doc/tutorial/17_confirmation_dialogs.md +43 -0
  90. data/doc/tutorial/18_progress_indicators.md +43 -0
  91. data/doc/tutorial/19_atomic_operations.md +42 -0
  92. data/doc/tutorial/20_external_editor.md +42 -0
  93. data/doc/tutorial/21_modal_overlays.md +41 -0
  94. data/doc/tutorial/22_error_handling.md +43 -0
  95. data/doc/tutorial/23_terminal_capabilities.md +53 -0
  96. data/doc/tutorial/24_mouse_events.md +43 -0
  97. data/doc/tutorial/25_resize_events.md +43 -0
  98. data/doc/tutorial/26_loading_states.md +42 -0
  99. data/doc/tutorial/27_performance.md +43 -0
  100. data/doc/tutorial/28_color_schemes.md +47 -0
  101. data/doc/tutorial/29_configuration.md +124 -0
  102. data/doc/tutorial/30_going_further.md +17 -0
  103. data/doc/tutorial/index.md +17 -0
  104. data/examples/app_file_browser/app.rb +40 -0
  105. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +7 -7
  106. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +5 -5
  107. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +1 -1
  108. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +2 -2
  109. data/examples/app_fractal_dashboard/fragments/network_panel.rb +4 -4
  110. data/examples/app_fractal_dashboard/fragments/ping.rb +2 -2
  111. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +4 -4
  112. data/examples/app_fractal_dashboard/fragments/system_info.rb +2 -2
  113. data/examples/app_fractal_dashboard/fragments/uptime.rb +2 -2
  114. data/examples/verify_website_first_app/app.rb +85 -0
  115. data/examples/verify_website_hello_mvu/app.rb +31 -0
  116. data/examples/widget_command_system/app.rb +15 -13
  117. data/exe/rooibos +10 -0
  118. data/generate_tutorial_stubs.rb +126 -0
  119. data/lib/rooibos/cli/commands/new.rb +373 -0
  120. data/lib/rooibos/cli/commands/run.rb +98 -0
  121. data/lib/rooibos/cli.rb +78 -0
  122. data/lib/rooibos/command/all.rb +76 -23
  123. data/lib/rooibos/command/batch.rb +61 -34
  124. data/lib/rooibos/command/custom.rb +84 -1
  125. data/lib/rooibos/command/http.rb +121 -55
  126. data/lib/rooibos/command/lifecycle.rb +5 -5
  127. data/lib/rooibos/command/open.rb +93 -0
  128. data/lib/rooibos/command/outlet.rb +105 -3
  129. data/lib/rooibos/command/wait.rb +9 -6
  130. data/lib/rooibos/command.rb +114 -89
  131. data/lib/rooibos/message/batch.rb +39 -0
  132. data/lib/rooibos/message/canceled.rb +51 -0
  133. data/lib/rooibos/message/error.rb +48 -0
  134. data/lib/rooibos/message/open.rb +30 -0
  135. data/lib/rooibos/message.rb +84 -4
  136. data/lib/rooibos/router.rb +11 -14
  137. data/lib/rooibos/runtime.rb +40 -43
  138. data/lib/rooibos/shortcuts.rb +47 -0
  139. data/lib/rooibos/test_helper.rb +71 -6
  140. data/lib/rooibos/version.rb +1 -1
  141. data/lib/rooibos/welcome.rb +237 -0
  142. data/lib/rooibos.rb +4 -3
  143. data/mise.toml +1 -1
  144. data/rbs_collection.lock.yaml +2 -2
  145. data/sig/concurrent.rbs +4 -0
  146. data/sig/gem.rbs +20 -0
  147. data/sig/rooibos/cli.rbs +42 -0
  148. data/sig/rooibos/command.rbs +59 -7
  149. data/sig/rooibos/message.rbs +66 -2
  150. data/sig/rooibos/shortcuts.rbs +14 -0
  151. data/sig/rooibos/test_helper.rbs +6 -2
  152. data/sig/rooibos/welcome.rbs +75 -0
  153. data/tasks/install.rake +29 -0
  154. data/tasks/resources/build.yml.erb +2 -0
  155. metadata +274 -38
  156. data/doc/concepts/application_architecture.md +0 -197
  157. data/doc/concepts/application_testing.md +0 -49
  158. data/doc/concepts/async_work.md +0 -164
  159. data/doc/concepts/commands.md +0 -530
  160. data/doc/concepts/message_processing.md +0 -51
  161. data/doc/contributors/WIP/decomposition_strategies_analysis.md +0 -258
  162. data/doc/contributors/WIP/implementation_plan.md +0 -409
  163. data/doc/contributors/WIP/init_callable_proposal.md +0 -344
  164. data/doc/contributors/WIP/runtime_refactoring_status.md +0 -47
  165. data/doc/contributors/WIP/task.md +0 -36
  166. data/doc/contributors/WIP/v0.4.0_todo.md +0 -468
  167. data/doc/contributors/kit-no-outlet.md +0 -238
  168. data/doc/contributors/priorities.md +0 -38
  169. data/doc/images/.gitkeep +0 -0
  170. data/exe/.gitkeep +0 -0
  171. /data/doc/contributors/{WIP → design}/mvu_tea_implementations_research.md +0 -0
@@ -1,197 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Application Architecture
7
-
8
- Build robust TUI applications with Rooibos patterns.
9
-
10
- ## Core Concepts
11
-
12
- _This section is incomplete. Check the source files._
13
-
14
- ## Thread and Ractor Safety
15
-
16
- ### The Strategic Context
17
-
18
- Ruby 4.0 introduces [Ractors](https://docs.ruby-lang.org/en/4.0/Ractor.html)—
19
- true parallel actors that forbid shared mutable state. Code that passes
20
- mutable objects between threads crashes in a Ractor world.
21
-
22
- Rooibos prepares you today. The runtime enforces Ractor-shareability on every
23
- Model and Message *now*, using standard threads. Pass a mutable object,
24
- and it raises an error immediately. Write Ractor-safe code today; upgrade
25
- to Ruby 4.0 without changes tomorrow.
26
-
27
- Enforce immutability rules before you strictly need them, and the migration
28
- is invisible.
29
-
30
- ### The Problem
31
-
32
- Ruby's Ractor model prevents data races by forbidding shared mutable state.
33
- Mutable objects cause runtime errors:
34
-
35
- ```
36
- RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
37
- ```
38
-
39
- ### The Solution
40
-
41
- Use `Ractor.make_shareable`. It recursively freezes everything:
42
-
43
- ```ruby
44
- Ractor.make_shareable(model.with(text: "#{model.text}#{char}"))
45
- ```
46
-
47
- For constants, wrap INITIAL:
48
-
49
- ```ruby
50
- INITIAL = Ractor.make_shareable(
51
- Model.new(text: "", running: false, chunks: [])
52
- )
53
- ```
54
-
55
- For collection updates:
56
-
57
- ```ruby
58
- new_chunks = Ractor.make_shareable([*model.chunks, new_item])
59
- ```
60
-
61
- ### The Lightweight Alternative
62
-
63
- When you know exactly what's mutable, `.freeze` is shorter:
64
-
65
- ```ruby
66
- [model.with(text: "#{model.text}#{char}".freeze), nil]
67
- ```
68
-
69
- ### Foot-Guns
70
-
71
- #### frozen_string_literal Only Affects Literals
72
-
73
- The magic comment freezes strings that appear directly in source code.
74
- Computed strings are mutable.
75
-
76
- ```ruby
77
- # frozen_string_literal: true
78
-
79
- "literal" # frozen ✓
80
- "#{var}" # mutable ✗
81
- str.chop # mutable ✗
82
- str + other # mutable ✗
83
- ```
84
-
85
- #### Data.define Needs Shareable Values
86
-
87
- `Data.define` creates frozen instances. The instance is Ractor-shareable
88
- only when all its values are shareable.
89
-
90
- ### Quick Reference
91
-
92
- | Pattern | Code |
93
- |---------|------|
94
- | Make anything shareable | `Ractor.make_shareable(obj)` |
95
- | Freeze a string | `str.freeze` |
96
- | INITIAL constant | `Ractor.make_shareable(Model.new(...))` |
97
- | Array update | `Ractor.make_shareable([*old, new])` |
98
-
99
- ### Debugging
100
-
101
- See this error?
102
-
103
- ```
104
- RatatuiRuby::Error::Invariant: Model is not Ractor-shareable.
105
- ```
106
-
107
- Wrap the returned model with `Ractor.make_shareable`.
108
-
109
- ## Modals and Command Result Routing
110
-
111
- Modals capture keyboard input. They overlay the main UI and intercept keypresses until dismissed. But async commands keep running in the background. When their results arrive, they look like any other message.
112
-
113
- It's tempting to intercept *all* messages when the modal is active. This swallows those command results.
114
-
115
- ### The Scenario
116
-
117
- 1. User presses "u" to fetch uptime. The runtime dispatches an async command.
118
- 2. User presses "c" to open a modal dialog.
119
- 3. The uptime command completes. It sends `[:network, :uptime, { stdout:, ... }]`.
120
- 4. The modal intercept sees the modal is active. It routes that message to the modal.
121
- 5. The modal ignores it.
122
- 6. The uptime panel never updates.
123
-
124
- ### The Fix
125
-
126
- Route command results before modal interception. Modals intercept user input, not async results.
127
-
128
- <!-- SPDX-SnippetBegin -->
129
- <!--
130
- SPDX-FileCopyrightText: 2026 Kerrick Long
131
- SPDX-License-Identifier: MIT-0
132
- -->
133
- ```ruby
134
- # Wrong: modal intercepts everything
135
- UPDATE = lambda do |message, model|
136
- if Modal.active?(model.modal)
137
- # Swallows command results
138
- return Modal::UPDATE.call(message, model.modal)
139
- end
140
-
141
- case message
142
- in [:network, *rest]
143
- # Never reached while modal is open
144
- end
145
- end
146
-
147
- # Correct: route command results first
148
- UPDATE = lambda do |message, model|
149
- # 1. Route async command results (always)
150
- case message
151
- in [:network, *rest]
152
- return [model.with(network: new_network), command]
153
- in [:stats, *rest]
154
- return [model.with(stats: new_stats), command]
155
- else
156
- nil
157
- end
158
-
159
- # 2. Modal intercepts user input
160
- if Modal.active?(model.modal)
161
- return Modal::UPDATE.call(message, model.modal)
162
- end
163
-
164
- # 3. Handle other input
165
- case message
166
- in _ if message.q? then Command.exit
167
- end
168
- end
169
- ```
170
- <!-- SPDX-SnippetEnd -->
171
-
172
- ### The Router DSL
173
-
174
- `Rooibos::Router` handles this correctly. Routes declared with `route :prefix, to: ChildModule` process before keymap handlers. Command results flow through even when guards block keyboard input.
175
-
176
- <!-- SPDX-SnippetBegin -->
177
- <!--
178
- SPDX-FileCopyrightText: 2026 Kerrick Long
179
- SPDX-License-Identifier: MIT-0
180
- -->
181
-
182
- ```ruby
183
-
184
- module Dashboard
185
- include Rooibos::Router
186
-
187
- route :stats, to: StatsPanel
188
- route :network, to: NetworkPanel
189
-
190
- keymap do
191
- only when: MODAL_INACTIVE do
192
- key "u", -> { Uptime.fetch_command }
193
- end
194
- end
195
- end
196
- ```
197
- <!-- SPDX-SnippetEnd -->
@@ -1,49 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
- # Application Testing Guide
6
-
7
- This guide explains how to test your RatatuiRuby applications using the provided `RatatuiRuby::TestHelper`.
8
-
9
- ## Overview
10
-
11
- You need to verify that your application looks and behaves correctly. Manually checking every character on a terminal screen is tedious. Dealing with race conditions and complex state management in tests creates friction.
12
-
13
- The `TestHelper` module solves this. It provides a headless "test terminal" to capture output and a suite of robust assertions to verify state.
14
-
15
- Use it to write fast, deterministic tests for your TUI applications.
16
-
17
- ## Setup
18
-
19
- First, require the test helper in your test file or `test_helper.rb`:
20
-
21
- <!-- SPDX-SnippetBegin -->
22
- <!--
23
- SPDX-FileCopyrightText: 2026 Kerrick Long
24
- SPDX-License-Identifier: MIT-0
25
- -->
26
- ```ruby
27
- require "ratatui_ruby/test_helper"
28
- require "minitest/autorun" # or your preferred test framework
29
- ```
30
- <!-- SPDX-SnippetEnd -->
31
-
32
- Then, include the module in your test class:
33
-
34
- <!-- SPDX-SnippetBegin -->
35
- <!--
36
- SPDX-FileCopyrightText: 2026 Kerrick Long
37
- SPDX-License-Identifier: MIT-0
38
- -->
39
- ```ruby
40
- class MyApplicationTest < Minitest::Test
41
- include RatatuiRuby::TestHelper
42
- # ...
43
- end
44
- ```
45
- <!-- SPDX-SnippetEnd -->
46
-
47
- ## Writing Tests
48
-
49
- _Because this gem is in pre-release, it lacks documentation. Please check the source files and automated tests._
@@ -1,164 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
- # Async Work
6
-
7
- ## Context
8
-
9
- Your application does concurrent work. It fetches from multiple APIs. It reads from sockets. It processes streams. These operations overlap in time.
10
-
11
- ## Problem
12
-
13
- Threads are hard. Exceptions in spawned threads vanish silently. The main thread never learns what happened. Your application hangs, waiting for messages that will never arrive. Debugging this is miserable.
14
-
15
- ## Solution
16
-
17
- Rooibos handles concurrency for you. Two patterns cover nearly every case. Use them instead of raw threads.
18
-
19
- ## Pattern 1: Command Orchestration
20
-
21
- Compose child commands instead of spawning threads. Use <tt>out.source</tt> for sequential steps. Use <tt>Command.all</tt> for parallel steps.
22
-
23
- <!-- SPDX-SnippetBegin -->
24
- <!--
25
- SPDX-FileCopyrightText: 2026 Kerrick Long
26
- SPDX-License-Identifier: MIT-0
27
- -->
28
- ```ruby
29
- class LoadDashboard < Data.define(:user_id, :tag)
30
- include Rooibos::Command::Custom
31
-
32
- def call(out, token)
33
- # Step 1: Authenticate (sequential - we need the token first)
34
- auth = out.source(Authenticate.new(user_id:, tag: :_), token)
35
- return if auth.nil? || token.canceled?
36
-
37
- # Step 2: Fetch dashboard data in parallel, waiting for all to complete
38
- dashboard = out.source(
39
- Command.all(:_, [
40
- FetchProfile.new(token: auth[:token], tag: :profile),
41
- FetchNotifications.new(token: auth[:token], tag: :notifications),
42
- FetchWeather.new(tag: :weather)
43
- ]),
44
- token
45
- )
46
- return if dashboard.nil? || token.canceled?
47
-
48
- # Step 3: Send a message to the update with the dashboard data
49
- out.put(tag, dashboard.results)
50
- return if token.canceled?
51
-
52
- # Step 4: Log the access (sequential - after we have data)
53
- out.source(LogAccess.new(user_id:, tag: :_), token)
54
- return if token.canceled?
55
-
56
- # COMING SOON: out.source_nonblock
57
- # Step 5: Watch for new data via HTTP Server-Sent Events, handling parallel streaming data
58
- # 5a: Pass messages from the StreamNotifications custom command directly to LoadDashboard's out.put
59
- notifications = out.source_nonblocki(
60
- StreamNotifications.new(:user_id, auth[:token]),
61
- token,
62
- )
63
- # 5b: Do work to the messages before sending them to the update function
64
- deltas = out.source_nonblock(
65
- StreamDashboardDeltas.new(dashboard.results[:profile][:id], auth[:token]),
66
- token,
67
- DeltaPostProcessor.new(:user_id)
68
- )
69
- # 5c: block this command on both async outsourced commands finishing
70
- out.last(notifications, deltas)
71
- return if token.canceled?
72
-
73
- # Step 6: Log completion
74
- out.source(LogAccess.new(user_id:, tag: :_, finished: true), token)
75
- end
76
- end
77
- ```
78
- <!-- SPDX-SnippetEnd -->
79
-
80
- <tt>out.source</tt> blocks until the child command finishes. Pass <tt>Command.all</tt> to run children in parallel. Exceptions propagate correctly. Cancellation stops the workflow at any point.
81
-
82
- Use this for any multi-step workflow with dependencies between stages.
83
-
84
- ## Pattern 2: Multiplexed I/O
85
-
86
- Read from multiple sources without threads. Ruby's <tt>IO.select</tt> waits for any of several IOs to become ready.
87
-
88
- <!-- SPDX-SnippetBegin -->
89
- <!--
90
- SPDX-FileCopyrightText: 2026 Kerrick Long
91
- SPDX-License-Identifier: MIT-0
92
- -->
93
- ```ruby
94
- class MultiSocketReader < Data.define(:sockets, :tag)
95
- include Rooibos::Command::Custom
96
-
97
- def call(out, token)
98
- remaining = sockets.dup
99
-
100
- until remaining.empty? || token.canceled?
101
- # Wait up to 0.1s for any socket to have data
102
- ready = IO.select(remaining, nil, nil, 0.1)
103
- next unless ready
104
-
105
- ready[0].each do |socket|
106
- data = socket.read_nonblock(4096, exception: false)
107
- case data
108
- when :wait_readable
109
- next
110
- when nil
111
- remaining.delete(socket)
112
- else
113
- out.put(:data, { socket: socket, chunk: data })
114
- end
115
- end
116
- end
117
-
118
- out.put(tag, :complete)
119
- end
120
- end
121
- ```
122
- <!-- SPDX-SnippetEnd -->
123
-
124
- <tt>IO.select</tt> multiplexes reads across sockets, pipes, or files. One thread handles many connections. No spawned threads means no silent failures.
125
-
126
- Use this for chat clients, log tailers, or any multi-stream scenario.
127
-
128
- ## Why Not Threads?
129
-
130
- You might wonder: "Why can't I just spawn a thread?"
131
-
132
- <!-- SPDX-SnippetBegin -->
133
- <!--
134
- SPDX-FileCopyrightText: 2026 Kerrick Long
135
- SPDX-License-Identifier: MIT-0
136
- -->
137
- ```ruby
138
- # ❌ Don't do this
139
- def call(out, token)
140
- Thread.new do
141
- data = fetch_something
142
- out.put(:result, data)
143
- end
144
- end
145
- ```
146
- <!-- SPDX-SnippetEnd -->
147
-
148
- This looks harmless. It hides a trap.
149
-
150
- If <tt>fetch_something</tt> raises, the exception happens in the spawned thread. Ruby logs it. The main thread never sees it. Your <tt>call</tt> method returns. The runtime considers the command complete. But <tt>out.put</tt> never ran. Your update function waits for <tt>:result</tt> forever.
151
-
152
- The runtime cannot protect you from this. Threads spawned inside your command escape its error handling. The framework wraps <tt>call</tt> in a rescue. It does not wrap threads you create.
153
-
154
- Use the patterns above instead. They keep errors visible.
155
-
156
- ## Choosing the Right Pattern
157
-
158
- | Situation | Pattern |
159
- |-----------|---------|
160
- | Multi-step workflows | <tt>out.source</tt> + <tt>Command.all</tt> |
161
- | Read from multiple sockets/pipes | <tt>IO.select</tt> |
162
- | One blocking operation | Just do it in <tt>call</tt> |
163
-
164
- Commands already run off the main thread. You rarely need additional concurrency. When you do, these patterns handle the hard parts.