solid_objects 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +69 -13
  4. data/benchmark/idle_polling.rb +98 -0
  5. data/benchmark/support.rb +2 -0
  6. data/docs/adr/0009-realtime-updates.md +5 -1
  7. data/docs/adr/0011-wake-up-strategy.md +6 -0
  8. data/docs/architecture.md +5 -4
  9. data/docs/authorization.md +11 -4
  10. data/docs/benchmarks.md +38 -0
  11. data/docs/correctness.md +3 -3
  12. data/docs/dashboard.md +200 -0
  13. data/docs/database-schema.md +5 -4
  14. data/docs/development.md +1 -0
  15. data/docs/operations.md +24 -0
  16. data/docs/realtime.md +22 -14
  17. data/docs/roadmap.md +32 -11
  18. data/docs/security.md +7 -7
  19. data/examples/application/README.md +5 -5
  20. data/examples/application/app/actors/chat_room_actor.rb +1 -1
  21. data/examples/application/app/actors/shopping_cart_actor.rb +2 -2
  22. data/lib/solid_objects/actor.rb +1 -1
  23. data/lib/solid_objects/broadcast_executor.rb +35 -4
  24. data/lib/solid_objects/configuration.rb +4 -0
  25. data/lib/solid_objects/effect_executor.rb +34 -3
  26. data/lib/solid_objects/polling_backoff.rb +45 -0
  27. data/lib/solid_objects/process_registry.rb +51 -0
  28. data/lib/solid_objects/reminder_scheduler.rb +35 -4
  29. data/lib/solid_objects/version.rb +1 -1
  30. data/lib/solid_objects/wake_up.rb +36 -4
  31. data/lib/solid_objects/wake_up_adapters/postgresql.rb +6 -0
  32. data/lib/solid_objects/wake_up_adapters/redis.rb +25 -3
  33. data/lib/solid_objects/web/action.rb +109 -0
  34. data/lib/solid_objects/web/application.rb +239 -0
  35. data/lib/solid_objects/web/csrf_protection.rb +130 -0
  36. data/lib/solid_objects/web/helpers.rb +227 -0
  37. data/lib/solid_objects/web/paginator.rb +64 -0
  38. data/lib/solid_objects/web/route.rb +55 -0
  39. data/lib/solid_objects/web/router.rb +46 -0
  40. data/lib/solid_objects/web/statistics.rb +78 -0
  41. data/lib/solid_objects/web.rb +222 -0
  42. data/lib/solid_objects/worker.rb +39 -3
  43. data/lib/solid_objects.rb +2 -0
  44. data/sig/generated/lib/solid_objects/broadcast_executor.rbs +7 -0
  45. data/sig/generated/lib/solid_objects/configuration.rbs +6 -2
  46. data/sig/generated/lib/solid_objects/effect_executor.rbs +7 -0
  47. data/sig/generated/lib/solid_objects/polling_backoff.rbs +29 -0
  48. data/sig/generated/lib/solid_objects/process_registry.rbs +15 -0
  49. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +7 -0
  50. data/sig/generated/lib/solid_objects/wake_up.rbs +19 -2
  51. data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +3 -0
  52. data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +17 -2
  53. data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
  54. data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
  55. data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
  56. data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
  57. data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
  58. data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
  59. data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
  60. data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
  61. data/sig/generated/lib/solid_objects/web.rbs +129 -0
  62. data/sig/generated/lib/solid_objects/worker.rbs +7 -0
  63. data/web/assets/javascripts/application.js +118 -0
  64. data/web/assets/javascripts/charts.js +190 -0
  65. data/web/assets/stylesheets/application.css +527 -0
  66. data/web/views/_messages.erb +29 -0
  67. data/web/views/_navigation.erb +15 -0
  68. data/web/views/_paging.erb +17 -0
  69. data/web/views/_status_filter.erb +8 -0
  70. data/web/views/_summary.erb +39 -0
  71. data/web/views/broadcasts.erb +35 -0
  72. data/web/views/dashboard.erb +128 -0
  73. data/web/views/dead_letter.erb +40 -0
  74. data/web/views/dead_letters.erb +42 -0
  75. data/web/views/effects.erb +35 -0
  76. data/web/views/instance.erb +139 -0
  77. data/web/views/instances.erb +49 -0
  78. data/web/views/layout.erb +22 -0
  79. data/web/views/mailbox.erb +35 -0
  80. data/web/views/message.erb +34 -0
  81. data/web/views/processes.erb +37 -0
  82. data/web/views/reminders.erb +37 -0
  83. metadata +58 -2
