ratatui_ruby-tea 0.3.1 → 0.4.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +42 -2
  3. data/CHANGELOG.md +76 -0
  4. data/README.md +8 -5
  5. data/doc/concepts/async_work.md +164 -0
  6. data/doc/concepts/commands.md +528 -0
  7. data/doc/concepts/message_processing.md +51 -0
  8. data/doc/contributors/WIP/decomposition_strategies_analysis.md +258 -0
  9. data/doc/contributors/WIP/implementation_plan.md +405 -0
  10. data/doc/contributors/WIP/init_callable_proposal.md +341 -0
  11. data/doc/contributors/WIP/mvu_tea_implementations_research.md +372 -0
  12. data/doc/contributors/WIP/runtime_refactoring_status.md +47 -0
  13. data/doc/contributors/WIP/task.md +36 -0
  14. data/doc/contributors/WIP/v0.4.0_todo.md +468 -0
  15. data/doc/contributors/design/commands_and_outlets.md +11 -1
  16. data/doc/contributors/priorities.md +22 -24
  17. data/examples/app_fractal_dashboard/app.rb +3 -7
  18. data/examples/app_fractal_dashboard/dashboard/base.rb +15 -16
  19. data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +8 -8
  20. data/examples/app_fractal_dashboard/dashboard/update_manual.rb +11 -11
  21. data/examples/app_fractal_dashboard/dashboard/update_router.rb +4 -4
  22. data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_input.rb +8 -4
  23. data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +82 -0
  24. data/examples/app_fractal_dashboard/{bags → fragments}/custom_shell_output.rb +8 -4
  25. data/examples/app_fractal_dashboard/{bags → fragments}/disk_usage.rb +13 -10
  26. data/examples/app_fractal_dashboard/{bags → fragments}/network_panel.rb +12 -12
  27. data/examples/app_fractal_dashboard/{bags → fragments}/ping.rb +12 -8
  28. data/examples/app_fractal_dashboard/{bags → fragments}/stats_panel.rb +12 -12
  29. data/examples/app_fractal_dashboard/{bags → fragments}/system_info.rb +11 -7
  30. data/examples/app_fractal_dashboard/{bags → fragments}/uptime.rb +11 -7
  31. data/examples/verify_readme_usage/README.md +7 -4
  32. data/examples/verify_readme_usage/app.rb +7 -4
  33. data/lib/ratatui_ruby/tea/command/all.rb +71 -0
  34. data/lib/ratatui_ruby/tea/command/batch.rb +79 -0
  35. data/lib/ratatui_ruby/tea/command/custom.rb +1 -1
  36. data/lib/ratatui_ruby/tea/command/http.rb +194 -0
  37. data/lib/ratatui_ruby/tea/command/lifecycle.rb +136 -0
  38. data/lib/ratatui_ruby/tea/command/outlet.rb +59 -27
  39. data/lib/ratatui_ruby/tea/command/wait.rb +82 -0
  40. data/lib/ratatui_ruby/tea/command.rb +245 -64
  41. data/lib/ratatui_ruby/tea/message/all.rb +47 -0
  42. data/lib/ratatui_ruby/tea/message/http_response.rb +63 -0
  43. data/lib/ratatui_ruby/tea/message/system/batch.rb +63 -0
  44. data/lib/ratatui_ruby/tea/message/system/stream.rb +69 -0
  45. data/lib/ratatui_ruby/tea/message/timer.rb +48 -0
  46. data/lib/ratatui_ruby/tea/message.rb +40 -0
  47. data/lib/ratatui_ruby/tea/router.rb +11 -11
  48. data/lib/ratatui_ruby/tea/runtime.rb +320 -185
  49. data/lib/ratatui_ruby/tea/shortcuts.rb +2 -2
  50. data/lib/ratatui_ruby/tea/test_helper.rb +58 -0
  51. data/lib/ratatui_ruby/tea/version.rb +1 -1
  52. data/lib/ratatui_ruby/tea.rb +44 -10
  53. data/rbs_collection.lock.yaml +1 -17
  54. data/sig/concurrent.rbs +72 -0
  55. data/sig/ratatui_ruby/tea/command.rbs +141 -37
  56. data/sig/ratatui_ruby/tea/message.rbs +123 -0
  57. data/sig/ratatui_ruby/tea/router.rbs +1 -1
  58. data/sig/ratatui_ruby/tea/runtime.rbs +39 -6
  59. data/sig/ratatui_ruby/tea/test_helper.rbs +12 -0
  60. data/sig/ratatui_ruby/tea.rbs +24 -4
  61. metadata +63 -11
  62. data/examples/app_fractal_dashboard/bags/custom_shell_modal.rb +0 -73
  63. data/lib/ratatui_ruby/tea/command/cancellation_token.rb +0 -135
