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.
- checksums.yaml +4 -4
- data/LICENSES/BSD-2-Clause.txt +9 -0
- data/REUSE.toml +5 -0
- data/exe/.gitkeep +0 -0
- data/lib/rooibos/cli/commands/new.rb +24 -0
- data/lib/rooibos/command/batch.rb +10 -0
- data/lib/rooibos/command/bubble.rb +34 -0
- data/lib/rooibos/command/custom.rb +3 -2
- data/lib/rooibos/command/deliver.rb +50 -0
- data/lib/rooibos/command/http.rb +1 -1
- data/lib/rooibos/command/lifecycle.rb +3 -1
- data/lib/rooibos/command/outlet.rb +19 -9
- data/lib/rooibos/command.rb +107 -3
- data/lib/rooibos/configuration.rb +29 -0
- data/lib/rooibos/message/bubbled.rb +29 -0
- data/lib/rooibos/message.rb +24 -6
- data/lib/rooibos/router/action.rb +36 -0
- data/lib/rooibos/router/flow/dispatch.rb +39 -0
- data/lib/rooibos/router/flow/inward.rb +41 -0
- data/lib/rooibos/router/flow/outward.rb +44 -0
- data/lib/rooibos/router/guard.rb +56 -0
- data/lib/rooibos/router/predicate.rb +65 -0
- data/lib/rooibos/router/registry/actions.rb +41 -0
- data/lib/rooibos/router/registry/forwards.rb +58 -0
- data/lib/rooibos/router/registry/observes.rb +57 -0
- data/lib/rooibos/router/registry/otherwises.rb +29 -0
- data/lib/rooibos/router/registry/receives.rb +57 -0
- data/lib/rooibos/router/registry/routes.rb +59 -0
- data/lib/rooibos/router/registry.rb +26 -0
- data/lib/rooibos/router/route.rb +42 -0
- data/lib/rooibos/router/router_update.rb +53 -0
- data/lib/rooibos/router/rule/forward.rb +39 -0
- data/lib/rooibos/router/rule/observe.rb +22 -0
- data/lib/rooibos/router/rule/otherwise.rb +26 -0
- data/lib/rooibos/router/rule/receive.rb +22 -0
- data/lib/rooibos/router/rule.rb +40 -0
- data/lib/rooibos/router.rb +424 -438
- data/lib/rooibos/runtime.rb +37 -52
- data/lib/rooibos/test_helper.rb +22 -0
- data/lib/rooibos/transition.rb +92 -0
- data/lib/rooibos/version.rb +1 -1
- data/lib/rooibos.rb +2 -57
- data/sig/rooibos/cli.rbs +1 -0
- data/sig/rooibos/command.rbs +44 -0
- data/sig/rooibos/configuration.rbs +20 -0
- data/sig/rooibos/message.rbs +12 -0
- data/sig/rooibos/router/action.rbs +33 -0
- data/sig/rooibos/router/actions.rbs +27 -0
- data/sig/rooibos/router/flow/dispatch.rbs +29 -0
- data/sig/rooibos/router/flow/inward.rbs +37 -0
- data/sig/rooibos/router/flow/outward.rbs +36 -0
- data/sig/rooibos/router/forward.rbs +35 -0
- data/sig/rooibos/router/forwards.rbs +34 -0
- data/sig/rooibos/router/guard.rbs +21 -0
- data/sig/rooibos/router/observe.rbs +20 -0
- data/sig/rooibos/router/observes.rbs +38 -0
- data/sig/rooibos/router/otherwise.rbs +22 -0
- data/sig/rooibos/router/otherwises.rbs +20 -0
- data/sig/rooibos/router/predicate.rbs +51 -0
- data/sig/rooibos/router/receive.rbs +20 -0
- data/sig/rooibos/router/receives.rbs +38 -0
- data/sig/rooibos/router/registry.rbs +24 -0
- data/sig/rooibos/router/route.rbs +46 -0
- data/sig/rooibos/router/router_update.rbs +33 -0
- data/sig/rooibos/router/routes.rbs +41 -0
- data/sig/rooibos/router/rule.rbs +36 -0
- data/sig/rooibos/router.rbs +216 -161
- data/sig/rooibos/runtime.rbs +0 -1
- data/sig/rooibos/test_helper.rbs +6 -0
- data/sig/rooibos/transition.rbs +33 -0
- data/sig/rooibos.rbs +0 -10
- metadata +144 -198
- data/.builds/ruby-3.2.yml +0 -55
- data/.builds/ruby-3.3.yml +0 -55
- data/.builds/ruby-3.4.yml +0 -55
- data/.builds/ruby-4.0.0.yml +0 -55
- data/.pre-commit-config.yaml +0 -16
- data/.rubocop.yml +0 -8
- data/AGENTS.md +0 -108
- data/CHANGELOG.md +0 -308
- data/README.md +0 -183
- data/README.rdoc +0 -374
- data/Rakefile +0 -16
- data/Steepfile +0 -13
- data/doc/best_practices/forms_and_validation.md +0 -20
- data/doc/best_practices/http_workflows.md +0 -20
- data/doc/best_practices/index.md +0 -26
- data/doc/best_practices/lists_and_tables.md +0 -20
- data/doc/best_practices/modal_dialogs.md +0 -20
- data/doc/best_practices/no_stateful_widgets.md +0 -184
- data/doc/best_practices/orchestration.md +0 -20
- data/doc/best_practices/streaming_data.md +0 -20
- data/doc/contributors/design/commands_and_outlets.md +0 -214
- data/doc/contributors/design/mvu_tea_implementations_research.md +0 -373
- data/doc/contributors/documentation_plan.md +0 -616
- data/doc/contributors/documentation_stub_audit.md +0 -112
- data/doc/contributors/documentation_style.md +0 -275
- data/doc/contributors/e2e_pty.md +0 -168
- data/doc/contributors/maybe_stateful_router.md +0 -56
- data/doc/contributors/specs/earliest_tutorial_steps_per_story.md +0 -70
- data/doc/contributors/specs/file_browser.md +0 -789
- data/doc/contributors/specs/file_browser_stories.md +0 -784
- data/doc/contributors/specs/tutorials_to_stories.rb +0 -167
- data/doc/contributors/todo/scrollbar.md +0 -118
- data/doc/contributors/tutorial_old/01_project_setup.md +0 -20
- data/doc/contributors/tutorial_old/02_hello_world.md +0 -24
- data/doc/contributors/tutorial_old/03_adding_state.md +0 -26
- data/doc/contributors/tutorial_old/06_organizing_your_code.md +0 -20
- data/doc/contributors/tutorial_old/07_your_first_command.md +0 -21
- data/doc/contributors/tutorial_old/08_the_preview_pane.md +0 -20
- data/doc/contributors/tutorial_old/09_loading_states.md +0 -20
- data/doc/contributors/tutorial_old/10_testing_your_app.md +0 -20
- data/doc/contributors/tutorial_old/11_polish_and_refine.md +0 -20
- data/doc/contributors/tutorial_old/12_going_further.md +0 -20
- data/doc/contributors/tutorial_old/index.md +0 -20
- data/doc/custom.css +0 -22
- data/doc/essentials/commands.md +0 -20
- data/doc/essentials/index.md +0 -31
- data/doc/essentials/messages.md +0 -21
- data/doc/essentials/models.md +0 -21
- data/doc/essentials/shortcuts.md +0 -19
- data/doc/essentials/the_elm_architecture.md +0 -24
- data/doc/essentials/the_runtime.md +0 -21
- data/doc/essentials/update_functions.md +0 -20
- data/doc/essentials/views.md +0 -22
- data/doc/getting_started/for_go_developers.md +0 -16
- data/doc/getting_started/for_python_developers.md +0 -16
- data/doc/getting_started/for_rails_developers.md +0 -17
- data/doc/getting_started/for_ratatui_ruby_developers.md +0 -17
- data/doc/getting_started/for_react_developers.md +0 -17
- data/doc/getting_started/index.md +0 -52
- data/doc/getting_started/install.md +0 -20
- data/doc/getting_started/quickstart.md +0 -20
- data/doc/getting_started/ruby_primer.md +0 -19
- data/doc/getting_started/why_rooibos.md +0 -20
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_cmd_exec.png +0 -0
- data/doc/index.md +0 -93
- data/doc/scaling_up/async_patterns.md +0 -20
- data/doc/scaling_up/command_composition.md +0 -20
- data/doc/scaling_up/custom_commands.md +0 -21
- data/doc/scaling_up/fractal_architecture.md +0 -20
- data/doc/scaling_up/index.md +0 -30
- data/doc/scaling_up/message_routing.md +0 -20
- data/doc/scaling_up/ractor_safety.md +0 -20
- data/doc/scaling_up/testing.md +0 -21
- data/doc/troubleshooting/common_errors.md +0 -20
- data/doc/troubleshooting/debugging.md +0 -21
- data/doc/troubleshooting/index.md +0 -23
- data/doc/troubleshooting/performance.md +0 -20
- data/doc/tutorial/01_project_setup.md +0 -44
- data/doc/tutorial/02_hello_world.md +0 -45
- data/doc/tutorial/03_static_file_list.md +0 -44
- data/doc/tutorial/04_arrow_navigation.md +0 -47
- data/doc/tutorial/05_real_files.md +0 -45
- data/doc/tutorial/06_safe_refactoring.md +0 -21
- data/doc/tutorial/07_red_first_tdd.md +0 -26
- data/doc/tutorial/08_file_metadata.md +0 -42
- data/doc/tutorial/09_text_preview.md +0 -44
- data/doc/tutorial/10_directory_tree.md +0 -42
- data/doc/tutorial/11_pane_focus.md +0 -40
- data/doc/tutorial/12_sorting.md +0 -41
- data/doc/tutorial/13_filtering.md +0 -43
- data/doc/tutorial/14_toggle_hidden.md +0 -41
- data/doc/tutorial/15_text_input_widget.md +0 -43
- data/doc/tutorial/16_rename_files.md +0 -42
- data/doc/tutorial/17_confirmation_dialogs.md +0 -43
- data/doc/tutorial/18_progress_indicators.md +0 -43
- data/doc/tutorial/19_atomic_operations.md +0 -42
- data/doc/tutorial/20_external_editor.md +0 -42
- data/doc/tutorial/21_modal_overlays.md +0 -41
- data/doc/tutorial/22_error_handling.md +0 -43
- data/doc/tutorial/23_terminal_capabilities.md +0 -53
- data/doc/tutorial/24_mouse_events.md +0 -43
- data/doc/tutorial/25_resize_events.md +0 -43
- data/doc/tutorial/26_loading_states.md +0 -42
- data/doc/tutorial/27_performance.md +0 -43
- data/doc/tutorial/28_color_schemes.md +0 -47
- data/doc/tutorial/29_configuration.md +0 -124
- data/doc/tutorial/30_going_further.md +0 -17
- data/doc/tutorial/index.md +0 -17
- data/examples/app_fractal_dashboard/README.md +0 -60
- data/examples/app_fractal_dashboard/app.rb +0 -63
- data/examples/app_fractal_dashboard/dashboard/base.rb +0 -73
- data/examples/app_fractal_dashboard/dashboard/update_helpers.rb +0 -86
- data/examples/app_fractal_dashboard/dashboard/update_manual.rb +0 -87
- data/examples/app_fractal_dashboard/dashboard/update_router.rb +0 -43
- data/examples/app_fractal_dashboard/fragments/custom_shell_input.rb +0 -81
- data/examples/app_fractal_dashboard/fragments/custom_shell_modal.rb +0 -82
- data/examples/app_fractal_dashboard/fragments/custom_shell_output.rb +0 -90
- data/examples/app_fractal_dashboard/fragments/disk_usage.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/network_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/ping.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/stats_panel.rb +0 -45
- data/examples/app_fractal_dashboard/fragments/system_info.rb +0 -47
- data/examples/app_fractal_dashboard/fragments/uptime.rb +0 -47
- data/examples/tutorial/01/app.rb +0 -50
- data/examples/tutorial/02/app.rb +0 -64
- data/examples/tutorial/03/app.rb +0 -91
- data/examples/tutorial/06_safe_refactoring/app.rb +0 -124
- data/examples/verify_readme_usage/README.md +0 -54
- data/examples/verify_readme_usage/app.rb +0 -47
- data/examples/verify_website_first_app/app.rb +0 -85
- data/examples/verify_website_hello_mvu/app.rb +0 -31
- data/examples/widget_command_system/README.md +0 -70
- data/examples/widget_command_system/app.rb +0 -134
- data/generate_tutorial_stubs.rb +0 -126
- data/mise.toml +0 -8
- data/rbs_collection.lock.yaml +0 -108
- data/rbs_collection.yaml +0 -15
- data/tasks/example_viewer.html.erb +0 -172
- data/tasks/install.rake +0 -29
- data/tasks/resources/build.yml.erb +0 -55
- data/tasks/resources/index.html.erb +0 -44
- data/tasks/resources/rubies.yml +0 -7
- data/tasks/steep.rake +0 -11
- /data/{vendor/goodcop/base.yml → lib/rooibos/rubocop.yml} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de0fd230ae66b1f7bb75d7bc0dbaad374d929efe3102638f0dfd2a7558312bc5
|
|
4
|
+
data.tar.gz: 03bc89ff9e8880b053e022dfdf8bbab94f107e153e174402d9ce5c83b9644c2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de7f4335062b9feefca507ef70b058d1561efe6d3fd69ccf764c2b1e4809d17bd3378380b752cf5c1108957dc7ca94a249b276fe1058c1fe6d9abbecc8d1f060
|
|
7
|
+
data.tar.gz: b50d6ddb3cc4ab37d0d2514d3852c95dbedd6c4cb756cbcb7fe7fcf8937189e7137961704bc1a284a9777f5ee83bd8fe263f9bad99e71f45ea5e29d1da6c864e
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright (c) <year> <owner>
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/REUSE.toml
CHANGED
|
@@ -27,3 +27,8 @@ SPDX-License-Identifier = "LGPL-3.0-or-later"
|
|
|
27
27
|
path = 'doc/images/*'
|
|
28
28
|
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
29
29
|
SPDX-License-Identifier = "CC-BY-SA-4.0"
|
|
30
|
+
|
|
31
|
+
[[annotations]]
|
|
32
|
+
path = 'examples/tutorial/**'
|
|
33
|
+
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
34
|
+
SPDX-License-Identifier = "MIT-0"
|
data/exe/.gitkeep
ADDED
|
File without changes
|
|
@@ -236,6 +236,13 @@ module Rooibos
|
|
|
236
236
|
end
|
|
237
237
|
end
|
|
238
238
|
|
|
239
|
+
# Overwrite bundle gem's .rubocop.yml with Rooibos config
|
|
240
|
+
rubocop_file = app_path / ".rubocop.yml"
|
|
241
|
+
if rubocop_file.exist?
|
|
242
|
+
File.write(rubocop_file.to_s, rubocop_template)
|
|
243
|
+
puts "Updated #{rubocop_file}"
|
|
244
|
+
end
|
|
245
|
+
|
|
239
246
|
# Make initial git commit if git is enabled and bundle didn't
|
|
240
247
|
if git_enabled?(passthrough_args)
|
|
241
248
|
make_initial_commit(app_path)
|
|
@@ -367,6 +374,23 @@ module Rooibos
|
|
|
367
374
|
RUBY
|
|
368
375
|
end
|
|
369
376
|
private_class_method :test_template
|
|
377
|
+
|
|
378
|
+
def self.rubocop_template
|
|
379
|
+
<<~YAML
|
|
380
|
+
inherit_gem:
|
|
381
|
+
rooibos: lib/rooibos/rubocop.yml
|
|
382
|
+
|
|
383
|
+
AllCops:
|
|
384
|
+
TargetRubyVersion: 3.2
|
|
385
|
+
|
|
386
|
+
Style/StringLiterals:
|
|
387
|
+
EnforcedStyle: double_quotes
|
|
388
|
+
|
|
389
|
+
Style/StringLiteralsInInterpolation:
|
|
390
|
+
EnforcedStyle: double_quotes
|
|
391
|
+
YAML
|
|
392
|
+
end
|
|
393
|
+
private_class_method :rubocop_template
|
|
370
394
|
end
|
|
371
395
|
end
|
|
372
396
|
end
|
|
@@ -98,6 +98,16 @@ module Rooibos
|
|
|
98
98
|
out.put(Message::Batch.new(command: self))
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
|
+
|
|
102
|
+
def extract_bubbles # :nodoc:
|
|
103
|
+
bubbles, rest = commands.partition { |c| c.is_a?(Bubble) }
|
|
104
|
+
remaining = case rest.size
|
|
105
|
+
when 0 then nil
|
|
106
|
+
when 1 then rest.first
|
|
107
|
+
else Batch.new(*rest)
|
|
108
|
+
end
|
|
109
|
+
[bubbles, remaining]
|
|
110
|
+
end
|
|
101
111
|
end
|
|
102
112
|
end
|
|
103
113
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module Rooibos
|
|
9
|
+
module Command
|
|
10
|
+
# Carries a message outward through the fragment hierarchy.
|
|
11
|
+
#
|
|
12
|
+
# Nested fragments produce signals. Outer fragments consume them. Passing
|
|
13
|
+
# callbacks down the tree couples fragments tightly. Direct references
|
|
14
|
+
# make reuse difficult.
|
|
15
|
+
#
|
|
16
|
+
# This command wraps a message for bubbling. Outer fragments intercept it
|
|
17
|
+
# and decide how to handle it. With the Router DSL, use <tt>observe</tt>
|
|
18
|
+
# or <tt>intercept</tt>. Without the Router, check for <tt>Command::Bubble</tt>
|
|
19
|
+
# manually and extract the message. Unhandled bubbles can be re-returned
|
|
20
|
+
# to continue propagation outward.
|
|
21
|
+
#
|
|
22
|
+
# The runtime does not execute this command. Outer fragments handle it.
|
|
23
|
+
# Calling <tt>call</tt> raises an error.
|
|
24
|
+
#
|
|
25
|
+
# [message] The payload to propagate outward.
|
|
26
|
+
class Bubble < Data.define(:message)
|
|
27
|
+
include Custom
|
|
28
|
+
|
|
29
|
+
# No-op: unhandled bubbles that escape the Router hierarchy( when no
|
|
30
|
+
# fragment intercepts the message) are silently dropped.
|
|
31
|
+
def call(_out, _token) = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -69,7 +69,8 @@ module Rooibos
|
|
|
69
69
|
#
|
|
70
70
|
# When the runtime cancels your command (app exit, navigation, explicit cancel),
|
|
71
71
|
# it calls <tt>token.cancel!</tt> and waits this long for your command to stop.
|
|
72
|
-
# If your command does not exit within this window, it is
|
|
72
|
+
# If your command does not exit within this window, it is orphaned until
|
|
73
|
+
# process exit. There is no safe way to force-kill a Ruby thread.
|
|
73
74
|
#
|
|
74
75
|
# *This is NOT a lifetime limit.* Your command runs indefinitely until canceled.
|
|
75
76
|
# A WebSocket open for 15 minutes is fine. This timeout only applies to the
|
|
@@ -80,7 +81,7 @@ module Rooibos
|
|
|
80
81
|
# - <tt>0.5</tt> — Quick HTTP abort, no cleanup needed
|
|
81
82
|
# - <tt>2.0</tt> — Default, suitable for most commands
|
|
82
83
|
# - <tt>5.0</tt> — WebSocket close handshake with remote server
|
|
83
|
-
# - <tt>Float::INFINITY</tt> —
|
|
84
|
+
# - <tt>Float::INFINITY</tt> — Wait indefinitely for cooperative exit (database transactions)
|
|
84
85
|
#
|
|
85
86
|
# === Example
|
|
86
87
|
#
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module Rooibos
|
|
9
|
+
module Command
|
|
10
|
+
# Delivers a message to Update.
|
|
11
|
+
#
|
|
12
|
+
# Sometimes you have data ready now. A synchronous calculation. A value from
|
|
13
|
+
# the model. You want Update to process it as a message — pattern match on it,
|
|
14
|
+
# use predicates, the whole workflow.
|
|
15
|
+
#
|
|
16
|
+
# This command wraps any message and delivers it to Update via the runtime.
|
|
17
|
+
#
|
|
18
|
+
# Use it to send structured messages from Update, or to produce messages
|
|
19
|
+
# from synchronous operations.
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
# Define a message type:
|
|
24
|
+
# class CacheLoaded < Data.define(:envelope, :data)
|
|
25
|
+
# include Rooibos::Message::Predicates
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# Send from Update:
|
|
29
|
+
# in { type: :key, code: "f" }
|
|
30
|
+
# cache = load_yaml_cache("auth")
|
|
31
|
+
# Command.deliver(CacheLoaded.new(envelope: :auth, data: cache))
|
|
32
|
+
#
|
|
33
|
+
# Receive in Update:
|
|
34
|
+
# in { type: :cache_loaded, envelope: :auth, data: }
|
|
35
|
+
# model.with(auth: data)
|
|
36
|
+
#
|
|
37
|
+
# Or use predicates:
|
|
38
|
+
# elsif message.cache_loaded? and message.auth
|
|
39
|
+
# model.with(auth: message.data)
|
|
40
|
+
#
|
|
41
|
+
class Deliver < Data.define(:message)
|
|
42
|
+
include Custom
|
|
43
|
+
|
|
44
|
+
# Sends the message to the runtime.
|
|
45
|
+
def call(out, _token)
|
|
46
|
+
out.put(message)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/rooibos/command/http.rb
CHANGED
|
@@ -139,7 +139,7 @@ module Rooibos
|
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
# Net::HTTP is blocking; no cooperative cancellation possible.
|
|
142
|
-
# Grace period = 0 means runtime
|
|
142
|
+
# Grace period = 0 means runtime will orphan the blocked thread immediately.
|
|
143
143
|
def rooibos_cancellation_grace_period = 0
|
|
144
144
|
|
|
145
145
|
def self.parse_dwim_args(args, method_kw, url_kw, envelope_kw, body_kw, method_keywords) # :nodoc:
|
|
@@ -33,7 +33,7 @@ module Rooibos
|
|
|
33
33
|
# Runs a command synchronously, returning its result.
|
|
34
34
|
#
|
|
35
35
|
# Spawns a thread, races the result against cancellation and timeout.
|
|
36
|
-
# On cancellation, waits the grace period then
|
|
36
|
+
# On cancellation, waits the grace period then orphans the thread if needed.
|
|
37
37
|
#
|
|
38
38
|
# [command] Callable with <tt>call(out, token)</tt>.
|
|
39
39
|
# [token] Parent's cancellation token.
|
|
@@ -89,6 +89,8 @@ module Rooibos
|
|
|
89
89
|
command.call(outlet, cancellation)
|
|
90
90
|
rescue => e
|
|
91
91
|
channel.push Message::Error.new(command:, exception: e)
|
|
92
|
+
ensure
|
|
93
|
+
outlet.wait # Don't resolve until children from standing complete
|
|
92
94
|
end
|
|
93
95
|
|
|
94
96
|
entry = Entry.new(future:, origin:)
|
|
@@ -97,19 +97,29 @@ module Rooibos
|
|
|
97
97
|
|
|
98
98
|
# Sends a message to the runtime.
|
|
99
99
|
#
|
|
100
|
-
# Custom commands produce results.
|
|
101
|
-
# update function. This method handles the wiring.
|
|
102
|
-
#
|
|
103
|
-
# Call with one argument to send it directly. Call with multiple
|
|
104
|
-
# arguments and they arrive as an array.
|
|
100
|
+
# Custom commands produce results. Messages about those results feed back
|
|
101
|
+
# into your update function. This method handles the wiring.
|
|
105
102
|
#
|
|
106
103
|
# Use it for complex data flows or transports Rooibos doesn't ship with.
|
|
107
104
|
#
|
|
108
|
-
#
|
|
105
|
+
# For structured data and to avoid NoMethodError, define a custom
|
|
106
|
+
# Message class with +envelope+ and domain-specific fields, and mix in
|
|
107
|
+
# <tt>Rooibos::Message::Predicates</tt>. This follows the same pattern as
|
|
108
|
+
# built-in Message types and RatatuiRuby events.
|
|
109
|
+
#
|
|
110
|
+
# === Structured Messages
|
|
111
|
+
#
|
|
112
|
+
# class UserFetched < Data.define(:envelope, :user)
|
|
113
|
+
# include Rooibos::Message::Predicates
|
|
114
|
+
# end
|
|
115
|
+
#
|
|
116
|
+
# out.put(UserFetched.new(envelope: :profile, user: alice))
|
|
117
|
+
#
|
|
118
|
+
# # Update can pattern match:
|
|
119
|
+
# # in { type: :user_fetched, envelope: :profile, user: }
|
|
109
120
|
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# out.put(:user, alice) # Update receives [:user, alice]
|
|
121
|
+
# # Update can also use predicates:
|
|
122
|
+
# # message.user if message.user_fetched? and message.profile?
|
|
113
123
|
#
|
|
114
124
|
# Debug mode validates Ractor-shareability.
|
|
115
125
|
def put(*args)
|
data/lib/rooibos/command.rb
CHANGED
|
@@ -14,6 +14,8 @@ require_relative "command/batch"
|
|
|
14
14
|
require_relative "command/all"
|
|
15
15
|
require_relative "command/http"
|
|
16
16
|
require_relative "command/open"
|
|
17
|
+
require_relative "command/deliver"
|
|
18
|
+
require_relative "command/bubble"
|
|
17
19
|
|
|
18
20
|
module Rooibos
|
|
19
21
|
# Commands represent side effects.
|
|
@@ -54,12 +56,39 @@ module Rooibos
|
|
|
54
56
|
class Exit < Data.define
|
|
55
57
|
include Custom
|
|
56
58
|
|
|
59
|
+
# Ruby 3.x does not auto-freeze zero-member Data.define instances,
|
|
60
|
+
# which prevents Ractor shareability. Explicit freeze is idempotent on 4.0+.
|
|
61
|
+
def initialize # :nodoc:
|
|
62
|
+
super
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
# Stub - Exit is a sentinel handled by runtime before dispatch.
|
|
58
67
|
def call(_out, _token)
|
|
59
68
|
raise "Exit command should never be dispatched"
|
|
60
69
|
end
|
|
61
70
|
end
|
|
62
71
|
|
|
72
|
+
# Internal wrapper for multiple commands to be dispatched separately.
|
|
73
|
+
#
|
|
74
|
+
# Router DSL uses this to return multiple commands from observe + keymap
|
|
75
|
+
# without triggering Message::Batch. The runtime unwraps this and dispatches
|
|
76
|
+
# each command independently.
|
|
77
|
+
#
|
|
78
|
+
# Unlike Batch:
|
|
79
|
+
# - Does NOT send Message::Batch on completion
|
|
80
|
+
# - Each command runs and sends its own messages
|
|
81
|
+
# - Invisible to app developers
|
|
82
|
+
class Separate < Data.define(:commands) # :nodoc:
|
|
83
|
+
include Custom
|
|
84
|
+
|
|
85
|
+
# Stub - Separate is a sentinel unwrapped by runtime before dispatch.
|
|
86
|
+
def call(_out, _token)
|
|
87
|
+
raise "Separate command should never be dispatched directly"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
private_constant :Separate
|
|
91
|
+
|
|
63
92
|
# Creates a quit command.
|
|
64
93
|
#
|
|
65
94
|
# Returns a sentinel the runtime detects to terminate the application.
|
|
@@ -78,6 +107,76 @@ module Rooibos
|
|
|
78
107
|
Exit.new
|
|
79
108
|
end
|
|
80
109
|
|
|
110
|
+
# Delivers a message to Update.
|
|
111
|
+
#
|
|
112
|
+
# Custom commands produce results. Those results feed back into your update
|
|
113
|
+
# function. This factory method wraps a message in a command that delivers
|
|
114
|
+
# it when executed.
|
|
115
|
+
#
|
|
116
|
+
# === Example
|
|
117
|
+
#
|
|
118
|
+
# # Define a message type
|
|
119
|
+
# class FetchComplete < Data.define(:envelope, :data)
|
|
120
|
+
# include Rooibos::Message::Predicates
|
|
121
|
+
# end
|
|
122
|
+
#
|
|
123
|
+
# # Send after a synchronous operation
|
|
124
|
+
# result = fetch_data_sync()
|
|
125
|
+
# [model, Command.deliver(FetchComplete.new(envelope: :items, data: result))]
|
|
126
|
+
#
|
|
127
|
+
# # Receive in Update
|
|
128
|
+
# in { type: :fetch_complete, envelope: :items, data: }
|
|
129
|
+
# model.with(items: data)
|
|
130
|
+
def self.deliver(message)
|
|
131
|
+
Deliver.new(message:)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Bubbles a message outward through the fragment hierarchy.
|
|
135
|
+
#
|
|
136
|
+
# Nested fragments produce results. Sometimes those results belong to an outer
|
|
137
|
+
# fragment. Passing callbacks or references inward couples fragments tightly.
|
|
138
|
+
# The hierarchy becomes rigid.
|
|
139
|
+
#
|
|
140
|
+
# This command wraps a message for outward propagation. Outer fragments
|
|
141
|
+
# intercept the bubble and decide how to handle it. With the Router DSL,
|
|
142
|
+
# use <tt>observe</tt> or <tt>intercept</tt>. Without the Router, check
|
|
143
|
+
# for <tt>Command::Bubble</tt> manually and extract the message.
|
|
144
|
+
#
|
|
145
|
+
# Use it for notifications, validation results, or any signal that flows
|
|
146
|
+
# from nested fragments to outer containers.
|
|
147
|
+
#
|
|
148
|
+
# === Example (Router DSL)
|
|
149
|
+
#
|
|
150
|
+
# # Nested fragment signals completion
|
|
151
|
+
# class TaskComplete < Data.define(:envelope, :task_id)
|
|
152
|
+
# include Rooibos::Message::Predicates
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# # Return from nested Update
|
|
156
|
+
# [model, Command.bubble(TaskComplete.new(envelope: :task, task_id: 42))]
|
|
157
|
+
#
|
|
158
|
+
# # Outer Router observes the bubble
|
|
159
|
+
# observe TaskComplete do |model, message|
|
|
160
|
+
# model.with(completed_tasks: model.completed_tasks + [message.task_id])
|
|
161
|
+
# end
|
|
162
|
+
#
|
|
163
|
+
# === Example (Manual Bubbling)
|
|
164
|
+
#
|
|
165
|
+
# # Outer Update handles bubbles without Router
|
|
166
|
+
# def self.handle_nested_result(cmd, model)
|
|
167
|
+
# return [model, nil] unless cmd.is_a?(Command::Bubble)
|
|
168
|
+
#
|
|
169
|
+
# case cmd.message
|
|
170
|
+
# when TaskComplete
|
|
171
|
+
# [model.with(completed_tasks: model.completed_tasks + [cmd.message.task_id]), nil]
|
|
172
|
+
# else
|
|
173
|
+
# [model, cmd] # Re-bubble outward
|
|
174
|
+
# end
|
|
175
|
+
# end
|
|
176
|
+
def self.bubble(message)
|
|
177
|
+
Bubble.new(message:)
|
|
178
|
+
end
|
|
179
|
+
|
|
81
180
|
# Creates a fresh cancellation that never fires.
|
|
82
181
|
#
|
|
83
182
|
# Some I/O operations cannot be canceled mid-execution. Ruby's <tt>Net::HTTP</tt>
|
|
@@ -118,7 +217,6 @@ module Rooibos
|
|
|
118
217
|
# [model, Cancel.new(handle: model.active_fetch)]
|
|
119
218
|
class Cancel < Data.define(:handle)
|
|
120
219
|
include Custom
|
|
121
|
-
include Message::Predicates
|
|
122
220
|
|
|
123
221
|
# Stub - Cancel is a sentinel handled by runtime before dispatch.
|
|
124
222
|
def call(_out, _token)
|
|
@@ -214,7 +312,12 @@ module Rooibos
|
|
|
214
312
|
end
|
|
215
313
|
|
|
216
314
|
private def stream_execution(out, token)
|
|
217
|
-
|
|
315
|
+
# pgroup: true spawns the child in its own process group.
|
|
316
|
+
# On Linux, popen3(string) invokes /bin/sh which may fork (not exec),
|
|
317
|
+
# so killing just the shell PID leaves the child orphaned. Signaling
|
|
318
|
+
# the process group (-pid) ensures TERM reaches all descendants.
|
|
319
|
+
pgroup_opts = Gem.win_platform? ? {} : { pgroup: true }
|
|
320
|
+
Open3.popen3(command, **pgroup_opts) do |stdin, stdout, stderr, wait_thr|
|
|
218
321
|
stdin.close
|
|
219
322
|
pid = wait_thr.pid
|
|
220
323
|
|
|
@@ -225,7 +328,8 @@ module Rooibos
|
|
|
225
328
|
# Check cancellation before blocking on IO.select
|
|
226
329
|
if token.canceled? && wait_thr.alive?
|
|
227
330
|
begin
|
|
228
|
-
|
|
331
|
+
# On Unix, signal the process group; on Windows, kill the process directly
|
|
332
|
+
Process.kill("TERM", Gem.win_platform? ? pid : -pid)
|
|
229
333
|
rescue Errno::ESRCH
|
|
230
334
|
# Already dead
|
|
231
335
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module Rooibos
|
|
9
|
+
# Configuration represents the Mealy machine (state, input) pair.
|
|
10
|
+
# Immutable value object; use with_model to derive new configurations.
|
|
11
|
+
class Configuration < Data.define(:message, :model)
|
|
12
|
+
##
|
|
13
|
+
# Derives a new Configuration with an updated model.
|
|
14
|
+
# Returns <tt>self</tt> if <tt>new_model</tt> is <tt>nil</tt>.
|
|
15
|
+
def with_model(new_model)
|
|
16
|
+
return self unless new_model
|
|
17
|
+
|
|
18
|
+
Configuration.new(message:, model: new_model)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Destructures into <tt>[message, model]</tt> array.
|
|
23
|
+
def to_a
|
|
24
|
+
[message, model]
|
|
25
|
+
end
|
|
26
|
+
alias to_ary to_a
|
|
27
|
+
alias deconstruct to_a
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
module Rooibos
|
|
9
|
+
module Message
|
|
10
|
+
# Message synthesized by Router when a child fragment bubbles.
|
|
11
|
+
#
|
|
12
|
+
# When a child fragment returns <tt>Command.bubble(message)</tt>, the Router
|
|
13
|
+
# wraps the inner message in Bubbled and dispatches it through the
|
|
14
|
+
# outward flow (observe, then intercept).
|
|
15
|
+
#
|
|
16
|
+
# [message] The inner message that was bubbled.
|
|
17
|
+
Bubbled = Data.define(:message) do
|
|
18
|
+
include Predicates
|
|
19
|
+
|
|
20
|
+
def deconstruct_keys(_keys)
|
|
21
|
+
{ type: :bubbled, message: }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bubbled?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/rooibos/message.rb
CHANGED
|
@@ -68,11 +68,17 @@ module Rooibos
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# Returns <tt>
|
|
71
|
+
# Returns <tt>true</tt> if predicate matches <tt>:type</tt> or
|
|
72
|
+
# <tt>:envelope</tt> from <tt>deconstruct_keys</tt>. Returns
|
|
73
|
+
# <tt>false</tt> for unknown predicate methods.
|
|
72
74
|
def method_missing(name, *args, **kwargs, &block)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
if name.to_s.end_with?("?") && args.empty? && kwargs.empty?
|
|
76
|
+
predicate = name.to_s.chomp("?").to_sym
|
|
77
|
+
keys = deconstruct_keys(nil)
|
|
78
|
+
keys[:type] == predicate || keys[:envelope] == predicate
|
|
79
|
+
else
|
|
80
|
+
super
|
|
81
|
+
end
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# Fallback pattern matching for classes without explicit deconstruct_keys.
|
|
@@ -89,14 +95,25 @@ module Rooibos
|
|
|
89
95
|
# msg = MyCustomMessage.new
|
|
90
96
|
# msg.deconstruct_keys(nil) # => { type: :my_custom_message }
|
|
91
97
|
# msg.to_sym # => :message_my_custom_message
|
|
92
|
-
def deconstruct_keys(
|
|
98
|
+
def deconstruct_keys(keys)
|
|
93
99
|
class_name = self.class.name&.split("::")&.last
|
|
94
100
|
type_name = if class_name
|
|
95
101
|
class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
|
96
102
|
else
|
|
97
103
|
:custom
|
|
98
104
|
end
|
|
99
|
-
|
|
105
|
+
|
|
106
|
+
# Filter out :type before calling super — Data returns {} if any
|
|
107
|
+
# requested key is unknown, which breaks pattern matching
|
|
108
|
+
filtered_keys = keys&.reject { |k| k == :type }
|
|
109
|
+
|
|
110
|
+
# Preserve parent's fields (e.g., Data.define members) and add :type
|
|
111
|
+
parent_keys = begin
|
|
112
|
+
super(filtered_keys)
|
|
113
|
+
rescue NoMethodError
|
|
114
|
+
{} #: Hash[Symbol, untyped]
|
|
115
|
+
end
|
|
116
|
+
parent_keys.merge(type: type_name)
|
|
100
117
|
end
|
|
101
118
|
|
|
102
119
|
# Responds to all predicate methods.
|
|
@@ -117,3 +134,4 @@ require_relative "message/batch"
|
|
|
117
134
|
require_relative "message/error"
|
|
118
135
|
require_relative "message/canceled"
|
|
119
136
|
require_relative "message/routed"
|
|
137
|
+
require_relative "message/bubbled"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
# Lambda action - calls handler directly with (message, model).
|
|
13
|
+
class LambdaAction < Data.define(:name, :handler)
|
|
14
|
+
def routed? = false
|
|
15
|
+
|
|
16
|
+
def apply(message, model, _routes)
|
|
17
|
+
result = handler.arity.zero? ? handler.call : handler.call(message, model)
|
|
18
|
+
Transition.from(result, model)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
private_constant :LambdaAction
|
|
22
|
+
|
|
23
|
+
# Routed action - wraps message and delegates through route to fragment.
|
|
24
|
+
class RoutedAction < Data.define(:name, :fragment)
|
|
25
|
+
def routed? = true
|
|
26
|
+
|
|
27
|
+
def apply(message, model, routes)
|
|
28
|
+
route = routes.find { |r| r.fragment == fragment }
|
|
29
|
+
raise ArgumentError, "No route found for fragment #{fragment}" unless route
|
|
30
|
+
routed_message = Message::Routed.new(envelope: name, event: message)
|
|
31
|
+
route.delegate(routed_message, model)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
private_constant :RoutedAction
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
module Flow
|
|
13
|
+
module Dispatch
|
|
14
|
+
private def run_all(rules, message, model)
|
|
15
|
+
transition = Transition.initial(model)
|
|
16
|
+
rules.each do |rule|
|
|
17
|
+
config = Configuration.new(message:, model: transition.model)
|
|
18
|
+
new_transition = rule.apply_if_matches(config, routes) or next
|
|
19
|
+
transition = separate_commands(transition, new_transition)
|
|
20
|
+
end
|
|
21
|
+
transition
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private def apply_first_matching(rules, config, transition)
|
|
25
|
+
rules.each do |rule|
|
|
26
|
+
new_transition = rule.apply_if_matches(config, routes) or next
|
|
27
|
+
return separate_commands(transition, new_transition)
|
|
28
|
+
end
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def separate_commands(transition, new_transition) = transition
|
|
33
|
+
.with_model(new_transition.model)
|
|
34
|
+
.with_separate_command(new_transition.command)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_constant :Flow
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
|
+
#++
|
|
8
|
+
|
|
9
|
+
module Rooibos
|
|
10
|
+
module Router
|
|
11
|
+
# :stopdoc:
|
|
12
|
+
module Flow
|
|
13
|
+
# Inward flow: messages traveling toward the leaves.
|
|
14
|
+
#
|
|
15
|
+
# Observe → receive → forward → otherwise.
|
|
16
|
+
class Inward < Data.define(:observes, :receives, :forwards, :otherwises, :routes)
|
|
17
|
+
include Dispatch
|
|
18
|
+
|
|
19
|
+
def initialize(observes:, receives:, forwards:, otherwises:, routes:)
|
|
20
|
+
super
|
|
21
|
+
validate_routes!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(message, model)
|
|
25
|
+
transition = run_all(observes, message, model)
|
|
26
|
+
config = Configuration.new(message:, model: transition.model)
|
|
27
|
+
apply_first_matching(receives, config, transition) ||
|
|
28
|
+
apply_first_matching(forwards, config, transition) ||
|
|
29
|
+
apply_first_matching(otherwises, config, transition) ||
|
|
30
|
+
transition
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private def validate_routes!
|
|
34
|
+
forwards.each { |fwd| Array(fwd.targets).each { |t| routes.for(t) } unless fwd.targets == ALL_ROUTES }
|
|
35
|
+
otherwises.each { |ow| routes.for(ow.target) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
private_constant :Flow
|
|
40
|
+
end
|
|
41
|
+
end
|