@@ -0,0 +1,222 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "erb"
4
+ require "json"
5
+ require "securerandom"
6
+ require "uri"
7
+ require "rack"
8
+ require "rack/builder"
9
+ require "rack/static"
10
+
11
+ require "solid_objects"
12
+ require "solid_objects/web/route"
13
+ require "solid_objects/web/router"
14
+ require "solid_objects/web/statistics"
15
+ require "solid_objects/web/paginator"
16
+ require "solid_objects/web/helpers"
17
+ require "solid_objects/web/action"
18
+ require "solid_objects/web/csrf_protection"
19
+ require "solid_objects/web/application"
20
+
21
+ module SolidObjects
22
+ # An operator dashboard for the actor runtime, served as a Rack application:
23
+ #
24
+ # Rails.application.routes.draw do
25
+ # mount SolidObjects::Web => "/solid_objects"
26
+ # end
27
+ #
28
+ # It is deliberately not loaded by `require "solid_objects"`. A worker
29
+ # process must not carry a web stack, and an application that never mounts
30
+ # the dashboard must not pay for it.
31
+ #
32
+ # Every page asks `configuration.authorize_administration` first. That block
33
+ # denies by default, so a mount alone exposes nothing until an application
34
+ # states who may read it.
35
+ class Web
36
+ ROOT = File.expand_path("../../web", __dir__)
37
+ VIEWS = File.join(ROOT, "views")
38
+ ASSETS = File.join(ROOT, "assets")
39
+
40
+ NONCE_KEY = "solid_objects.content_security_policy_nonce"
41
+ CSRF_TOKEN_KEY = "solid_objects.csrf_token"
42
+ NONCE_BYTES = 16
43
+ ASSET_CACHE_SECONDS = 86_400
44
+ TEMPLATE_NAME = /\A_?[a-z][a-z0-9_]*\z/
45
+
46
+ # The dashboard charts need a charting library, and this one is fetched
47
+ # from a public CDN with a subresource integrity hash, so a compromised CDN
48
+ # cannot substitute other code. A deployment with no outbound network
49
+ # access should vendor the file and point `chart_library_url` at it, or set
50
+ # that to nil to render the dashboard without charts.
51
+ CHART_LIBRARY_URL = "https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"
52
+ CHART_LIBRARY_INTEGRITY = "sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"
53
+
54
+ DEFAULT_TABS = {
55
+ "Dashboard" => "/",
56
+ "Instances" => "/instances",
57
+ "Mailbox" => "/mailbox",
58
+ "Reminders" => "/reminders",
59
+ "Effects" => "/effects",
60
+ "Broadcasts" => "/broadcasts",
61
+ "Dead letters" => "/dead_letters",
62
+ "Processes" => "/processes"
63
+ }.freeze
64
+
65
+ LOCK = Mutex.new
66
+
67
+ class << self
68
+ # A path below the mount serves a vendored copy; an absolute URL is
69
+ # fetched from that host and is named in the content security policy.
70
+ # nil renders the dashboard without charts.
71
+ # @rbs @chart_library_url: String?
72
+ # @rbs @chart_library_integrity: String?
73
+ attr_writer :chart_library_url, :chart_library_integrity
74
+
75
+ # @rbs () -> String?
76
+ def chart_library_url
77
+ defined?(@chart_library_url) ? @chart_library_url : CHART_LIBRARY_URL
78
+ end
79
+
80
+ # @rbs () -> String?
81
+ def chart_library_integrity
82
+ defined?(@chart_library_integrity) ? @chart_library_integrity : CHART_LIBRARY_INTEGRITY
83
+ end
84
+
85
+ # @rbs () -> bool
86
+ def charts?
87
+ !chart_library_url.nil?
88
+ end
89
+
90
+ # The origin the content security policy has to allow. A vendored copy
91
+ # served from the mount has none, so the policy stays at 'self'.
92
+ # @rbs () -> String?
93
+ def chart_library_origin
94
+ url = chart_library_url
95
+ return nil unless url&.include?("//")
96
+
97
+ uri = URI.parse(url)
98
+ return nil unless uri.scheme && uri.host
99
+
100
+ "#{uri.scheme}://#{uri.host}"
101
+ rescue URI::InvalidURIError
102
+ nil
103
+ end
104
+
105
+ # @rbs () -> Hash[String, String]
106
+ def tabs
107
+ @tabs ||= DEFAULT_TABS.dup
108
+ end
109
+
110
+ # Searched in order, so an extension directory added first wins over the
111
+ # packaged one and an application can replace a single page.
112
+ # @rbs () -> Array[String]
113
+ def views
114
+ @views ||= [ VIEWS ]
115
+ end
116
+
117
+ # @rbs () -> Array[[Array[untyped], Proc?]]
118
+ def middlewares
119
+ @middlewares ||= []
120
+ end
121
+
122
+ # The built stack is memoized, so a middleware added after the first
123
+ # request would otherwise be dropped without a word.
124
+ # @rbs (*untyped) ?{ () -> untyped } -> void
125
+ def use(*arguments, &block)
126
+ middlewares << [ arguments, block ]
127
+ LOCK.synchronize { @application = nil }
128
+ end
129
+
130
+ # Adds pages to the dashboard. The extension receives the application
131
+ # class and declares its own routes on it, which means its routes carry
132
+ # an authorization policy like every other route.
133
+ #
134
+ # @rbs (untyped, tab: String, path: String, ?views: String?) -> void
135
+ def register(extension, tab:, path:, views: nil)
136
+ if views
137
+ self.views.unshift(views)
138
+ # A template compiled before this call resolved against the old
139
+ # search path, so a replacement view would never be reached.
140
+ LOCK.synchronize { @templates = {} }
141
+ end
142
+ tabs[tab] = path
143
+ extension.registered(Application)
144
+ end
145
+
146
+ # @rbs (Symbol) -> untyped
147
+ def template(name)
148
+ LOCK.synchronize do
149
+ templates[name] ||= ERB.new(File.read(template_path(name)), trim_mode: "-")
150
+ end
151
+ end
152
+
153
+ # @rbs () -> void
154
+ def reset!
155
+ LOCK.synchronize { @templates = {} }
156
+ @tabs = nil
157
+ @views = nil
158
+ @middlewares = nil
159
+ @application = nil
160
+ remove_instance_variable(:@chart_library_url) if defined?(@chart_library_url)
161
+ remove_instance_variable(:@chart_library_integrity) if defined?(@chart_library_integrity)
162
+ end
163
+
164
+ # @rbs (Hash[String, untyped]) -> Array[untyped]
165
+ def call(env)
166
+ application.call(env)
167
+ end
168
+
169
+ # @rbs () -> Web
170
+ def application
171
+ LOCK.synchronize { @application ||= new }
172
+ end
173
+
174
+ private
175
+
176
+ # @rbs () -> Hash[Symbol, untyped]
177
+ def templates
178
+ @templates ||= {}
179
+ end
180
+
181
+ # A template name never comes from a request, and this keeps it that way
182
+ # rather than trusting every future caller to know it.
183
+ # @rbs (Symbol) -> String
184
+ def template_path(name)
185
+ raise ArgumentError, "invalid template name" unless name.to_s.match?(TEMPLATE_NAME)
186
+
187
+ found = views.lazy.map { |directory| File.join(directory, "#{name}.erb") }.find { |path| File.exist?(path) }
188
+ found || raise(ArgumentError, "no template named #{name}")
189
+ end
190
+ end
191
+
192
+ # @rbs (Hash[String, untyped]) -> Array[untyped]
193
+ def call(env)
194
+ env[NONCE_KEY] = SecureRandom.base64(NONCE_BYTES)
195
+ app.call(env)
196
+ end
197
+
198
+ # @rbs () -> untyped
199
+ def app
200
+ @app ||= build
201
+ end
202
+
203
+ private
204
+
205
+ # @rbs () -> untyped
206
+ def build
207
+ assets = ASSETS
208
+ extra = self.class.middlewares
209
+
210
+ ::Rack::Builder.new do
211
+ use ::Rack::Static,
212
+ urls: [ "/stylesheets", "/javascripts" ],
213
+ root: assets,
214
+ cascade: true,
215
+ header_rules: [ [ :all, { "cache-control" => "private, max-age=#{ASSET_CACHE_SECONDS}" } ] ]
216
+ extra.each { |arguments, block| use(*arguments, &block) }
217
+ use CsrfProtection
218
+ run Application.new
219
+ end
220
+ end
221
+ end
222
+ end
@@ -5,6 +5,7 @@ require "solid_objects/activation_manager"
5
5
  require "solid_objects/activation"