@@ -0,0 +1,47 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Runtime Refactoring - Final Status
8
+
9
+ ## ✅ Completed
10
+
11
+ 1. **CHANGE LOG documented** - Breaking changes added:
12
+ - Runtime API signature (positional fragment, fps param, removed argv/env, renamed init to command)
13
+ - Model validation timing (now validates immediately after Init)
14
+
15
+ 2. **RBS Signatures** - Fully updated for new signature
16
+
17
+ 3. **Tea.run signatures** - All test files updated from `fragment:` to positional
18
+
19
+ 4. **Model Ractor-shareability** - All tests fixed with `Ractor.make_shareable(..., copy: true)`
20
+
21
+ ## ⚠️ Remaining Issues (3 errors from agent_rake)
22
+
23
+ ### 1. test_runtime.rb:237 - Test body deleted by accident
24
+ **File**: `test/test_runtime.rb`
25
+ **Issue**: Deleted lines 237-245 which contained the test body for `test_init_triggers_update_before_first_event`
26
+
27
+ **Fix needed**: Restore test body. The test should validate that `command:` parameter gets dispatched at startup.
28
+
29
+ ### 2. test_fragment_first_api.rb:48 - Missing with_argv helper
30
+ **File**: `test/test_fragment_first_api.rb`
31
+ **Issue**: `NoMethodError: undefined method 'with_argv'`
32
+
33
+ **Fix needed**: Add `with_argv` helper to `/Users/kerrick/Developer/ratatui_ruby/test/test_helper.rb` (already documented earlier in conversation)
34
+
35
+ ### 3. test_runtime_timer.rb:155 - Cancel assertion
36
+ **File**: `test/test_runtime_timer.rb`
37
+ **Issue**: Assertion compares Cancel object vs Wait command directly
38
+
39
+ **Current**: `assert_same original_cmd, cancel_msg`
40
+ **Should be**: `assert_same original_cmd, cancel_msg.handle`
41
+
42
+ The sed command ran but didn't match because cancel_msg is on its own line.
43
+
44
+ ## Summary
45
+
46
+ **272 runs, 779 assertions, 2 failures, 1 errors from 95+ errors**
47
+ Major progress! Just need to fix these 3 small issues.
@@ -0,0 +1,36 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Task: Update Tests and Docs for Runtime Refactoring
8
+
9
+ ## Signature Changes
10
+ - [x] Update RBS signatures for new Runtime.run signature
11
+ - [x] Add with_argv helper to ratatui_ruby test_helper
12
+ - [x] Update Tea.run wrapper signature
13
+
14
+ ## Test Updates
15
+ - [x] Update test_fragment_first_api.rb - change fragment: to positional
16
+ - [x] Fix argv/env tests to use with_argv/with_env helpers
17
+ - [x] Fix all Command.custom tests to use Ractor-shareable callables
18
+ - [ ] Fix test_init_triggers_update_before_first_event - still failing
19
+ - [ ] Fix test_fragment_first_api_passes_argv_and_env_to_init - ENV hash comparison issue
20
+ - [ ] Update test_snapshots.rb - change all Tea.run calls (15+ occurrences)
21
+ - [ ] Update test_fractal_dashboard.rb - change Tea.run calls
22
+ - [ ] Update all other test files using Tea.run
23
+ - [ ] Add test for fps: parameter
24
+
25
+ ## Example Updates
26
+ - [ ] Update verify_readme_usage/app.rb
27
+ - [ ] Update app_fractal_dashboard Init callables (remove argv/env params)
28
+
29
+ ## Current Status
30
+ **3 failures, 1 error**: Making final fixes
31
+ - test_init_triggers_update_before_first_event - still needs investigation
32
+ - test_fragment_first_api_passes_argv_and_env_to_init - ENV not being passed correctly
33
+ - test_cancelled_wait_acknowledges_cancellation - intermittent
34
+ - test_custom_accepts_block - fixing Command.custom wrapper pattern
35
+
36
+ Restructuring Command.custom tests to demonstrate automatic shareability handling.
@@ -0,0 +1,468 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # v0.4.0 Remaining TODOs
7
+
8
+ Outstanding items before releasing v0.4.0. Each section is self-contained with enough detail to implement independently.
9
+
10
+ ---
11
+
12
+ ## 1. ~~Rename `tag` → `envelope` in Command.system and Command.all~~ ✅ DONE
13
+
14
+ ---
15
+
16
+ ## 2. ~~Command.all Must Emit Message::All Instead of Raw Arrays~~ ✅ DONE
17
+
18
+ ---
19
+
20
+ # v0.4.1 Remaining TODOs
21
+
22
+ Outstanding items before releasing v0.4.1. Each section is self-contained with enough detail to implement independently.
23
+
24
+ ---
25
+
26
+ ## 3. Add Default `deconstruct_keys` to Command::Custom
27
+
28
+ ### Problem
29
+ Design doc (lines 786-797) specifies that custom commands should have a default `deconstruct_keys` for hash-based pattern matching. Currently missing from `lib/ratatui_ruby/tea/command/custom.rb`.
30
+
31
+ ### Implementation
32
+
33
+ Add to `lib/ratatui_ruby/tea/command/custom.rb` in the `Custom` module:
34
+
35
+ ```ruby
36
+ # Methods inherited from common ancestor classes that are infrastructure,
37
+ # not domain-specific query methods. Computed once from bare prototypes.
38
+ INFRASTRUCTURE_METHODS = begin
39
+ bare_data = Data.define(:_)
40
+ bare_struct = Struct.new(:_)
41
+
42
+ methods = Object.public_instance_methods +
43
+ bare_data.public_instance_methods +
44
+ bare_struct.public_instance_methods
45
+
46
+ # Include OpenStruct only if already loaded (don't require it ourselves)
47
+ if defined?(OpenStruct)
48
+ methods += OpenStruct.new(_: nil).public_instance_methods
49
+ end
50
+
51
+ methods.uniq.freeze
52
+ end
53
+ private_constant :INFRASTRUCTURE_METHODS
54
+
55
+ # Deconstructs for pattern matching.
56
+ #
57
+ # Introspects the object and builds a hash from all public query methods
58
+ # (following CQS: methods with no side effects that return values).
59
+ # Excludes infrastructure methods from Object, Data, and Struct to focus
60
+ # on domain-specific fields.
61
+ #
62
+ # Always includes :type as a snake_case symbol of the class name.
63
+ # Data.define members are automatically included since they generate
64
+ # public accessor methods.
65
+ #
66
+ # This is a naive but practical default. Override for:
67
+ # - Hot paths (introspects methods on every call)
68
+ # - Ghost methods via method_missing/respond_to_missing?
69
+ # - Methods with optional arguments (only zero-arity detected)
70
+ #
71
+ # === Example
72
+ #
73
+ # # For a Data.define(:envelope, :status, :body) command:
74
+ # case msg
75
+ # in { type: :http_response, envelope: :users, status: 200 }
76
+ # # handle success
77
+ # end
78
+ def deconstruct_keys(keys)
79
+ # Snake-case class name as type discriminator
80
+ type_name = self.class.name.split("::").last
81
+ .gsub(/([a-z])([A-Z])/, '\1_\2')
82
+ .downcase
83
+ .to_sym
84
+
85
+ # Find public query methods: zero arity, not infrastructure, not commands/setters
86
+ query_methods = (public_methods - INFRASTRUCTURE_METHODS)
87
+ .select { |m| method(m).arity.zero? }
88
+ .reject { |m| m.to_s.end_with?("=", "!") }
89
+
90
+ # Build hash: filter by requested keys if provided
91
+ result = { type: type_name }
92
+ query_methods.each do |m|
93
+ result[m] = public_send(m) if keys.nil? || keys.include?(m)
94
+ end
95
+ result
96
+ end
97
+ ```
98
+
99
+ This provides a smart default that:
100
+ - Computes infrastructure methods once from bare `Data.define`, `Struct`, and `OpenStruct` prototypes
101
+ - Catches `members`, `with`, `to_h`, `deconstruct`, `deconstruct_keys`, etc. automatically
102
+ - No hardcoded method lists — if Ruby adds new methods to these classes, they're excluded
103
+ - Includes all Data.define members automatically (they generate accessor methods)
104
+ - Excludes commands (methods ending in `!`) and setters (ending in `=`)
105
+ - Respects the `keys` argument for performance (only calls methods for requested keys)
106
+
107
+ ### Limitations
108
+
109
+ This is a **naive but practical** default:
110
+
111
+ 1. **Performance**: Introspects methods on every call. For hot paths, app devs should override with a static implementation.
112
+ 2. **Metaprogramming**: Ghost methods via `method_missing`/`respond_to_missing?` don't appear in `public_methods`. RBS introspection could help but is overkill for a default.
113
+ 3. **Custom accessors**: Only zero-arity methods are included. Methods with optional args won't be detected.
114
+
115
+ **App devs who need more control should override `deconstruct_keys` directly.**
116
+
117
+ ---
118
+
119
+ ## 4. Update Doc Comments to Show Hash Pattern Matching
120
+
121
+ ### Problem
122
+ Doc comments in `lib/ratatui_ruby/tea/command.rb` show array-based pattern matching (old style) instead of hash-based (new style per Appendix A).
123
+
124
+ ### Files to Update
125
+
126
+ **lib/ratatui_ruby/tea/command.rb** — Lines 326-350 (Command.system examples):
127
+
128
+ ```ruby
129
+ # Current (wrong):
130
+ # in [:got_files, {stdout:, status: 0}]
131
+ # [model.with(files: stdout.lines), nil]
132
+ # in [:log, :stdout, line]
133
+ # [model.with(lines: [*model.lines, line]), nil]
134
+
135
+ # Correct:
136
+ # in { type: :system, envelope: :got_files, stdout:, status: 0 }
137
+ # [model.with(files: stdout.lines), nil]
138
+ # in { type: :system, envelope: :log, stream: :stdout, content: line }
139
+ # [model.with(lines: [*model.lines, line]), nil]
140
+ ```
141
+
142
+ Also update:
143
+ - `lib/ratatui_ruby/tea.rb` lines 106, 112: Change `::UPDATE` → `::Update` in examples
144
+ - `lib/ratatui_ruby/tea/router.rb` lines 19-20, 37: Change `INITIAL`, `UPDATE`, `VIEW` → `Init`, `Update`, `View` in docstrings
145
+
146
+ ---
147
+
148
+ ## 5. Update widget_command_system Example
149
+
150
+ ### Problem
151
+ `examples/widget_command_system/app.rb` uses outdated conventions:
152
+ - Screaming-case constants: `INITIAL`, `VIEW`, `UPDATE`
153
+ - Old `Tea.run` API: `Tea.run(model:, view:, update:)`
154
+
155
+ ### Implementation
156
+
157
+ 1. Rename constants (lines 28, 34, 100):
158
+ - `INITIAL` → `Init` (make it a lambda: `Init = -> { Model.new(...) }`)
159
+ - `VIEW` → `View`
160
+ - `UPDATE` → `Update`
161
+
162
+ 2. Change `Tea.run` call (line 128):
163
+ ```ruby
164
+ # Current:
165
+ RatatuiRuby::Tea.run(model: INITIAL, view: VIEW, update: UPDATE)
166
+
167
+ # Correct (Fragment-first API):
168
+ RatatuiRuby::Tea.run(WidgetCommandSystem)
169
+ ```
170
+
171
+ 3. Update pattern matching in `Update` lambda (lines 103-106) to use hash-based format:
172
+ ```ruby
173
+ # Current:
174
+ in [:got_output, { stdout:, status: 0 }]
175
+
176
+ # Correct:
177
+ in { type: :system, envelope: :got_output, stdout:, status: 0 }
178
+ ```
179
+
180
+ 4. Fix variable shadowing bugs on lines 117, 121: `command` vs `cmd`
181
+
182
+ ---
183
+
184
+ ## 6. Tea.delegate Array Wrapping (Lower Priority)
185
+
186
+ ### Problem
187
+ Line 853 of design doc states: "Tea's delegate helper should probably not wrap in arrays."
188
+
189
+ Currently `Tea.delegate` (lib/ratatui_ruby/tea.rb:116) passes `rest = message[1..]` as an array to child update. The design suggests passing unwrapped messages.
190
+
191
+ ### Consideration
192
+ This may be intentional for consistency. Review whether child updates expect array or unwrapped messages before changing. If changing:
193
+
194
+ ```ruby
195
+ # Current:
196
+ rest = message[1..]
197
+ new_child, command = child_update.call(rest, child_model)
198
+
199
+ # Potentially:
200
+ rest = message[1] # Single value, not array
201
+ new_child, command = child_update.call(rest, child_model)
202
+ ```
203
+
204
+ **Note:** This change may break existing code. Audit all `Tea.delegate` callers first.
205
+
206
+ ---
207
+
208
+ ## 7. Missing Tests (Technical Debt)
209
+
210
+ ### Phase 3: Mixed Command Types Test
211
+ Add test demonstrating `Command.batch` or `Command.all` with heterogeneous child commands:
212
+
213
+ ```ruby
214
+ def test_batch_with_mixed_command_types
215
+ cmd = Command.batch(
216
+ Command.http(:get, "/api/users", :users),
217
+ Command.wait(0.1, :timer),
218
+ Command.system("echo hello", :shell)
219
+ )
220
+ # Verify all three types execute and return properly
221
+ end
222
+ ```
223
+
224
+ ### Phase 5: Sync→Parallel→Sync Flow Test
225
+ Add test demonstrating `Outlet#source` orchestration:
226
+
227
+ ```ruby
228
+ def test_source_sync_parallel_sync_flow
229
+ # Custom command that:
230
+ # 1. source(single_command) - sync
231
+ # 2. source(Command.all([...]) - parallel
232
+ # 3. source(single_command) - sync again
233
+ # Verify correct sequencing and result handling
234
+ end
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 8. Update test_widget_command_system.rb
240
+
241
+ ### Problem
242
+ Uses `::UPDATE` and `::INITIAL` screaming-case (lines 14, 17, 28, 31).
243
+
244
+ ### Implementation
245
+ After updating the example (TODO #5), update the tests to use `::Update` and `::Init`.
246
+
247
+ ---
248
+
249
+ ## Verification
250
+
251
+ After implementing all items, run:
252
+
253
+ ```bash
254
+ bundle exec rake test
255
+ ```
256
+
257
+ All tests should pass. The following patterns should work in update functions:
258
+
259
+ ```ruby
260
+ case msg
261
+ in { type: :http, envelope: :users, status: 200, body: }
262
+ # HTTP success
263
+ in { type: :system, envelope: :files, stdout:, status: 0 }
264
+ # System batch success
265
+ in { type: :timer, envelope: :dismiss, elapsed: }
266
+ # Timer completed
267
+ in { type: :all, envelope: :dashboard, results: }
268
+ # Aggregated parallel results
269
+ end
270
+ ```
271
+
272
+ ---
273
+
274
+ ## 9. Add `out.source_nonblock` for Parallel Streaming Commands
275
+
276
+ ### Context
277
+
278
+ Custom commands can orchestrate work using `out.source(cmd, token)` for sequential steps and `Command.all` for parallel-but-wait-for-all patterns. However, there's no way to run multiple streaming commands in parallel where each emits messages live as they arrive.
279
+
280
+ ### Problem
281
+
282
+ Consider a dashboard that:
283
+ 1. Authenticates
284
+ 2. Fetches initial data
285
+ 3. Opens two Server-Sent Events (SSE) streams for live updates
286
+ 4. Post-processes each stream's chunks differently
287
+ 5. Emits processed chunks to the update function as they arrive
288
+
289
+ With current primitives:
290
+ - `out.source` blocks — can only read one stream at a time
291
+ - `Command.all` waits for ALL to complete — no live streaming
292
+ - `Command.batch` only works from `update`, not inside custom commands
293
+ - Raw `Thread.new` causes silent hangs if threads crash (documented in `doc/concepts/async_work.md`)
294
+
295
+ Developers are stuck.
296
+
297
+ ### Solution
298
+
299
+ Add `out.source_nonblock` to spawn async child commands that stream messages live:
300
+
301
+ ```ruby
302
+ # API:
303
+ handle = out.source_nonblock(command, token) # messages pass directly to runtime
304
+ handle = out.source_nonblock(command, token, processor) # messages go through processor first
305
+ out.last(handle1, handle2, ...) # block until all handles complete
306
+ ```
307
+
308
+ ### Usage Example
309
+
310
+ ```ruby
311
+ class LoadDashboard < Data.define(:user_id, :tag)
312
+ include RatatuiRuby::Tea::Command::Custom
313
+
314
+ def call(out, token)
315
+ # Sequential: authenticate first
316
+ auth = out.source(Authenticate.new(user_id:, tag: :_), token)
317
+ return if auth.nil? || token.canceled?
318
+
319
+ # Sequential: fetch initial data
320
+ dashboard = out.source(
321
+ Command.all(:_, [FetchProfile.new(...), FetchNotifications.new(...)]),
322
+ token
323
+ )
324
+ return if dashboard.nil? || token.canceled?
325
+ out.put(tag, dashboard.results)
326
+
327
+ # Parallel streaming: two SSE connections
328
+ # 5a: Pass messages directly to update function (no processing)
329
+ h1 = out.source_nonblock(StreamNotifications.new(auth[:token]), token)
330
+
331
+ # 5b: Post-process messages before they reach update function
332
+ h2 = out.source_nonblock(
333
+ StreamDashboardDeltas.new(dashboard.results[:profile][:id], auth[:token]),
334
+ token,
335
+ DeltaPostProcessor.new(user_id) # Ractor-shareable callable
336
+ )
337
+
338
+ # 5c: Block until both streams complete (or are cancelled)
339
+ out.last(h1, h2)
340
+ end
341
+ end
342
+
343
+ # The processor is a Ractor-shareable callable:
344
+ DeltaPostProcessor = Data.define(:user_id) do
345
+ def call(chunk, out)
346
+ processed = transform(chunk)
347
+ out.put(:delta, { user_id:, data: processed })
348
+ end
349
+
350
+ def transform(chunk)
351
+ # post-processing logic
352
+ end
353
+ end
354
+ ```
355
+
356
+ ### Implementation
357
+
358
+ #### 1. Add `AsyncHandle` class
359
+
360
+ In `lib/ratatui_ruby/tea/command/outlet.rb`:
361
+
362
+ ```ruby
363
+ AsyncHandle = Data.define(:future, :channel) do
364
+ def done?
365
+ future.resolved?
366
+ end
367
+ end
368
+ ```
369
+
370
+ #### 2. Add `source_nonblock` to `Outlet`
371
+
372
+ ```ruby
373
+ def source_nonblock(command, token, processor = nil)
374
+ channel = Concurrent::Promises::Channel.new
375
+
376
+ # Create outlet that writes to channel instead of parent queue
377
+ child_outlet = if processor
378
+ ProcessorOutlet.new(channel, processor, @message_queue)
379
+ else
380
+ ChannelOutlet.new(channel, @message_queue)
381
+ end
382
+
383
+ # Spawn child command
384
+ future = Concurrent::Promises.future do
385
+ command.call(child_outlet, token)
386
+ channel.push(:done)
387
+ rescue => e
388
+ @message_queue.push Command::Error.new(command:, exception: e)
389
+ channel.push(:done)
390
+ end
391
+
392
+ (@pending_async ||= []) << AsyncHandle.new(future:, channel:)
393
+ AsyncHandle.new(future:, channel:)
394
+ end
395
+ ```
396
+
397
+ #### 3. Add outlet variants
398
+
399
+ ```ruby
400
+ # Passes messages directly to runtime queue
401
+ ChannelOutlet = Data.define(:channel, :message_queue) do
402
+ def put(*args)
403
+ message = (args.size == 1) ? args.first : args.freeze
404
+ message_queue.push(message)
405
+ end
406
+ end
407
+
408
+ # Invokes processor, which calls out.put on a Ractor-safe outlet
409
+ ProcessorOutlet = Data.define(:channel, :processor, :message_queue) do
410
+ def put(*args)
411
+ message = (args.size == 1) ? args.first : args.freeze
412
+ # Processor receives message and a simple outlet for its output
413
+ output_outlet = ChannelOutlet.new(channel, message_queue)
414
+ processor.call(message, output_outlet)
415
+ rescue => e
416
+ message_queue.push Command::Error.new(command: processor, exception: e)
417
+ end
418
+ end
419
+ ```
420
+
421
+ #### 4. Add `last` method
422
+
423
+ ```ruby
424
+ def last(*handles)
425
+ handles = @pending_async if handles.empty?
426
+ handles.each do |handle|
427
+ handle.future.wait
428
+ end
429
+ @pending_async&.clear
430
+ end
431
+ ```
432
+
433
+ ### Ractor Safety
434
+
435
+ - **Child command**: runs in Ractor/future, must be shareable (existing requirement)
436
+ - **Processor**: must be Ractor-shareable (e.g., `Data.define` with shareable members)
437
+ - **Messages**: must be shareable (existing requirement)
438
+ - **The block IS NOT USED**: callbacks are shareable callables, not closures
439
+
440
+ The processor runs in the child's context (Ractor/future). It receives a Ractor-safe outlet that pushes to a channel. The runtime reads the channel and delivers messages — no closure crosses the Ractor boundary.
441
+
442
+ ### Tests
443
+
444
+ Add to `test/test_outlet.rb`:
445
+
446
+ ```ruby
447
+ def test_source_nonblock_streams_messages_live
448
+ # Custom command that:
449
+ # 1. source(single_command) - sync
450
+ # 2. source_nonblock(streaming_cmd1), source_nonblock(streaming_cmd2)
451
+ # 3. last(h1, h2)
452
+ # Verify messages arrive interleaved as streams produce them
453
+ end
454
+
455
+ def test_source_nonblock_with_processor_transforms_messages
456
+ # Verify processor.call is invoked for each message
457
+ # Verify transformed output reaches update function
458
+ end
459
+
460
+ def test_source_nonblock_crash_produces_command_error
461
+ # Verify child crash doesn't hang — becomes Command::Error
462
+ end
463
+
464
+ def test_source_nonblock_processor_crash_produces_command_error
465
+ # Verify processor crash doesn't hang — becomes Command::Error
466
+ end
467
+ ```
468
+
@@ -75,6 +75,16 @@ The runtime catches unhandled exceptions and pushes `Command::Error` to the queu
75
75
 
76
76
  This mirrors the sentinel pattern used for `Command::Exit` and `Command::Cancel`.
77
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
+
78
88
  ### 5. Ractor Readiness
79
89
 
80
90
  This design is forward-compatible with Ruby's Ractor-based parallelism.
@@ -150,7 +160,7 @@ This design implements several established patterns from the software architectu
150
160
  | Library | Pattern | Mapping |
151
161
  |---------|---------|---------|
152
162
  | **Redux Thunk** | Raw dispatch access | Direct queue access (rejected) |
153
- | **Redux Saga** | `put()` effect dispatches actions | **Outlet.put** ← adopted |
163
+ | **Redux Saga** | `put()` effect dispatches actions | **Outlet#put** ← adopted |
154
164
  | **Redux Observable** | RxJS Observables | RxRuby (rejected for complexity) |
155
165
  | **redux-loop** | Elm-style Cmd | Recursive commands |
156
166
 
@@ -5,36 +5,34 @@
5
5
 
6
6
  # Feature Priorities
7
7
 
8
- This document outlines the critical next steps for `ratatui_ruby-tea`. Each item explains the context, the problem, and the solution, following our [Documentation Style](../ratatui_ruby/doc/contributors/documentation_style.md).
8
+ This document outlines the remaining work before `ratatui_ruby-tea` reaches v1.0.0.
9
9
 
10
- ## 1. Composition (Cmd.map)
10
+ ## 1. Built-In Commands
11
11
 
12
- **Context:** Real applications grow. You start with a file picker. Then a modal. Then a sidebar. Each component has its own model and update function.
12
+ Six primitives remain unimplemented. See [Command Composition Design](./design/command_composition.md) for full specifications.
13
13
 
14
- **Problem:** The Tea architecture naturally isolates components. A parent model holds a child model. But when the child update function returns a message (like `:selected`), the parent `update` function only understands its own messages. It cannot "hear" the child. The architecture breaks at the boundary of the first file.
14
+ | Command / Method | Purpose | Complexity |
15
+ |------------------|---------|------------|
16
+ | `Command.wait(seconds, tag)` | One-shot timer | Low |
17
+ | `Command.tick(interval, tag)` | Recurring timer (subscriptions) | Low |
18
+ | `Command.batch([...])` | Parallel execution (fire-and-forget) | Medium |
19
+ | `Command.all([...])` | Parallel execution (aggregating) | Medium |
20
+ | `Command.http(method, url, tag)` | HTTP requests via stdlib | Medium |
21
+ | `Outlet#source(command, token)` | Command composition | Low |
15
22
 
