rooibos 0.7.3 → 0.8.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/REUSE.toml +5 -0
- data/lib/rooibos/cli/commands/new.rb +1 -1
- data/lib/rooibos/command/clock.rb +57 -0
- data/lib/rooibos/command/random.rb +71 -0
- data/lib/rooibos/command/timed.rb +60 -0
- data/lib/rooibos/command/wait.rb +4 -27
- data/lib/rooibos/command.rb +39 -0
- data/lib/rooibos/message/clock.rb +40 -0
- data/lib/rooibos/message/random.rb +40 -0
- data/lib/rooibos/message.rb +2 -0
- data/lib/rooibos/router/flow/outward.rb +3 -2
- data/lib/rooibos/router/registry/forwards.rb +6 -6
- data/lib/rooibos/router/router_update.rb +15 -7
- data/lib/rooibos/router/rule/forward.rb +2 -1
- data/lib/rooibos/router.rb +7 -1
- data/lib/rooibos/runtime.rb +4 -2
- data/lib/rooibos/shortcuts.rb +8 -0
- data/lib/rooibos/version.rb +1 -1
- data/sig/rooibos/command.rbs +54 -1
- data/sig/rooibos/message.rbs +22 -0
- data/sig/rooibos/router/forwards.rbs +3 -3
- data/sig/rooibos/runtime.rbs +6 -3
- data/sig/rooibos/shortcuts.rbs +2 -0
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 978bf3a71ec2efd09d2b4d1fd7f5d8c04bfe9af2705df1b26f3f341a0e56e1e3
|
|
4
|
+
data.tar.gz: 6ab63cbe1f40881689c49a4990e71f4e4ba1dcb5b76f81d27b9fe96a9257121d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e89b8ec3d0fa1377204256c9bcd2a6ba9cbdea6e862b3b041b82e35482a84449308737ed087e13ef65266bce291dae8e35b7e6d778bc319dbbb6e1df25cdfa5
|
|
7
|
+
data.tar.gz: e64af325d6c7f4e578390d541f5414d5838aaa434ae8fd828d352b6d949b02fa1ea12102a41f3e0da01342e2ba5d2fa633901b8e3aa728300c54defc2ad3f879
|
data/REUSE.toml
CHANGED
|
@@ -8,6 +8,11 @@ path = 'Gemfile.lock'
|
|
|
8
8
|
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
9
9
|
SPDX-License-Identifier = "CC0-1.0"
|
|
10
10
|
|
|
11
|
+
[[annotations]]
|
|
12
|
+
path = 'rbs_collection.lock.yaml'
|
|
13
|
+
SPDX-FileCopyrightText = "2026 Kerrick Long <me@kerricklong.com>"
|
|
14
|
+
SPDX-License-Identifier = "CC0-1.0"
|
|
15
|
+
|
|
11
16
|
[[annotations]]
|
|
12
17
|
path = 'README.rdoc'
|
|
13
18
|
SPDX-FileCopyrightText = "2025 Kerrick Long <me@kerricklong.com>"
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
# A one-shot clock command.
|
|
11
|
+
#
|
|
12
|
+
# Applications display the time, throttle refreshes, and show
|
|
13
|
+
# "last updated 30 seconds ago." All of these call <tt>Time.now</tt>.
|
|
14
|
+
# But <tt>Time.now</tt> in Update is a side effect. The same model
|
|
15
|
+
# and message produce different results depending on when you call
|
|
16
|
+
# them. Testing becomes non-deterministic.
|
|
17
|
+
#
|
|
18
|
+
# This command waits, then sends a <tt>Message::Clock</tt> with the
|
|
19
|
+
# current time. It responds to cancellation cooperatively. When
|
|
20
|
+
# canceled, it sends <tt>Message::Canceled</tt> so you know the
|
|
21
|
+
# clock stopped.
|
|
22
|
+
#
|
|
23
|
+
# Use it for periodic time updates, scheduling, or any feature that
|
|
24
|
+
# asks "what time is it?"
|
|
25
|
+
#
|
|
26
|
+
# Prefer the <tt>Command.clock</tt> factory method for convenience.
|
|
27
|
+
#
|
|
28
|
+
# === Example: Periodic refresh
|
|
29
|
+
#
|
|
30
|
+
# def update(msg, model)
|
|
31
|
+
# case msg
|
|
32
|
+
# in { type: :clock, envelope: :refresh, time: }
|
|
33
|
+
# [model.with(current_time: time.utc.to_s),
|
|
34
|
+
# Command.clock(1, :refresh)]
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# === Example: "Last updated" display
|
|
39
|
+
#
|
|
40
|
+
# def update(msg, model)
|
|
41
|
+
# case msg
|
|
42
|
+
# in { type: :clock, envelope: :clock, time: }
|
|
43
|
+
# ago = (time - model.last_fetch).round
|
|
44
|
+
# [model.with(status: "Updated #{ago}s ago"),
|
|
45
|
+
# Command.clock(1, :clock)]
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
class Clock < Data.define(:seconds, :envelope)
|
|
49
|
+
include Custom
|
|
50
|
+
include Timed
|
|
51
|
+
|
|
52
|
+
private def timed_response(_start_time)
|
|
53
|
+
Message::Clock.new(envelope:, time: Time.now)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
# A command that generates a random value.
|
|
11
|
+
#
|
|
12
|
+
# Games roll dice. Encryption needs keys. Shuffling needs seeds.
|
|
13
|
+
# All of these call <tt>Kernel#rand</tt> or <tt>Random</tt>. But
|
|
14
|
+
# randomness in Update is a side effect: the same model and message
|
|
15
|
+
# would produce different results on each call. Testing would become
|
|
16
|
+
# fragile and non-deterministic.
|
|
17
|
+
#
|
|
18
|
+
# This command delegates to Ruby's <tt>Random</tt> class through
|
|
19
|
+
# the runtime. Update receives the result as a
|
|
20
|
+
# <tt>Message::Random</tt>.
|
|
21
|
+
#
|
|
22
|
+
# Use it for dice rolls, shuffles, key generation, or any feature
|
|
23
|
+
# that needs random values without side effects in Update.
|
|
24
|
+
#
|
|
25
|
+
# Prefer the <tt>Command.random</tt> factory method for convenience.
|
|
26
|
+
#
|
|
27
|
+
# === Example: Dice roll
|
|
28
|
+
#
|
|
29
|
+
# def update(msg, model)
|
|
30
|
+
# case msg
|
|
31
|
+
# in { type: :random, envelope: :roll, value: }
|
|
32
|
+
# model.with(die_face: value)
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# === Example: Random float
|
|
37
|
+
#
|
|
38
|
+
# def update(msg, model)
|
|
39
|
+
# case msg
|
|
40
|
+
# in { type: :random, envelope: :spawn_chance, value: }
|
|
41
|
+
# model.with(should_spawn: value < 0.3)
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
class Random < Data.define(:args, :envelope)
|
|
45
|
+
include Custom
|
|
46
|
+
|
|
47
|
+
# Executes the random command.
|
|
48
|
+
#
|
|
49
|
+
# Without a leading symbol, calls <tt>Random.rand</tt> with the
|
|
50
|
+
# stored arguments. With a leading symbol, calls that method on
|
|
51
|
+
# <tt>Random</tt> via <tt>public_send</tt>.
|
|
52
|
+
#
|
|
53
|
+
# [out] Outlet for sending messages.
|
|
54
|
+
# [token] Cancellation token from the runtime.
|
|
55
|
+
def call(out, token)
|
|
56
|
+
if token.canceled?
|
|
57
|
+
out.put(Message::Canceled.new(command: self))
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
value = if args.first.is_a?(Symbol)
|
|
62
|
+
method_name = args.first
|
|
63
|
+
::Random.public_send(method_name, *args[1..])
|
|
64
|
+
else
|
|
65
|
+
::Random.rand(*args)
|
|
66
|
+
end
|
|
67
|
+
out.put(Ractor.make_shareable(Message::Random.new(envelope:, value:)))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
# Shared behavior for commands that wait, then respond.
|
|
11
|
+
#
|
|
12
|
+
# Timer and clock commands follow the same pattern: wait for
|
|
13
|
+
# <tt>seconds</tt>, check for cancellation, then send a response.
|
|
14
|
+
# Duplicating the cancellation dance is error-prone.
|
|
15
|
+
#
|
|
16
|
+
# This module extracts the wait-cancel-respond skeleton. Include
|
|
17
|
+
# it in any <tt>Data.define(:seconds, :envelope)</tt> command and
|
|
18
|
+
# implement <tt>timed_response</tt> to build the success message.
|
|
19
|
+
#
|
|
20
|
+
# === Example
|
|
21
|
+
#
|
|
22
|
+
# class MyTimer < Data.define(:seconds, :envelope)
|
|
23
|
+
# include Custom
|
|
24
|
+
# include Timed
|
|
25
|
+
#
|
|
26
|
+
# private def timed_response(start_time)
|
|
27
|
+
# Message::Timer.new(envelope:, elapsed: Time.now - start_time)
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
module Timed # :nodoc:
|
|
32
|
+
# Cooperative cancellation needs no grace period.
|
|
33
|
+
# The command responds instantly to cancellation via
|
|
34
|
+
# <tt>Concurrent::Cancellation.timeout</tt>.
|
|
35
|
+
def rooibos_cancellation_grace_period
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Waits for <tt>seconds</tt>, then sends the result of
|
|
40
|
+
# <tt>timed_response</tt>. If canceled, sends
|
|
41
|
+
# <tt>Message::Canceled</tt> instead.
|
|
42
|
+
#
|
|
43
|
+
# [out] Outlet for sending messages.
|
|
44
|
+
# [token] Cancellation token from the runtime.
|
|
45
|
+
def call(out, token)
|
|
46
|
+
start_time = Time.now
|
|
47
|
+
timer_cancellation, _origin = Concurrent::Cancellation.timeout(seconds)
|
|
48
|
+
combined = token.join(timer_cancellation)
|
|
49
|
+
combined.origin.wait
|
|
50
|
+
|
|
51
|
+
if token.canceled?
|
|
52
|
+
out.put(Message::Canceled.new(command: self))
|
|
53
|
+
else
|
|
54
|
+
out.put(Ractor.make_shareable(timed_response(start_time)))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
private_constant :Timed
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/rooibos/command/wait.rb
CHANGED
|
@@ -49,34 +49,11 @@ module Rooibos
|
|
|
49
49
|
# end
|
|
50
50
|
class Wait < Data.define(:seconds, :envelope)
|
|
51
51
|
include Custom
|
|
52
|
+
include Timed
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def rooibos_cancellation_grace_period
|
|
57
|
-
0
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Executes the timer.
|
|
61
|
-
#
|
|
62
|
-
# Waits for <tt>seconds</tt>, then sends <tt>TimerResponse</tt>.
|
|
63
|
-
# If canceled, sends <tt>Message::Canceled</tt> instead.
|
|
64
|
-
#
|
|
65
|
-
# [out] Outlet for sending messages.
|
|
66
|
-
# [token] Cancellation token from the runtime.
|
|
67
|
-
def call(out, token)
|
|
68
|
-
start_time = Time.now
|
|
69
|
-
timer_cancellation, _origin = Concurrent::Cancellation.timeout(seconds)
|
|
70
|
-
combined = token.join(timer_cancellation)
|
|
71
|
-
combined.origin.wait
|
|
72
|
-
|
|
73
|
-
if token.canceled?
|
|
74
|
-
out.put(Message::Canceled.new(command: self))
|
|
75
|
-
else
|
|
76
|
-
elapsed = Time.now - start_time
|
|
77
|
-
response = Message::Timer.new(envelope:, elapsed:)
|
|
78
|
-
out.put(Ractor.make_shareable(response))
|
|
79
|
-
end
|
|
54
|
+
private def timed_response(start_time)
|
|
55
|
+
elapsed = Time.now - start_time
|
|
56
|
+
Message::Timer.new(envelope:, elapsed:)
|
|
80
57
|
end
|
|
81
58
|
end
|
|
82
59
|
end
|
data/lib/rooibos/command.rb
CHANGED
|
@@ -9,7 +9,10 @@ require "concurrent-edge"
|
|
|
9
9
|
require_relative "command/custom"
|
|
10
10
|
require_relative "command/outlet"
|
|
11
11
|
require_relative "command/lifecycle"
|
|
12
|
+
require_relative "command/timed"
|
|
12
13
|
require_relative "command/wait"
|
|
14
|
+
require_relative "command/clock"
|
|
15
|
+
require_relative "command/random"
|
|
13
16
|
require_relative "command/batch"
|
|
14
17
|
require_relative "command/all"
|
|
15
18
|
require_relative "command/http"
|
|
@@ -86,6 +89,16 @@ module Rooibos
|
|
|
86
89
|
def call(_out, _token)
|
|
87
90
|
raise "Separate command should never be dispatched directly"
|
|
88
91
|
end
|
|
92
|
+
|
|
93
|
+
def extract_bubbles # :nodoc:
|
|
94
|
+
bubbles, rest = commands.partition { |c| c.is_a?(Bubble) }
|
|
95
|
+
remaining = case rest.size
|
|
96
|
+
when 0 then nil
|
|
97
|
+
when 1 then rest.first
|
|
98
|
+
else Separate.new(commands: rest)
|
|
99
|
+
end
|
|
100
|
+
[bubbles, remaining]
|
|
101
|
+
end
|
|
89
102
|
end
|
|
90
103
|
private_constant :Separate
|
|
91
104
|
|
|
@@ -584,6 +597,32 @@ module Rooibos
|
|
|
584
597
|
# [tag] Symbol to tag the result message.
|
|
585
598
|
singleton_class.alias_method :tick, :wait
|
|
586
599
|
|
|
600
|
+
# Creates a wall-clock time command.
|
|
601
|
+
#
|
|
602
|
+
# Waits for +seconds+ then sends +Message::Clock+ with the current time.
|
|
603
|
+
# Use for displaying time, throttling refreshes, or scheduling.
|
|
604
|
+
#
|
|
605
|
+
# [seconds] Duration to wait (Float or Integer).
|
|
606
|
+
# [envelope] Symbol to tag the result message.
|
|
607
|
+
def self.clock(seconds, envelope)
|
|
608
|
+
Clock.new(seconds:, envelope:)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Creates a random value command.
|
|
612
|
+
#
|
|
613
|
+
# Delegates to Ruby's <tt>Random</tt> class through the runtime.
|
|
614
|
+
# The last argument is always the envelope. Everything before it
|
|
615
|
+
# maps to <tt>Random</tt>.
|
|
616
|
+
#
|
|
617
|
+
# Without a leading symbol, calls <tt>Random#rand</tt>.
|
|
618
|
+
# With a leading symbol, calls that method on <tt>Random</tt>.
|
|
619
|
+
#
|
|
620
|
+
# [*args] Arguments to pass to <tt>Random</tt>, followed by the envelope.
|
|
621
|
+
def self.random(*args)
|
|
622
|
+
envelope = args.pop
|
|
623
|
+
Random.new(args: args.freeze, envelope:)
|
|
624
|
+
end
|
|
625
|
+
|
|
587
626
|
# Creates a parallel batch command.
|
|
588
627
|
#
|
|
589
628
|
# Applications fetch data from multiple sources. Dashboard panels load
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
# Response from a clock command.
|
|
11
|
+
#
|
|
12
|
+
# Long-lived applications display the time. Dashboards show "last updated
|
|
13
|
+
# 30 seconds ago." Schedulers fire actions at specific hours. All of these
|
|
14
|
+
# need wall-clock time. But calling <tt>Time.now</tt> in Update is a side
|
|
15
|
+
# effect—the same model and message produce different results depending
|
|
16
|
+
# on when you call them.
|
|
17
|
+
#
|
|
18
|
+
# This response carries the wall-clock time from the runtime. Update
|
|
19
|
+
# pattern-matches on the envelope to distinguish multiple clocks.
|
|
20
|
+
#
|
|
21
|
+
# Use it to handle <tt>Command.clock</tt> completions.
|
|
22
|
+
#
|
|
23
|
+
# === Example
|
|
24
|
+
#
|
|
25
|
+
# case msg
|
|
26
|
+
# in { type: :clock, envelope: :refresh, time: }
|
|
27
|
+
# [model.with(last_refresh: time), Command.clock(1, :refresh)]
|
|
28
|
+
# in { type: :clock, envelope: :display, time: }
|
|
29
|
+
# model.with(current_time: time.utc.to_s)
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class Clock < Data.define(:envelope, :time)
|
|
33
|
+
include Predicates
|
|
34
|
+
# Returns <tt>true</tt> for clock responses.
|
|
35
|
+
def clock?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
# Response from a random command.
|
|
11
|
+
#
|
|
12
|
+
# Games roll dice. Encryption needs keys. Shuffling needs seeds.
|
|
13
|
+
# All of these call <tt>Kernel#rand</tt> or <tt>Random</tt>. But
|
|
14
|
+
# randomness in Update is a side effect: the same model and message
|
|
15
|
+
# would produce different results on each call. Testing would become
|
|
16
|
+
# fragile.
|
|
17
|
+
#
|
|
18
|
+
# This response carries the random value from the runtime. Update
|
|
19
|
+
# pattern-matches on the envelope to distinguish multiple rolls.
|
|
20
|
+
#
|
|
21
|
+
# Use it to handle <tt>Command.random</tt> completions.
|
|
22
|
+
#
|
|
23
|
+
# === Example
|
|
24
|
+
#
|
|
25
|
+
# case msg
|
|
26
|
+
# in { type: :random, envelope: :roll_die, value: }
|
|
27
|
+
# model.with(roll: value)
|
|
28
|
+
# in { type: :random, envelope: :shuffle_seed, value: }
|
|
29
|
+
# model.with(sort_seed: value)
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class Random < Data.define(:envelope, :value)
|
|
33
|
+
include Predicates
|
|
34
|
+
# Returns +true+ for random responses.
|
|
35
|
+
def random?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/rooibos/message.rb
CHANGED
|
@@ -125,6 +125,8 @@ module Rooibos
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
require_relative "message/timer"
|
|
128
|
+
require_relative "message/clock"
|
|
129
|
+
require_relative "message/random"
|
|
128
130
|
require_relative "message/open"
|
|
129
131
|
require_relative "message/http_response"
|
|
130
132
|
require_relative "message/system/batch"
|
|
@@ -12,8 +12,8 @@ module Rooibos
|
|
|
12
12
|
module Flow
|
|
13
13
|
# Outward flow: messages traveling toward the root.
|
|
14
14
|
#
|
|
15
|
-
# Observe → intercept.
|
|
16
|
-
class Outward < Data.define(:observes, :receives, :routes)
|
|
15
|
+
# Observe → intercept → forward.
|
|
16
|
+
class Outward < Data.define(:observes, :receives, :forwards, :routes)
|
|
17
17
|
include Dispatch
|
|
18
18
|
|
|
19
19
|
# Sentinel: intercept consumed the bubble. Non-nil so
|
|
@@ -25,6 +25,7 @@ module Rooibos
|
|
|
25
25
|
transition = run_all(observes, message, model)
|
|
26
26
|
config = Configuration.new(message:, model: transition.model)
|
|
27
27
|
intercept_first_matching(receives, config, transition) ||
|
|
28
|
+
apply_first_matching(forwards, config, transition) ||
|
|
28
29
|
transition
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -23,22 +23,22 @@ module Rooibos
|
|
|
23
23
|
envelope: as
|
|
24
24
|
))
|
|
25
25
|
|
|
26
|
-
def add_events(keys, to
|
|
26
|
+
def add_events(keys, to: nil, as: nil, broadcast: false, broadcast_to: nil, guard: nil, when: nil, unless: nil) = add(Forward.new(
|
|
27
27
|
predicate: Predicate::Events.new(keys:),
|
|
28
|
-
targets: to,
|
|
28
|
+
targets: resolve_targets(to:, broadcast:, broadcast_to:),
|
|
29
29
|
envelope: as,
|
|
30
30
|
guard: Guard.from(guard:, when: binding.local_variable_get(:when), unless: binding.local_variable_get(:unless))
|
|
31
31
|
))
|
|
32
32
|
|
|
33
|
-
def add_routed(envelopes, to
|
|
33
|
+
def add_routed(envelopes, to: nil, as: nil, broadcast: false, broadcast_to: nil) = add(Forward.new(
|
|
34
34
|
predicate: Predicate::RoutedEnvelopes.new(envelopes:),
|
|
35
|
-
targets: to,
|
|
35
|
+
targets: resolve_targets(to:, broadcast:, broadcast_to:),
|
|
36
36
|
envelope: as
|
|
37
37
|
))
|
|
38
38
|
|
|
39
|
-
def add_custom(predicate, to
|
|
39
|
+
def add_custom(predicate, to: nil, as: nil, broadcast: false, broadcast_to: nil, **guard_opts) = add(Forward.new(
|
|
40
40
|
predicate:,
|
|
41
|
-
targets: to,
|
|
41
|
+
targets: resolve_targets(to:, broadcast:, broadcast_to:),
|
|
42
42
|
envelope: as,
|
|
43
43
|
guard: Guard.from(**guard_opts)
|
|
44
44
|
))
|
|
@@ -11,6 +11,7 @@ module Rooibos
|
|
|
11
11
|
# :stopdoc:
|
|
12
12
|
# Encapsulates dispatch logic - given frozen rule sets, processes messages.
|
|
13
13
|
class RouterUpdate < Data.define(:inward, :outward)
|
|
14
|
+
Separate = Command.const_get(:Separate)
|
|
14
15
|
def call(message, model)
|
|
15
16
|
case message
|
|
16
17
|
when Message::Bubbled then dispatch_outward(message.message, model)
|
|
@@ -20,18 +21,24 @@ module Rooibos
|
|
|
20
21
|
|
|
21
22
|
private def dispatch_inward(message, model)
|
|
22
23
|
transition = inward.call(message, model)
|
|
24
|
+
transition = extract_bubbles(transition)
|
|
25
|
+
transition = transition.with(command: nil) if transition.command.equal?(Flow::Outward::INTERCEPTED)
|
|
26
|
+
transition.to_a
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private def extract_bubbles(transition)
|
|
23
30
|
case transition.command
|
|
24
31
|
when Command::Bubble # Sentinel; not a real Command to be handled by the runtime
|
|
25
32
|
bubble_model, bubble_cmd = dispatch_outward(transition.command.message, transition.model)
|
|
26
|
-
transition
|
|
27
|
-
when Command::Batch
|
|
28
|
-
|
|
33
|
+
transition.with_model(bubble_model).with_command(bubble_cmd)
|
|
34
|
+
when Command::Batch, Separate
|
|
35
|
+
extract_bubbles_from_compound(transition)
|
|
36
|
+
else
|
|
37
|
+
transition
|
|
29
38
|
end
|
|
30
|
-
transition = transition.with(command: nil) if transition.command.equal?(Flow::Outward::INTERCEPTED)
|
|
31
|
-
transition.to_a
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
private def
|
|
41
|
+
private def extract_bubbles_from_compound(transition)
|
|
35
42
|
bubbles, remaining = transition.command.extract_bubbles
|
|
36
43
|
return transition if bubbles.empty?
|
|
37
44
|
|
|
@@ -39,7 +46,8 @@ module Rooibos
|
|
|
39
46
|
bubbles.each do |bubble|
|
|
40
47
|
model, cmd = dispatch_outward(bubble.message, result.model)
|
|
41
48
|
result = Transition.new(model:, command: result.command)
|
|
42
|
-
result = result.with_added_command(
|
|
49
|
+
result = result.with_added_command(bubble) if cmd.nil?
|
|
50
|
+
result = result.with_added_command(cmd) unless cmd.equal?(Flow::Outward::INTERCEPTED)
|
|
43
51
|
end
|
|
44
52
|
result
|
|
45
53
|
end
|
data/lib/rooibos/router.rb
CHANGED
|
@@ -123,7 +123,7 @@ module Rooibos
|
|
|
123
123
|
def from_router
|
|
124
124
|
RouterUpdate.new(
|
|
125
125
|
inward: Flow::Inward.new(observes:, receives:, forwards:, otherwises:, routes:),
|
|
126
|
-
outward: Flow::Outward.new(observes:, receives:, routes:)
|
|
126
|
+
outward: Flow::Outward.new(observes:, receives:, forwards:, routes:)
|
|
127
127
|
)
|
|
128
128
|
end
|
|
129
129
|
|
|
@@ -298,6 +298,9 @@ module Rooibos
|
|
|
298
298
|
# with a semantic envelope. This decouples keybindings from nested
|
|
299
299
|
# fragment internals.
|
|
300
300
|
#
|
|
301
|
+
# Use <tt>broadcast: true</tt> to send to all declared routes, or
|
|
302
|
+
# <tt>broadcast_to:</tt> with an array of specific route targets.
|
|
303
|
+
#
|
|
301
304
|
# === Example
|
|
302
305
|
#
|
|
303
306
|
# forward_events :enter, to: :active_form, as: :submit
|
|
@@ -332,6 +335,9 @@ module Rooibos
|
|
|
332
335
|
# Each layer speaks its inner fragment's API without knowing what
|
|
333
336
|
# lies deeper.
|
|
334
337
|
#
|
|
338
|
+
# Use <tt>broadcast: true</tt> to send to all declared routes, or
|
|
339
|
+
# <tt>broadcast_to:</tt> with an array of specific route targets.
|
|
340
|
+
#
|
|
335
341
|
# === Example
|
|
336
342
|
#
|
|
337
343
|
# forward_routed :leaf_1, to: :top_leaf, as: :increment
|
data/lib/rooibos/runtime.rb
CHANGED
|
@@ -120,16 +120,18 @@ module Rooibos
|
|
|
120
120
|
# [view] Callable receiving <tt>(model, tui)</tt>, returns a widget. *Required if fragment not provided.*
|
|
121
121
|
# [update] Callable receiving <tt>(message, model)</tt>, returns <tt>[new_model, command]</tt> or just <tt>new_model</tt>. *Required if fragment not provided.*
|
|
122
122
|
# [command] Optional callable to run at startup. Returns a message for update.
|
|
123
|
+
# [update_every_frame] When +true+, <tt>Event::None</tt> (idle frame) events are passed to Update instead of being dropped. Use for animations, physics, or any per-frame state change. Default +false+.
|
|
123
124
|
#
|
|
124
125
|
# == Raises
|
|
125
126
|
#
|
|
126
127
|
# [Rooibos::Error::Invariant] If both fragment and any of (model, view, update, command) are provided.
|
|
127
|
-
def self.run(root_fragment = nil, fps: 60, model: nil, view: nil, update: nil, command: nil)
|
|
128
|
+
def self.run(root_fragment = nil, fps: 60, model: nil, view: nil, update: nil, command: nil, update_every_frame: false)
|
|
128
129
|
@fragment = fragment_from_kwargs(root_fragment, model:, view:, update:, command:)
|
|
129
130
|
@view = @fragment::View
|
|
130
131
|
@update = @fragment::Update
|
|
131
132
|
@init_callable = init_callable
|
|
132
133
|
@timeout = 1.0 / fps
|
|
134
|
+
@update_every_frame = update_every_frame
|
|
133
135
|
|
|
134
136
|
start_runtime
|
|
135
137
|
end
|
|
@@ -310,7 +312,7 @@ module Rooibos
|
|
|
310
312
|
|
|
311
313
|
private def handle_ratatui_event
|
|
312
314
|
message = @tui.poll_event(timeout: @timeout)
|
|
313
|
-
return false if message.none?
|
|
315
|
+
return false if message.none? && !@update_every_frame
|
|
314
316
|
|
|
315
317
|
# Handle sync events: wait for pending async work before continuing
|
|
316
318
|
if message.sync?
|
data/lib/rooibos/shortcuts.rb
CHANGED
|
@@ -68,6 +68,14 @@ module Rooibos
|
|
|
68
68
|
# Alias for +Message::Timer+.
|
|
69
69
|
Timer = Message::Timer
|
|
70
70
|
|
|
71
|
+
# Clock message type.
|
|
72
|
+
# Alias for +Message::Clock+.
|
|
73
|
+
Clock = Message::Clock
|
|
74
|
+
|
|
75
|
+
# Random value message type.
|
|
76
|
+
# Alias for +Message::Random+.
|
|
77
|
+
Rand = Message::Random
|
|
78
|
+
|
|
71
79
|
# HTTP response message type.
|
|
72
80
|
# Alias for +Message::HttpResponse+.
|
|
73
81
|
Http = Message::HttpResponse
|
data/lib/rooibos/version.rb
CHANGED
data/sig/rooibos/command.rbs
CHANGED
|
@@ -134,19 +134,72 @@ module Rooibos
|
|
|
134
134
|
# Alias for wait, semantically used for recurring timers.
|
|
135
135
|
def self.tick: (Float seconds, Symbol tag) -> Wait
|
|
136
136
|
|
|
137
|
+
# Methods expected from classes that include Timed.
|
|
138
|
+
interface _TimedBase
|
|
139
|
+
def seconds: () -> Float
|
|
140
|
+
def timed_response: (Time start_time) -> (Message::Timer | Message::Clock)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Shared behavior for commands that wait, then respond.
|
|
144
|
+
module Timed : _TimedBase
|
|
145
|
+
# Cooperative cancellation needs no grace period.
|
|
146
|
+
def rooibos_cancellation_grace_period: () -> Integer
|
|
147
|
+
|
|
148
|
+
# Waits then sends the result of timed_response, or Message::Canceled.
|
|
149
|
+
def call: (Outlet out, Concurrent::Cancellation token) -> void
|
|
150
|
+
end
|
|
151
|
+
|
|
137
152
|
# One-shot timer command that waits, then sends a message.
|
|
138
153
|
class Wait < Data
|
|
139
154
|
include Custom
|
|
155
|
+
include Timed
|
|
156
|
+
|
|
157
|
+
attr_reader seconds: Float
|
|
158
|
+
attr_reader envelope: Symbol
|
|
159
|
+
|
|
160
|
+
def self.new: (seconds: Float, envelope: Symbol) -> instance
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# Builds the timer response with elapsed time.
|
|
165
|
+
def timed_response: (Time start_time) -> Message::Timer
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# One-shot clock command that waits, then sends a message with wall-clock time.
|
|
169
|
+
class Clock < Data
|
|
170
|
+
include Custom
|
|
171
|
+
include Timed
|
|
140
172
|
|
|
141
173
|
attr_reader seconds: Float
|
|
142
174
|
attr_reader envelope: Symbol
|
|
143
175
|
|
|
144
176
|
def self.new: (seconds: Float, envelope: Symbol) -> instance
|
|
145
177
|
|
|
146
|
-
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Builds the clock response with wall-clock time.
|
|
181
|
+
def timed_response: (Time start_time) -> Message::Clock
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Creates a wall-clock time command.
|
|
185
|
+
def self.clock: (Float seconds, Symbol envelope) -> Clock
|
|
186
|
+
|
|
187
|
+
# A command that generates a random value.
|
|
188
|
+
class Random < Data
|
|
189
|
+
include Custom
|
|
190
|
+
|
|
191
|
+
attr_reader args: Array[(Symbol | Integer | Float | Range[Integer] | Range[Float])]
|
|
192
|
+
attr_reader envelope: Symbol
|
|
193
|
+
|
|
194
|
+
def self.new: (args: Array[(Symbol | Integer | Float | Range[Integer] | Range[Float])], envelope: Symbol) -> instance
|
|
195
|
+
|
|
196
|
+
# Execute the random command.
|
|
147
197
|
def call: (Outlet out, Concurrent::Cancellation token) -> void
|
|
148
198
|
end
|
|
149
199
|
|
|
200
|
+
# Creates a random value command.
|
|
201
|
+
def self.random: (*(Symbol | Integer | Float | Range[Integer] | Range[Float])) -> Random
|
|
202
|
+
|
|
150
203
|
# Private wrapper for Command.custom.
|
|
151
204
|
class Wrapped < Data
|
|
152
205
|
include Custom
|
data/sig/rooibos/message.rbs
CHANGED
|
@@ -89,6 +89,28 @@ module Rooibos
|
|
|
89
89
|
def deconstruct_keys: (Array[Symbol]? keys) -> Hash[Symbol, untyped]
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Response from a clock command.
|
|
93
|
+
class Clock < Data
|
|
94
|
+
include Predicates
|
|
95
|
+
attr_reader envelope: Symbol
|
|
96
|
+
attr_reader time: Time
|
|
97
|
+
|
|
98
|
+
def self.new: (envelope: Symbol, time: Time) -> instance
|
|
99
|
+
|
|
100
|
+
def clock?: () -> bool
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Response from a random command.
|
|
104
|
+
class Random < Data
|
|
105
|
+
include Predicates
|
|
106
|
+
attr_reader envelope: Symbol
|
|
107
|
+
attr_reader value: (Integer | Float | String)
|
|
108
|
+
|
|
109
|
+
def self.new: (envelope: Symbol, value: (Integer | Float | String)) -> instance
|
|
110
|
+
|
|
111
|
+
def random?: () -> bool
|
|
112
|
+
end
|
|
113
|
+
|
|
92
114
|
# Response from an HTTP command.
|
|
93
115
|
class HttpResponse
|
|
94
116
|
include Predicates
|
|
@@ -17,13 +17,13 @@ module Rooibos
|
|
|
17
17
|
def add_instances_of: (Class klass, ?to: route_target?, ?as: Symbol?, ?broadcast: bool, ?broadcast_to: Array[route_target]?) -> void
|
|
18
18
|
|
|
19
19
|
# Adds a forward rule matching key events.
|
|
20
|
-
def add_events: (Symbol | Array[Symbol] keys, ?to: route_target?, ?as: Symbol?, ?guard: guard?, ?when: guard?, ?unless: guard?) -> void
|
|
20
|
+
def add_events: (Symbol | Array[Symbol] keys, ?to: route_target?, ?as: Symbol?, ?broadcast: bool, ?broadcast_to: Array[route_target]?, ?guard: guard?, ?when: guard?, ?unless: guard?) -> void
|
|
21
21
|
|
|
22
22
|
# Adds a forward rule matching routed messages by envelope.
|
|
23
|
-
def add_routed: (Symbol | Array[Symbol] envelopes, ?to: route_target?, ?as: Symbol?) -> void
|
|
23
|
+
def add_routed: (Symbol | Array[Symbol] envelopes, ?to: route_target?, ?as: Symbol?, ?broadcast: bool, ?broadcast_to: Array[route_target]?) -> void
|
|
24
24
|
|
|
25
25
|
# Adds a forward rule matching a custom predicate.
|
|
26
|
-
def add_custom: (guard predicate, ?to: route_target?, ?as: Symbol?) -> void
|
|
26
|
+
def add_custom: (guard predicate, ?to: route_target?, ?as: Symbol?, ?broadcast: bool, ?broadcast_to: Array[route_target]?) -> void
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
data/sig/rooibos/runtime.rbs
CHANGED
|
@@ -30,7 +30,8 @@ module Rooibos
|
|
|
30
30
|
# Starts the MVU event loop (positional fragment).
|
|
31
31
|
def self.run: [Model] (
|
|
32
32
|
?Module? root_fragment,
|
|
33
|
-
?fps: Integer
|
|
33
|
+
?fps: Integer,
|
|
34
|
+
?update_every_frame: bool
|
|
34
35
|
) -> Model
|
|
35
36
|
|
|
36
37
|
# Starts the MVU event loop (explicit parameters).
|
|
@@ -40,7 +41,8 @@ module Rooibos
|
|
|
40
41
|
model: Model,
|
|
41
42
|
view: ^(Model, RatatuiRuby::TUI) -> renderable,
|
|
42
43
|
update: ^(RatatuiRuby::Event, Model) -> update_result?,
|
|
43
|
-
?command: Command::execution
|
|
44
|
+
?command: Command::execution?,
|
|
45
|
+
?update_every_frame: bool
|
|
44
46
|
) -> Model
|
|
45
47
|
|
|
46
48
|
# Starts the MVU event loop (explicit parameters without fps).
|
|
@@ -49,7 +51,8 @@ module Rooibos
|
|
|
49
51
|
model: Model,
|
|
50
52
|
view: ^(Model, RatatuiRuby::TUI) -> renderable,
|
|
51
53
|
update: ^(RatatuiRuby::Event, Model) -> update_result?,
|
|
52
|
-
?command: Command::execution
|
|
54
|
+
?command: Command::execution?,
|
|
55
|
+
?update_every_frame: bool
|
|
53
56
|
) -> Model
|
|
54
57
|
|
|
55
58
|
# Normalizes Init callable return value to [model, command] tuple.
|
data/sig/rooibos/shortcuts.rbs
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rooibos
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
18
|
+
version: '1.5'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.
|
|
25
|
+
version: '1.5'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: concurrent-ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -306,12 +306,15 @@ files:
|
|
|
306
306
|
- lib/rooibos/command/all.rb
|
|
307
307
|
- lib/rooibos/command/batch.rb
|
|
308
308
|
- lib/rooibos/command/bubble.rb
|
|
309
|
+
- lib/rooibos/command/clock.rb
|
|
309
310
|
- lib/rooibos/command/custom.rb
|
|
310
311
|
- lib/rooibos/command/deliver.rb
|
|
311
312
|
- lib/rooibos/command/http.rb
|
|
312
313
|
- lib/rooibos/command/lifecycle.rb
|
|
313
314
|
- lib/rooibos/command/open.rb
|
|
314
315
|
- lib/rooibos/command/outlet.rb
|
|
316
|
+
- lib/rooibos/command/random.rb
|
|
317
|
+
- lib/rooibos/command/timed.rb
|
|
315
318
|
- lib/rooibos/command/wait.rb
|
|
316
319
|
- lib/rooibos/configuration.rb
|
|
317
320
|
- lib/rooibos/error.rb
|
|
@@ -320,9 +323,11 @@ files:
|
|
|
320
323
|
- lib/rooibos/message/batch.rb
|
|
321
324
|
- lib/rooibos/message/bubbled.rb
|
|
322
325
|
- lib/rooibos/message/canceled.rb
|
|
326
|
+
- lib/rooibos/message/clock.rb
|
|
323
327
|
- lib/rooibos/message/error.rb
|
|
324
328
|
- lib/rooibos/message/http_response.rb
|
|
325
329
|
- lib/rooibos/message/open.rb
|
|
330
|
+
- lib/rooibos/message/random.rb
|
|
326
331
|
- lib/rooibos/message/routed.rb
|
|
327
332
|
- lib/rooibos/message/system/batch.rb
|
|
328
333
|
- lib/rooibos/message/system/stream.rb
|
|
@@ -412,7 +417,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
412
417
|
requirements:
|
|
413
418
|
- - ">="
|
|
414
419
|
- !ruby/object:Gem::Version
|
|
415
|
-
version: 3.
|
|
420
|
+
version: 3.3.11
|
|
416
421
|
- - "<"
|
|
417
422
|
- !ruby/object:Gem::Version
|
|
418
423
|
version: '5'
|
|
@@ -422,7 +427,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
422
427
|
- !ruby/object:Gem::Version
|
|
423
428
|
version: '0'
|
|
424
429
|
requirements: []
|
|
425
|
-
rubygems_version: 4.0.
|
|
430
|
+
rubygems_version: 4.0.6
|
|
426
431
|
specification_version: 4
|
|
427
432
|
summary: "☕ Confidently Build Terminal Apps"
|
|
428
433
|
test_files: []
|