6
6
  require "solid_objects/executor"
7
7
  require "solid_objects/lease_renewer"
8
+ require "solid_objects/polling_backoff"
8
9
 
9
10
  module SolidObjects
10
11
  class Worker
@@ -13,6 +14,7 @@ module SolidObjects
13
14
  # @rbs @activations: Hash[Integer, Activation]
14
15
  # @rbs @stopped: bool
15
16
  # @rbs @shutdown_requested: bool
17
+ # @rbs @polling_backoff: PollingBackoff
16
18
 
17
19
  # @rbs (?process_registry: ProcessRegistry) -> void
18
20
  def initialize(process_registry: ProcessRegistry.new)
@@ -22,6 +24,23 @@ module SolidObjects
22
24
  @activations = {}
23
25
  @stopped = false
24
26
  @shutdown_requested = false
27
+ @polling_backoff = PollingBackoff.new(
28
+ minimum_interval: [
29
+ SolidObjects.configuration.polling_interval,
30
+ SolidObjects.configuration.lease_renewal_interval
31
+ ].min,
32
+ maximum_interval: [
33
+ SolidObjects.configuration.idle_polling_interval,
34
+ SolidObjects.configuration.lease_renewal_interval
35
+ ].min,
36
+ on_change: ->(transition) do
37
+ SolidObjects.instrument(
38
+ :"polling.interval_changed",
39
+ role: "actors",
40
+ **transition
41
+ )
42
+ end
43
+ )
25
44
  end
