rooibos 0.6.2 → 0.7.0

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 (217) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSES/BSD-2-Clause.txt +9 -0
  3. data/REUSE.toml +5 -0
  4. data/exe/.gitkeep +0 -0
  5. data/lib/rooibos/cli/commands/new.rb +24 -0
  6. data/lib/rooibos/command/batch.rb +10 -0
  7. data/lib/rooibos/command/bubble.rb +34 -0
  8. data/lib/rooibos/command/custom.rb +3 -2
  9. data/lib/rooibos/command/deliver.rb +50 -0
  10. data/lib/rooibos/command/http.rb +1 -1
  11. data/lib/rooibos/command/lifecycle.rb +3 -1
  12. data/lib/rooibos/command/outlet.rb +19 -9
  13. data/lib/rooibos/command.rb +107 -3
  14. data/lib/rooibos/configuration.rb +29 -0
  15. data/lib/rooibos/message/bubbled.rb +29 -0
  16. data/lib/rooibos/message.rb +24 -6
  17. data/lib/rooibos/router/action.rb +36 -0
  18. data/lib/rooibos/router/flow/dispatch.rb +39 -0
  19. data/lib/rooibos/router/flow/inward.rb +41 -0
  20. data/lib/rooibos/router/flow/outward.rb +44 -0
  21. data/lib/rooibos/router/guard.rb +56 -0
  22. data/lib/rooibos/router/predicate.rb +65 -0
  23. data/lib/rooibos/router/registry/actions.rb +41 -0
  24. data/lib/rooibos/router/registry/forwards.rb +58 -0
  25. data/lib/rooibos/router/registry/observes.rb +57 -0
  26. data/lib/rooibos/router/registry/otherwises.rb +29 -0
  27. data/lib/rooibos/router/registry/receives.rb +57 -0
  28. data/lib/rooibos/router/registry/routes.rb +59 -0
  29. data/lib/rooibos/router/registry.rb +26 -0
  30. data/lib/rooibos/router/route.rb +42 -0
  31. data/lib/rooibos/router/router_update.rb +53 -0
  32. data/lib/rooibos/router/rule/forward.rb +39 -0
  33. data/lib/rooibos/router/rule/observe.rb +22 -0
  34. data/lib/rooibos/router/rule/otherwise.rb +26 -0
  35. data/lib/rooibos/router/rule/receive.rb +22 -0
  36. data/lib/rooibos/router/rule.rb +40 -0
  37. data/lib/rooibos/router.rb +424 -438
  38. data/lib/rooibos/runtime.rb +37 -52
  39. data/lib/rooibos/test_helper.rb +22 -0
  40. data/lib/rooibos/transition.rb +92 -0
  41. data/lib/rooibos/version.rb +1 -1
  42. data/lib/rooibos.rb +2 -57
  43. data/sig/rooibos/cli.rbs +1 -0
  44. data/sig/rooibos/command.rbs +44 -0
  45. data/sig/rooibos/configuration.rbs +20 -0
  46. data/sig/rooibos/message.rbs +12 -0
  47. data/sig/rooibos/router/action.rbs +33 -0
  48. data/sig/rooibos/router/actions.rbs +27 -0
  49. data/sig/rooibos/router/flow/dispatch.rbs +29 -0
  50. data/sig/rooibos/router/flow/inward.rbs +37 -0
  51. data/sig/rooibos/router/flow/outward.rbs +36 -0
  52. data/sig/rooibos/router/forward.rbs +35 -0
  53. data/sig/rooibos/router/forwards.rbs +34 -0
  54. data/sig/rooibos/router/guard.rbs +21 -0
  55. data/sig/rooibos/router/observe.rbs +20 -0
  56. data/sig/rooibos/router/observes.rbs +38 -0
  57. data/sig/rooibos/router/otherwise.rbs +22 -0
  58. data/sig/rooibos/router/otherwises.rbs +20 -0
  59. data/sig/rooibos/router/predicate.rbs +51 -0
  60. data/sig/rooibos/router/receive.rbs +20 -0
  61. data/sig/rooibos/router/receives.rbs +38 -0
  62. data/sig/rooibos/router/registry.rbs +24 -0
  63. data/sig/rooibos/router/route.rbs +46 -0
  64. data/sig/rooibos/router/router_update.rbs +33 -0
  65. data/sig/rooibos/router/routes.rbs +41 -0
  66. data/sig/rooibos/router/rule.rbs +36 -0
  67. data/sig/rooibos/router.rbs +216 -161
  68. data/sig/rooibos/runtime.rbs +0 -1
  69. data/sig/rooibos/test_helper.rbs +6 -0
  70. data/sig/rooibos/transition.rbs +33 -0
  71. data/sig/rooibos.rbs +0 -10
  72. metadata +144 -198
  73. data/.builds/ruby-3.2.yml +0 -55
  74. data/.builds/ruby-3.3.yml +0 -55
  75. data/.builds/ruby-3.4.yml +0 -55
  76. data/.builds/ruby-4.0.0.yml +0 -55
  77. data/.pre-commit-config.yaml +0 -16
  78. data/.rubocop.yml +0 -8
  79. data/AGENTS.md +0 -108
  80. data/CHANGELOG.md +0 -308
  81. data/README.md +0 -183
  82. data/README.rdoc +0 -374
  83. data/Rakefile +0 -16
  84. data/Steepfile +0 -13
  85. data/doc/best_practices/forms_and_validation.md +0 -20
  86. data/doc/best_practices/http_workflows.md +0 -20
  87. data/doc/best_practices/index.md +0 -26
  88. data/doc/best_practices/lists_and_tables.md +0 -20
  89. data/doc/best_practices/modal_dialogs.md +0 -20
  90. data/doc/best_practices/no_stateful_widgets.md +0 -184
  91. data/doc/best_practices/orchestration.md +0 -20
  92. data/doc/best_practices/streaming_data.md +0 -20
  93. data/doc/contributors/design/commands_and_outlets.md +0 -214
  94. data/doc/contributors/design/mvu_tea_implementations_research.md +0 -373
  95. data/doc/contributors/documentation_plan.md +0 -616
  96. data/doc/contributors/documentation_stub_audit.md +0 -112
  97. data/doc/contributors/documentation_style.md +0 -275
  98. data/doc/contributors/e2e_pty.md +0 -168
  99. data/doc/contributors/maybe_stateful_router.md +0 -56
  100. data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +0 -70
  101. data/doc/contributors/specs/file_browser.md +0 -789
  102. data/doc/contributors/specs/file_browser_stories.md +0 -784
  103. data/doc/contributors/specs/tutorials_to_stories.rb +0 -167
  104. data/doc/contributors/todo/scrollbar.md +0 -118
  105. data/doc/contributors/tutorial_old/01_project_setup.md +0 -20
  106. data/doc/contributors/tutorial_old/02_hello_world.md +0 -24
  107. data/doc/contributors/tutorial_old/03_adding_state.md +0 -26
  108. data/doc/contributors/tutorial_old/06_organizing_your_code.md +0 -20
  109. data/doc/contributors/tutorial_old/07_your_first_command.md +0 -21
  110. data/doc/contributors/tutorial_old/08_the_preview_pane.md +0 -20
  111. data/doc/contributors/tutorial_old/09_loading_states.md +0 -20
  112. data/doc/contributors/tutorial_old/10_testing_your_app.md +0 -20
  113. data/doc/contributors/tutorial_old/11_polish_and_refine.md +0 -20
  114. data/doc/contributors/tutorial_old/12_going_further.md +0 -20
  115. data/doc/contributors/tutorial_old/index.md +0 -20
  116. data/doc/custom.css +0 -22
  117. data/doc/essentials/commands.md +0 -20
  118. data/doc/essentials/index.md +0 -31
  119. data/doc/essentials/messages.md +0 -21
  120. data/doc/essentials/models.md +0 -21
  121. data/doc/essentials/shortcuts.md +0 -19
  122. data/doc/essentials/the_elm_architecture.md +0 -24
  123. data/doc/essentials/the_runtime.md +0 -21
  124. data/doc/essentials/update_functions.md +0 -20
  125. data/doc/essentials/views.md +0 -22
  126. data/doc/getting_started/for_go_developers.md +0 -16
  127. data/doc/getting_started/for_python_developers.md +0 -16
  128. data/doc/getting_started/for_rails_developers.md +0 -17
  129. data/doc/getting_started/for_ratatui_ruby_developers.md +0 -17
  130. data/doc/getting_started/for_react_developers.md +0 -17
  131. data/doc/getting_started/index.md +0 -52
  132. data/doc/getting_started/install.md +0 -20
  133. data/doc/getting_started/quickstart.md +0 -20
  134. data/doc/getting_started/ruby_primer.md +0 -19
  135. data/doc/getting_started/why_rooibos.md +0 -20
  136. data/doc/images/verify_readme_usage.png +0 -0
  137. data/doc/images/widget_cmd_exec.png +0 -0
  138. data/doc/index.md +0 -93
  139. data/doc/scaling_up/async_patterns.md +0 -20
  140. data/doc/scaling_up/command_composition.md +0 -20
  141. data/doc/scaling_up/custom_commands.md +0 -21
  142. data/doc/scaling_up/fractal_architecture.md +0 -20
  143. data/doc/scaling_up/index.md +0 -30
  144. data/doc/scaling_up/message_routing.md +0 -20
  145. data/doc/scaling_up/ractor_safety.md +0 -20
  146. data/doc/scaling_up/testing.md +0 -21
  147. data/doc/troubleshooting/common_errors.md +0 -20
  148. data/doc/troubleshooting/debugging.md +0 -21
  149. data/doc/troubleshooting/index.md +0 -23
  150. data/doc/troubleshooting/performance.md +0 -20
  151. data/doc/tutorial/01_project_setup.md +0 -44
  152. data/doc/tutorial/02_hello_world.md +0 -45
  153. data/doc/tutorial/03_static_file_list.md +0 -44
  154. data/doc/tutorial/04_arrow_navigation.md +0 -47
  155. data/doc/tutorial/05_real_files.md +0 -45
  156. data/doc/tutorial/06_safe_refactoring.md +0 -21
  157. data/doc/tutorial/07_red_first_tdd.md +0 -26
  158. data/doc/tutorial/08_file_metadata.md +0 -42
  159. data/doc/tutorial/09_text_preview.md +0 -44
  160. data/doc/tutorial/10_directory_tree.md +0 -42
  161. data/doc/tutorial/11_pane_focus.md +0 -40
  162. data/doc/tutorial/12_sorting.md +0 -41
  163. data/doc/tutorial/13_filtering.md +0 -43
  164. data/doc/tutorial/14_toggle_hidden.md +0 -41
  165. data/doc/tutorial/15_text_input_widget.md +0 -43
  166. data/doc/tutorial/16_rename_files.md +0 -42
  167. data/doc/tutorial/17_confirmation_dialogs.md +0 -43
  168. data/doc/tutorial/18_progress_indicators.md +0 -43
  169. data/doc/tutorial/19_atomic_operations.md +0 -42
  170. data/doc/tutorial/20_external_editor.md +0 -42
  171. data/doc/tutorial/21_modal_overlays.md +0 -41
  172. data/doc/tutorial/22_error_handling.md +0 -43
  173. data/doc/tutorial/23_terminal_capabilities.md +0 -53
  174. data/doc/tutorial/24_mouse_events.md +0 -43
  175. data/doc/tutorial/25_resize_events.md +0 -43
  176. data/doc/tutorial/26_loading_states.md +0 -42
  177. data/doc/tutorial/27_performance.md +0 -43
  178. data/doc/tutorial/28_color_schemes.md +0 -47
  179. data/doc/tutorial/29_configuration.md +0 -124
  180. data/doc/tutorial/30_going_further.md +0 -17
  181. data/doc/tutorial/index.md +0 -17
  182. data/examples/app_fractal_dashboard/README.md +0 -60
  183. data/examples/app_fractal_dashboard/app.rb +0 -63
  184. data/examples/app_fractal_dashboard/dashboard/base.rb +0 -73
  185. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +0 -86
  186. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +0 -87
  187. data/examples/app_fractal_dashboard/dashboard/update_router.rb +0 -43
  188. data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +0 -81
  189. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +0 -82
  190. data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +0 -90
  191. data/examples/app_fractal_dashboard/fragments/disk_usage.rb +0 -47
  192. data/examples/app_fractal_dashboard/fragments/network_panel.rb +0 -45
  193. data/examples/app_fractal_dashboard/fragments/ping.rb +0 -47
  194. data/examples/app_fractal_dashboard/fragments/stats_panel.rb +0 -45
  195. data/examples/app_fractal_dashboard/fragments/system_info.rb +0 -47
  196. data/examples/app_fractal_dashboard/fragments/uptime.rb +0 -47
  197. data/examples/tutorial/01/app.rb +0 -50
  198. data/examples/tutorial/02/app.rb +0 -64
  199. data/examples/tutorial/03/app.rb +0 -91
  200. data/examples/tutorial/06_safe_refactoring/app.rb +0 -124
  201. data/examples/verify_readme_usage/README.md +0 -54
  202. data/examples/verify_readme_usage/app.rb +0 -47
  203. data/examples/verify_website_first_app/app.rb +0 -85
  204. data/examples/verify_website_hello_mvu/app.rb +0 -31
  205. data/examples/widget_command_system/README.md +0 -70
  206. data/examples/widget_command_system/app.rb +0 -134
  207. data/generate_tutorial_stubs.rb +0 -126
  208. data/mise.toml +0 -8
  209. data/rbs_collection.lock.yaml +0 -108
  210. data/rbs_collection.yaml +0 -15
  211. data/tasks/example_viewer.html.erb +0 -172
  212. data/tasks/install.rake +0 -29
  213. data/tasks/resources/build.yml.erb +0 -55
  214. data/tasks/resources/index.html.erb +0 -44
  215. data/tasks/resources/rubies.yml +0 -7
  216. data/tasks/steep.rake +0 -11
  217. /data/{vendor/goodcop/base.yml → lib/rooibos/rubocop.yml} +0 -0