16
- **Solution:** Implement `Cmd.map(cmd) { |child_msg| ... }`. This wraps the child's effect. When the effect completes, the runtime passes the result through the block, transforming the child's message into a parent's message. This restores the flow of data up the tree.
23
+ Implementation order follows increasing complexity. Each can be tested independently.
17
24
 
18
- ## 2. Parallelism (Cmd.batch)
25
+ > [!NOTE]
26
+ > `Command.sequence` was considered but rejected. See [Rejected Alternatives](./design/command_composition.md#rejected-alternatives) for rationale.
19
27
 
20
- **Context:** Applications often need to do two things at once. You initialize the app. You need to load the config *and* fetch the latest data *and* start the tick timer.
28
+ ## 2. Documentation
21
29
 
22
- **Problem:** The `update` function returns a single tuple `[Model, Cmd]`. It cannot return `[Model, Cmd1, Cmd2]`. Without a way to group them, you are forced to sequence independent operations, making the UI feel slow and linear.
30
+ The README warns: "Because this gem is in pre-release, it lacks documentation."
23
31
 
24
- **Solution:** Implement `Cmd.batch([cmd1, cmd2, ...])`. This command takes an array of commands and submits them all to the runtime. The runtime executes them in parallel (where possible) or concurrently.
32
+ Before v1.0.0:
25
33
 
26
- ## 3. Serial Execution (Cmd.sequence)
27
-
28
- **Context:** Some effects depend on others. You cannot read a file until you have downloaded it. You cannot query the database until you have opened the connection.
29
-
30
- **Problem:** The Tea architecture relies on async messages. You send a command, and *eventually* you get a message. To chain actions, you must handle the first success message in `update`, then return the second command. This smears a single logical transaction across multiple independent `case` clauses, creating "callback hell" but in the shape of a state machine.
31
-
32
- **Solution:** Implement `Cmd.sequence([cmd1, cmd2, ...])`. This command executes the first command. If successful, it runs the next. If any fail, it stops. Note: This assumes commands have a standard "success/failure" result shape, or simply runs them blindly. (Design decision required: does `sequence` wait for the message, or just the execution?)
33
-
34
- ## 4. Time (Cmd.tick)
35
-
36
- **Context:** Animations and real-time updates. A spinner rotating. A clock ticking. Evaluation metrics updating live.
37
-
38
- **Problem:** The runtime blocks on input. If the user doesn't type or click, the screen stays frozen. You cannot implement a simple "Loading..." spinner because the frame never updates.
39
-
40
- **Solution:** Implement `Cmd.tick(interval, tag)`. This command sleeps for the interval and then sends a message. The `update` function handles the message and returns the *same* tick command again. This creates a recursive loop, driving the frame rate independent of user input.
34
+ - [ ] Document all public `Command.*` factories with RDoc examples
35
+ - [ ] Write Quickstart guide
36
+ - [ ] Write Fractal Architecture guide
37
+ - [ ] Write Custom Commands guide (including `out.source` composition)
38
+ - [ ] Ensure all examples are copy-pasteable and tested
@@ -19,7 +19,7 @@ require "ratatui_ruby/tea"
19
19
  # ruby app.rb helpers # Tea.route and Tea.delegate helpers
20
20
  # ruby app.rb router # Tea::Router DSL
21
21
  #
22
- # All three share the same bags, Model, INITIAL, and VIEW. Only the UPDATE
22
+ # All three share the same fragments, Model, INITIAL, and VIEW. Only the UPDATE
23
23
  # implementation differs. Compare the three update_*.rb files to see the
24
24
  # progression from verbose to declarative.
25
25
  #
@@ -31,7 +31,7 @@ require "ratatui_ruby/tea"
31
31
  # ├── update_manual.rb
32
32
  # ├── update_helpers.rb
33
33
  # └── update_router.rb
34
- # bags/
34
+ # fragments/
35
35
  # ├── system_info.rb
36
36
  # ├── disk_usage.rb
37
37
  # ├── ping.rb
@@ -60,8 +60,4 @@ dashboard = case mode
60
60
  end
61
61
 
62
62
  puts "Running with #{mode} UPDATE..."
63
- RatatuiRuby::Tea.run(
64
- model: dashboard::INITIAL,
65
- view: dashboard::VIEW,
66
- update: dashboard::UPDATE
67
- )
63
+ RatatuiRuby::Tea.run(fragment: dashboard)