26
45
 
27
46
  # @rbs () -> Integer
@@ -70,11 +89,23 @@ module SolidObjects
70
89
 
71
90
  # @rbs () -> void
72
91
  def run
92
+ ProcessRegistry.warn_if_polling_is_only_cross_process_wake_up
93
+
73
94
  until shutdown_requested?
95
+ wake_up = SolidObjects.wake_up
96
+ watch = wake_up.respond_to?(:watch) ? wake_up.watch : wake_up
74
97
  processed = run_once
75
- next if processed.positive?
98
+ if processed.positive?
99
+ polling_backoff.reset(:work)
100
+ next
101
+ end
76
102
 
77
- SolidObjects.wake_up.wait(timeout: SolidObjects.configuration.polling_interval)
103
+ notified = watch.wait(timeout: current_polling_interval)
104
+ if notified == false
105
+ polling_backoff.record_idle
106
+ else
107
+ polling_backoff.reset(:wake_up)
108
+ end
78
109
  end
79
110
  ensure
80
111
  stop
@@ -107,9 +138,14 @@ module SolidObjects
107
138
  @shutdown_requested
108
139
  end
109
140
 
141
+ # @rbs () -> Float
142
+ def current_polling_interval
143
+ polling_backoff.current_interval
144
+ end
145
+
110
146
  private
111
147
 
112
- attr_reader :process_registry, :activation_manager, :activations
148
+ attr_reader :process_registry, :activation_manager, :activations, :polling_backoff
113
149
 
114
150
  # @rbs () -> Activation?
115
151
  def cached_ready_activation
data/lib/solid_objects.rb CHANGED
@@ -52,6 +52,7 @@ require "solid_objects/wake_up"
52
52
  require "solid_objects/wake_up_adapters/postgresql"
53
53
  require "solid_objects/wake_up_adapters/redis"
54
54
  require "solid_objects/wake_up_adapters"
55
+ require "solid_objects/polling_backoff"
55
56
  require "solid_objects/effect_registry"
56
57
  require "solid_objects/commit_action_registry"
57
58
  require "solid_objects/lease"
@@ -153,6 +154,7 @@ module SolidObjects
153
154
 
154
155
  # @rbs () -> void
155
156
  def reset!
157
+ ProcessRegistry.reset_polling_warning! if defined?(ProcessRegistry)
156
158
  @configuration = Configuration.new
157
159
  @registry = ActorRegistry.new
158
160
  @client = nil
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class BroadcastExecutor
5
+ @polling_backoff: PollingBackoff
6
+
5
7
  @shutdown_requested: bool
6
8
 
7
9
  @stopped: bool