@@ -1,20 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Modal Dialogs
7
-
8
-
9
- By the end of this guide, you will:
10
-
11
- - Implement a modal overlay that captures focus
12
- - Route the dialog result (confirm/cancel) back to the parent
13
- - Handle the "escape to close" pattern
14
- - Design reusable modal fragments
15
-
16
- > ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
17
-
18
- ---
19
-
20
- **Best Practices:** [Forms and Validation](./forms_and_validation.md) | [Lists and Tables](./lists_and_tables.md) | [HTTP Workflows](./http_workflows.md) | [Streaming Data](./streaming_data.md) | [Orchestration](./orchestration.md)
@@ -1,184 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Stateless Widgets in MVU
7
-
8
- After reading this guide, you will know:
9
-
10
- - Why Rooibos uses stateless widgets instead of stateful widgets
11
- - How to achieve selection highlighting, scrolling, and bounds clamping
12
- - When to handle Resize events for viewport-aware layouts
13
-
14
- ---
15
-
16
- ## Context
17
-
18
- RatatuiRuby offers two widget rendering modes: stateless (`render_widget`) and stateful (`render_stateful_widget`). Stateful widgets accept a mutable state object during render, updating scroll offsets and clamping selections based on viewport size.
19
-
20
- ```ruby
21
- # Stateful approach (imperative, inside draw block)
22
- RatatuiRuby.draw do |frame|
23
- frame.render_stateful_widget(list, frame.area, @list_state)
24
- # @list_state is mutated during render
25
- end
26
- ```
27
-
28
- ## Problem
29
-
30
- MVU separates *what to display* (View) from *when to render* (Runtime). The View callable builds a widget tree. The Runtime renders it later.
31
-
32
- Stateful widgets blur this boundary. They mutate state during render, creating hidden side effects. This breaks the MVU guarantee: **state only changes via Update**.
33
-
34
- Passing mutable state during render means:
35
-
36
- 1. State changes happen outside Update
37
- 2. State mutations are invisible to the message flow
38
- 3. Testing becomes harder—you can't predict state after render
39
-
40
- ## Solution
41
-
42
- Rooibos uses stateless widgets exclusively. All state management happens in your Model, computed by your Update function.
43
-
44
- The "magic" that stateful widgets perform is simple arithmetic. You can do it yourself.
45
-
46
- ---
47
-
48
- ## What Stateful Widgets Actually Do
49
-
50
- Stateful widgets perform two render-time operations:
51
-
52
- ### 1. Selection Bounds Clamping
53
-
54
- If `selected >= items.length`, clamp to the last item:
55
-
56
- ```ruby
57
- # In Update, after any selection change
58
- def clamp_selection(model)
59
- max_index = model.items.length - 1
60
- if model.selected && model.selected > max_index
61
- model.with(selected: [max_index, 0].max)
62
- else
63
- model
64
- end
65
- end
66
- ```
67
-
68
- ### 2. Scroll Offset Calculation
69
-
70
- Compute which item should be at the top of the visible window:
71
-
72
- ```ruby
73
- # In Update, after selection or viewport changes
74
- def calculate_offset(model)
75
- return model unless model.selected
76
-
77
- visible_count = model.viewport.height
78
- selected = model.selected
79
- offset = model.offset
80
-
81
- # If selected is above current view, scroll up
82
- if selected < offset
83
- model.with(offset: selected)
84
- # If selected is below current view, scroll down
85
- elsif selected >= offset + visible_count
86
- model.with(offset: selected - visible_count + 1)
87
- else
88
- model
89
- end
90
- end
91
- ```
92
-
93
- ---
94
-
95
- ## Storing Viewport Dimensions
96
-
97
- Your Model stores the terminal dimensions. Query them in `Init` and update on `Resize` events.
98
-
99
- ### Init
100
-
101
- ```ruby
102
- Init = -> {
103
- [
104
- Model.new(
105
- items: [],
106
- selected: nil,
107
- offset: 0,
108
- viewport: RatatuiRuby.viewport_area
109
- ),
110
- fetch_items_command
111
- ]
112
- }
113
- ```
114
-
115
- ### Update
116
-
117
- ```ruby
118
- Update = ->(model, message) {
119
- case message
120
- in Rooibos::Message::Resize(width:, height:)
121
- new_viewport = RatatuiRuby::Layout::Rect.new(x: 0, y: 0, width:, height:)
122
- new_model = model.with(viewport: new_viewport)
123
- [calculate_offset(new_model), nil]
124
- in SelectNext
125
- new_selected = [(model.selected || -1) + 1, model.items.length - 1].min
126
- new_model = model.with(selected: new_selected)
127
- [calculate_offset(new_model), nil]
128
- # ...
129
- end
130
- }
131
- ```
132
-
133
- ### View
134
-
135
- ```ruby
136
- View = ->(model, tui) {
137
- visible_items = model.items[model.offset, model.viewport.height] || []
138
-
139
- list = tui.list(
140
- items: visible_items,
141
- highlight_style: tui.style(fg: :yellow)
142
- )
143
-
144
- # Highlighting uses the offset-adjusted index
145
- if model.selected
146
- visible_selected = model.selected - model.offset
147
- list = list.with(selected_index: visible_selected)
148
- end
149
-
150
- list
151
- }
152
- ```
153
-
154
- ---
155
-
156
- ## Benefits
157
-
158
- ### Testable State
159
-
160
- All state changes happen in Update. Test your logic without rendering:
161
-
162
- ```ruby
163
- def test_selection_clamps_to_bounds
164
- viewport = RatatuiRuby::Layout::Rect.new(x: 0, y: 0, width: 80, height: 10)
165
- model = Model.new(items: ["a", "b"], selected: 5, offset: 0, viewport:)
166
- result, _ = Update.call(model, ClampSelection.new)
167
- assert_equal 1, result.selected
168
- end
169
- ```
170
-
171
- ### Predictable Rendering
172
-
173
- View is a pure function. Given the same Model, it returns the same widget tree. No hidden mutations.
174
-
175
- ### No Frame Dependency
176
-
177
- View runs before the draw callback. Terminal queries work normally.
178
-
179
- ---
180
-
181
- ## See Also
182
-
183
- - [The Elm Architecture](../essentials/the_elm_architecture.md) — Core MVU concepts
184
- - [Lists and Tables](lists_and_tables.md) — Patterns for scrollable collections
@@ -1,20 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Orchestration
7
-
8
-
9
- By the end of this guide, you will:
10
-
11
- - Coordinate parallel commands with `Command.all`
12
- - Sequence dependent operations (A then B then C)
13
- - Handle partial failures in multi-step workflows
14
- - Design robust async pipelines
15
-
16
- > ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
17
-
18
- ---
19
-
20
- **Best Practices:** [Modal Dialogs](./modal_dialogs.md) | [Forms and Validation](./forms_and_validation.md) | [Lists and Tables](./lists_and_tables.md) | [HTTP Workflows](./http_workflows.md) | [Streaming Data](./streaming_data.md)
@@ -1,20 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Streaming Data
7
-
8
-
9
- By the end of this guide, you will:
10
-
11
- - Handle SSE (Server-Sent Events) streams
12
- - Process websocket messages as they arrive
13
- - Update UI incrementally without blocking
14
- - Clean up streaming connections on app exit
15
-
16
- > ⚠️ **This page is a stub.** Help us write it! See the [Documentation Plan](../contributors/documentation_plan.md) and [Style Guide](../contributors/documentation_style.md).
17
-
18
- ---
19
-
20
- **Best Practices:** [Modal Dialogs](./modal_dialogs.md) | [Forms and Validation](./forms_and_validation.md) | [Lists and Tables](./lists_and_tables.md) | [HTTP Workflows](./http_workflows.md) | [Orchestration](./orchestration.md)
@@ -1,214 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Custom Commands Design (`rooibos`)
7
-
8
- 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.
9
-
10
- ## Core Abstractions
11
-
12
- Custom commands extend Rooibos with user-defined side effects: WebSockets, gRPC, database polling, background workers. The architecture provides four key components:
13
-
14
- | Component | Purpose |
15
- |-----------|---------|
16
- | `Command::Custom` | Mixin for command identification |
17
- | `Command::Outlet` | Messaging gateway for result delivery |
18
- | `Command::CancellationToken` | Cooperative cancellation mechanism |
19
- | `Command.custom { ... }` | Wrapper giving callables unique identity |
20
-
21
- ---
22
-
23
- ## Guiding Design Principles
24
-
25
- ### 1. Messaging Gateway over Raw Queue
26
-
27
- Commands produce messages. Those messages cross threads. Without abstraction, queue manipulation scatters across the codebase.
28
-
29
- 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.
30
-
31
- | Aspect | Raw Queue | Outlet |
32
- |--------|-----------|--------|
33
- | Ractor safety | Manual | Automatic |
34
- | Error messages | Generic | Contextual |
35
- | API | `queue << Ractor.make_shareable([...])` | `out.put(:tag, data)` |
36
- | Pattern | Implementation detail | **Messaging Gateway** |
37
-
38
- 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.
39
-
40
- ### 2. Cooperative Cancellation over Thread Termination
41
-
42
- Long-running commands block the event loop. WebSocket listeners, database pollers, and streaming connections run indefinitely until stopped. Stopping them abruptly corrupts state.
43
-
44
- `Thread#kill` terminates immediately. Mutexes may deadlock. Resources may leak. Database transactions may abort mid-write.
45
-
46
- The `CancellationToken` signals cancellation requests. Commands check `token.canceled?` periodically and stop at safe points. Cleanup code runs. Resources release. Transactions commit.
47
-
48
- | Aspect | `Thread#kill` | CancellationToken |
49
- |--------|---------------|------------------|
50
- | Cleanup | None (immediate termination) | Command controls cleanup |
51
- | Resource safety | May corrupt state | Clean shutdown |
52
- | Mutexes | May deadlock | Released properly |
53
- | Pattern | Forceful | **Cooperative** |
54
-
55
- **Configurable grace periods** accommodate different cleanup needs:
56
-
57
- - `0.5` seconds — Quick HTTP abort, minimal cleanup
58
- - `2.0` seconds — Default, suitable for most commands
59
- - `5.0` seconds — WebSocket close handshake with remote server
60
- - `Float::INFINITY` — Never force-kill (database transactions)
61
-
62
- ### 3. Command Identity via Object Reference
63
-
64
- 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.
65
-
66
- 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.
67
-
68
- `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)`.
69
-
70
- ### 4. Automatic Error Propagation
71
-
72
- Commands run in threads. Exceptions bubble up silently. The update function never sees them. Backtraces written to STDERR corrupt the TUI display.
73
-
74
- 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.
75
-
76
- This mirrors the sentinel pattern used for `Command::Exit` and `Command::Cancel`.
77
-
78
- **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:
79
-
80
- | Exception Class | Source |
81
- |-----------------|--------|
82
- | `RatatuiRuby::Error::Invariant` | Framework validation (debug mode) |
83
- | `RatatuiRuby::Error::Internal` | Framework bugs |
84
- | `ArgumentError`, `RuntimeError`, etc. | User code |
85
-
86
- The update function can pattern-match on `error_msg.exception.class` if it needs to distinguish sources.
87
-
88
- ### 5. Ractor Readiness
89
-
90
- This design is forward-compatible with Ruby's Ractor-based parallelism.
91
-
92
- **What's already shareable:**
93
-
94
- | Pattern | Shareable? | Why |
95
- |---------|------------|-----|
96
- | Module-constant lambda | ✅ Yes | `self` is the frozen module |
97
- | Closure-free block | ✅ Yes | No captured variables |
98
- | `Data.define` command | ✅ Yes | Immutable by definition |
99
- | Frozen class instance | ✅ Yes | Deeply frozen |
100
-
101
- **Debug mode validation:**
102
-
103
- 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.
104
-
105
- **Why Thread dispatch is Ractor-safe:**
106
-
107
- 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).
108
-
109
- 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.
110
-
111
- ---
112
-
113
- ## Pattern Lineage
114
-
115
- This design implements several established patterns from the software architecture literature.
116
-
117
- ### Design Patterns (Gang of Four)
118
-
119
- | Component | Pattern |
120
- |-----------|---------|
121
- | Custom Mixin | **Command** — Encapsulates a request as an object |
122
- | Outlet | **Facade** — Simplified interface to a complex subsystem |
123
- | Runtime | **Mediator** — Coordinates command dispatch and message routing |
124
- | Update Function | **State** — Behavior changes based on internal state |
125
-
126
- ### Enterprise Integration Patterns (Hohpe & Woolf)
127
-
128
- | Component | Pattern |
129
- |-----------|---------|
130
- | Outlet | **Messaging Gateway** — Wraps message channel access |
131
- | Outlet | **Channel Adapter** — Connects applications to messaging systems |
132
- | Queue | **Point-to-Point Channel** — Single consumer receives each message |
133
-
134
- ### Cancellation Patterns
135
-
136
- | Platform | Mechanism | Relationship |
137
- |----------|-----------|--------------|
138
- | **.NET** | `CancellationToken` | Direct inspiration for API design |
139
- | **Go** | `context.Context` | Cooperative cancellation via `ctx.Done()` |
140
- | **JavaScript** | `AbortController` | Web Fetch API cancellation |
141
- | **Java** | `Thread.interrupt()` | Flag-based cooperative cancellation |
142
- | **Kotlin** | `Job.cancel()` | Coroutine cancellation |
143
-
144
- ---
145
-
146
- ## Prior Art
147
-
148
- ### The Elm Architecture (TEA)
149
-
150
- | Framework | Language | Notes |
151
- |-----------|----------|-------|
152
- | **Elm** | Elm | Original TEA; `Cmd` + `Sub` primitives |
153
- | **BubbleTea** | Go | TUI implementation; "Subscriptions are Loops" philosophy |
154
- | **Iced** | Rust | GUI TEA with Command + Subscription |
155
- | **Miso** | Haskell | Web TEA with Effect + Sub |
156
- | **Bolero** | F# | Web TEA with Cmd + Sub |
157
-
158
- ### Redux Ecosystem
159
-
160
- | Library | Pattern | Mapping |
161
- |---------|---------|---------|
162
- | **Redux Thunk** | Raw dispatch access | Direct queue access (rejected) |
163
- | **Redux Saga** | `put()` effect dispatches actions | **Outlet#put** ← adopted |
164
- | **Redux Observable** | RxJS Observables | RxRuby (rejected for complexity) |
165
- | **redux-loop** | Elm-style Cmd | Recursive commands |
166
-
167
- 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.
168
-
169
- ### Ruby Ecosystem
170
-
171
- | Library | Pattern | Relationship |
172
- |---------|---------|--------------|
173
- | **Observable** (stdlib) | Observer pattern | Similar push semantics, lacks thread-safety |
174
- | **Concurrent Ruby** | Actor, Promises | More complex than needed |
175
- | **Celluloid** | Actor mailboxes | Inspiration for internal terminology |
176
- | **Sidekiq** | Background jobs | Similar "fire command, receive result" model |
177
-
178
- ---
179
-
180
- ## Key Influences Summary
181
-
182
- | Influence | What We Adopted |
183
- |-----------|-----------------|
184
- | **Elm** | MVU architecture, Cmd/Msg pattern |
185
- | **BubbleTea** | "Subscriptions are Loops" philosophy |
186
- | **Redux Saga** | `put()` for message dispatch |
187
- | **.NET CancellationToken** | Cooperative cancellation with grace periods |
188
- | **EIP Messaging Gateway** | Outlet as infrastructure abstraction |
189
- | **Go context.Context** | Cancellation propagation pattern |
190
-
191
- ---
192
-
193
- ## Further Reading
194
-
195
- ### External Resources
196
-
197
- - [Elm Guide](https://guide.elm-lang.org/) — Official Elm documentation covering commands and effects
198
- - [BubbleTea](https://github.com/charmbracelet/bubbletea) — Go TUI framework based on The Elm Architecture
199
- - [Redux Saga](https://redux-saga.js.org/) — Saga pattern and `put()` effect documentation
200
- - [Enterprise Integration Patterns](https://www.enterpriseintegrationpatterns.com/) — Messaging patterns reference by Hohpe & Woolf
201
-
202
- ### Academic References
203
-
204
- 1. **Gamma, Helm, Johnson, Vlissides** (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
205
- - Command, Facade, Mediator, State patterns
206
-
207
- 2. **Fowler, M.** (2002). *Patterns of Enterprise Application Architecture*. Addison-Wesley.
208
- - Gateway pattern
209
-
210
- 3. **Hohpe, G. & Woolf, B.** (2003). *Enterprise Integration Patterns*. Addison-Wesley.
211
- - Messaging Gateway, Message Channel, Point-to-Point Channel, Channel Adapter
212
-
213
- 4. **Joshi, U.** (2023). *Patterns of Distributed Systems*. Addison-Wesley.
214
- - Write-Ahead Log, Thread Pool