@@ -31,12 +33,17 @@ module SolidObjects
31
33
  # @rbs () -> bool
32
34
  def shutdown_requested?: () -> bool
33
35
 
36
+ # @rbs () -> Float
37
+ def current_polling_interval: () -> Float
38
+
34
39
  private
35
40
 
36
41
  attr_reader process_registry: untyped
37
42
 
38
43
  attr_reader database_adapter: untyped
39
44
 
45
+ attr_reader polling_backoff: untyped
46
+
40
47
  # @rbs () -> Broadcast?
41
48
  def claim_next: () -> Broadcast?
42
49
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Configuration
5
+ @table_name_prefix: String
6
+
5
7
  @process_alive_threshold: Float
6
8
 
7
9
  @shutdown_timeout: Float
@@ -56,10 +58,10 @@ module SolidObjects
56
58
 
57
59
  @authorize_administration: Proc
58
60
 
59
- @table_name_prefix: String
60
-
61
61
  @polling_interval: Float
62
62
 
63
+ @idle_polling_interval: Float
64
+
63
65
  @sync_polling_interval: Float
64
66
 
65
67
  @lease_duration: Float
@@ -94,6 +96,8 @@ module SolidObjects
94
96
 
95
97
  attr_accessor polling_interval: untyped
96
98
 
99
+ attr_accessor idle_polling_interval: untyped
100
+
97
101
  attr_accessor sync_polling_interval: untyped
98
102
 
99
103
  attr_accessor lease_duration: untyped
@@ -23,6 +23,8 @@ module SolidObjects
23
23
  class EffectExecutor
24
24
  ACTOR_MESSAGE_EFFECT: ::String
25
25
 
26
+ @polling_backoff: PollingBackoff
27
+
26
28
  @shutdown_requested: bool
27
29
 
28
30
  @stopped: bool
@@ -52,12 +54,17 @@ module SolidObjects
52
54
  # @rbs () -> bool
53
55
  def shutdown_requested?: () -> bool
54
56
 
57
+ # @rbs () -> Float
58
+ def current_polling_interval: () -> Float
59
+
55
60
  private
56
61
 
57
62
  attr_reader process_registry: untyped
58
63
 
59
64
  attr_reader database_adapter: untyped
60
65
 
66
+ attr_reader polling_backoff: untyped
67
+
61
68
  # @rbs () -> Effect?
62
69
  def claim_next: () -> Effect?
63
70
 
@@ -0,0 +1,29 @@
1
+ # Generated from lib/solid_objects/polling_backoff.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class PollingBackoff
5
+ @minimum_interval: Float
6
+
7
+ @maximum_interval: Float
8
+
9
+ @current_interval: Float
10
+
11
+ @on_change: Proc?
12
+
13
+ attr_reader current_interval: untyped
14
+
15
+ # @rbs (minimum_interval: Numeric, maximum_interval: Numeric, ?on_change: Proc?) -> void
16
+ def initialize: (minimum_interval: Numeric, maximum_interval: Numeric, ?on_change: Proc?) -> void
17
+
18
+ # @rbs () -> void
19
+ def record_idle: () -> void
20
+
21
+ # @rbs (:work | :wake_up) -> void
22
+ def reset: (:work | :wake_up) -> void
23
+
24
+ private
25
+
26
+ # @rbs (Float, :idle | :work | :wake_up) -> void
27
+ def change: (Float, :idle | :work | :wake_up) -> void
28
+ end
29
+ end
@@ -8,6 +8,21 @@ module SolidObjects
8
8
  # @rbs (Process, ?now: Time) -> bool
9
9
  def self.deregister: (Process, ?now: Time) -> bool
10
10
 
11
+ # @rbs () -> void
12
+ def self.warn_if_polling_is_only_cross_process_wake_up: () -> void
13
+
14
+ # @rbs () -> void
15
+ def self.reset_polling_warning!: () -> void
16
+
17
+ # @rbs () -> bool
18
+ private def self.another_live_process?: () -> bool
19
+
20
+ # @rbs () -> bool
21
+ private def self.polling_warning_emitted?: () -> bool
22
+
23
+ # @rbs () -> Mutex
24
+ private def self.polling_warning_mutex: () -> Mutex
25
+
11
26
  # @rbs (Process, Time) -> void
12
27
  private def self.cleanup_process: (Process, Time) -> void
13
28
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class ReminderScheduler
5
+ @polling_backoff: PollingBackoff
6
+
5
7
  @shutdown_requested: bool
6
8
 
7
9
  @stopped: bool
@@ -31,12 +33,17 @@ module SolidObjects
31
33
  # @rbs () -> bool
32
34
  def shutdown_requested?: () -> bool
33
35
 
36
+ # @rbs () -> Float
37
+ def current_polling_interval: () -> Float
38
+
34
39
  private
35
40
 
36
41
  attr_reader process_registry: untyped
37
42
 
38
43
  attr_reader database_adapter: untyped
39
44
 
45
+ attr_reader polling_backoff: untyped
46
+
40
47
  # @rbs (now: Time?) -> Reminder?
41
48
  def claim_next: (now: Time?) -> Reminder?
42
49
 
@@ -2,18 +2,35 @@
2
2
 
3
3
  module SolidObjects
4
4
  class WakeUp
5
+ class Watch
6
+ @wake_up: WakeUp
7
+
8
+ @generation: Integer
9
+
10
+ # @rbs (WakeUp, Integer) -> void
11
+ def initialize: (WakeUp, Integer) -> void
12
+
13
+ # @rbs (timeout: Numeric) -> bool
14
+ def wait: (timeout: Numeric) -> bool
15
+ end
16
+
5
17
  @mutex: Mutex
6
18
 
7
19
  @condition: Thread::ConditionVariable
8
20
 
21
+ @generation: Integer
22
+
9
23
  # @rbs () -> void
10
24
  def initialize: () -> void
11
25
 
12
26
  # @rbs () -> void
13
27
  def signal: () -> void
14
28
 
15
- # @rbs (timeout: Numeric) -> void
16
- def wait: (timeout: Numeric) -> void
29
+ # @rbs () -> Watch
30
+ def watch: () -> Watch
31
+
32
+ # @rbs (timeout: Numeric, ?generation: Integer?) -> bool
33
+ def wait: (timeout: Numeric, ?generation: Integer?) -> bool
17
34
 
18
35
  private
19
36
 
@@ -31,6 +31,9 @@ module SolidObjects
31
31
  # @rbs (timeout: Numeric) -> bool
32
32
  def wait: (timeout: Numeric) -> bool
33
33
 
34
+ # @rbs () -> self
35
+ def watch: () -> self
36
+
34
37
  # Starts listening before a caller blocks, so a notification sent between
35
38
  # startup and the first wait is not missed.
36
39
  # @rbs () -> bool
@@ -10,6 +10,18 @@ module SolidObjects
10
10
  # polling interval remains the upper bound, so a missed or failed
11
11
  # notification costs latency rather than correctness.
12
12
  class Redis
13
+ class Watch
14
+ @adapter: Redis
15
+
16
+ @generation: Integer
17
+
18
+ # @rbs (Redis, Integer) -> void
19
+ def initialize: (Redis, Integer) -> void
20
+
21
+ # @rbs (timeout: Numeric) -> bool
22
+ def wait: (timeout: Numeric) -> bool
23
+ end
24
+
13
25
  CHANNEL: ::String
14
26
 
15
27
  FAILED_WAIT_INTERVAL: ::Float
@@ -43,8 +55,11 @@ module SolidObjects
43
55
  # The counter is snapshotted before subscribing, and re-checked before
44
56
  # blocking, so a signal delivered while this caller was still getting
45
57
  # ready is observed rather than absorbed into the new baseline.
46
- # @rbs (timeout: Numeric) -> bool
47
- def wait: (timeout: Numeric) -> bool
58
+ # @rbs (timeout: Numeric, ?generation: Integer?) -> bool
59
+ def wait: (timeout: Numeric, ?generation: Integer?) -> bool
60
+
61
+ # @rbs () -> Watch
62
+ def watch: () -> Watch
48
63
 
49
64
  # Redis delivers to a subscribed connection only, and a subscribed
50
65
  # connection cannot serve other callers, so one background subscription
@@ -0,0 +1,78 @@
1
+ # Generated from lib/solid_objects/web/action.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class Web
5
+ # One request. A route handler runs inside an instance of this class, so a
6
+ # handler and the template it renders share the same helpers and the same
7
+ # request.
8
+ class Action
9
+ include Helpers
10
+
11
+ @response_status: Integer
12
+
13
+ @layout_rendered: bool
14
+
15
+ @request: untyped
16
+
17
+ @captures: Hash[Symbol, String?]
18
+
19
+ @route: Route
20
+
21
+ @env: Hash[String, untyped]
22
+
23
+ attr_reader env: untyped
24
+
25
+ attr_reader route: untyped
26
+
27
+ attr_reader response_status: untyped
28
+
29
+ # @rbs (env: Hash[String, untyped], route: Route) -> void
30
+ def initialize: (env: Hash[String, untyped], route: Route) -> void
31
+
32
+ # Sets the status a rendered page is served with. `halt` and `redirect`
33
+ # stop the handler, so a page that must render its own body and still
34
+ # report a failure needs this instead.
35
+ # @rbs (Integer) -> void
36
+ def status: (Integer) -> void
37
+
38
+ # @rbs () -> untyped
39
+ def request: () -> untyped
40
+
41
+ # @rbs () -> Hash[untyped, untyped]?
42
+ def session: () -> Hash[untyped, untyped]?
43
+
44
+ # @rbs (Symbol) -> String?
45
+ def route_params: (Symbol) -> String?
46
+
47
+ # @rbs (String) -> untyped
48
+ def url_params: (String) -> untyped
49
+
50
+ # @rbs () -> untyped
51
+ def call: () -> untyped
52
+
53
+ # The layout is rendered once per request. The flag is raised before the
54
+ # page body runs so a partial the body renders returns its own fragment
55
+ # rather than a second whole page. The layout reads the page it wraps
56
+ # from `locals.fetch(:content)`.
57
+ # @rbs (Symbol, ?Hash[Symbol, untyped]) -> String
58
+ def erb: (Symbol, ?Hash[Symbol, untyped]) -> String
59
+
60
+ # @rbs (Integer, ?String) -> void
61
+ def halt: (Integer, ?String) -> void
62
+
63
+ # @rbs (String) -> void
64
+ def redirect: (String) -> void
65
+
66
+ # @rbs (untyped) -> void
67
+ def json: (untyped) -> void
68
+
69
+ private
70
+
71
+ # `locals` is a local variable of this method, so a template reads it by
72
+ # name, and every helper is reachable because the template runs against
73
+ # this object.
74
+ # @rbs (untyped, Hash[Symbol, untyped]) -> String
75
+ def evaluate: (untyped, Hash[Symbol, untyped]) -> String
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ # Generated from lib/solid_objects/web/application.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class Web
5
+ # The pages. Each route states the administration policy it needs, and
6
+ # `call` asks that policy before the handler runs, so a page cannot read
7
+ # the actor tables on behalf of an unauthorized request.
8
+ class Application
9
+ extend Router
10
+
11
+ SCRIPT_PLACEHOLDER: ::String
12
+
13
+ CONTENT_SECURITY_POLICY: untyped
14
+
15
+ MAILBOX_MEMBERSHIPS: untyped
16
+
17
+ RECENT_LIMIT: ::Integer
18
+
19
+ CHART_TYPE_LIMIT: ::Integer
20
+
21
+ # @rbs (Hash[String, untyped]) -> Array[untyped]
22
+ def call: (Hash[String, untyped]) -> Array[untyped]
23
+
24
+ private
25
+
26
+ # @rbs (Action, untyped) -> Array[untyped]
27
+ def respond: (Action, untyped) -> Array[untyped]
28
+
29
+ # @rbs (Action) -> bool
30
+ def authorized?: (Action) -> bool
31
+
32
+ # @rbs (Hash[String, untyped]) -> Hash[String, String]
33
+ def page_headers: (Hash[String, untyped]) -> Hash[String, String]
34
+
35
+ # The chart host is named only when one is configured, so a deployment
36
+ # that vendors the library or turns charts off never advertises a third
37
+ # party origin it does not use.
38
+ # @rbs (Hash[String, untyped]) -> String
39
+ def content_security_policy: (Hash[String, untyped]) -> String
40
+
41
+ # The cascade header lets a host application serve its own 404 for a path
42
+ # below the mount that the dashboard does not define.
43
+ # @rbs () -> Array[untyped]
44
+ def not_found: () -> Array[untyped]
45
+
46
+ # @rbs () -> Array[untyped]
47
+ def forbidden: () -> Array[untyped]
48
+ end
49
+ end
50
+ end