solid_objects 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +72 -6
  4. data/app/models/solid_objects/broadcast.rb +8 -0
  5. data/docs/adr/0009-realtime-updates.md +5 -1
  6. data/docs/architecture.md +4 -2
  7. data/docs/authorization.md +11 -4
  8. data/docs/correctness.md +3 -3
  9. data/docs/dashboard.md +200 -0
  10. data/docs/database-schema.md +5 -4
  11. data/docs/development.md +12 -0
  12. data/docs/realtime.md +37 -7
  13. data/docs/roadmap.md +28 -6
  14. data/docs/security.md +8 -0
  15. data/examples/application/README.md +5 -5
  16. data/examples/application/app/actors/chat_room_actor.rb +1 -1
  17. data/examples/application/app/actors/shopping_cart_actor.rb +5 -5
  18. data/lib/solid_objects/actor.rb +6 -5
  19. data/lib/solid_objects/actor_channel.rb +7 -4
  20. data/lib/solid_objects/actor_definition.rb +15 -3
  21. data/lib/solid_objects/actor_view.rb +6 -1
  22. data/lib/solid_objects/effect_executor.rb +10 -2
  23. data/lib/solid_objects/errors.rb +3 -0
  24. data/lib/solid_objects/executor.rb +7 -1
  25. data/lib/solid_objects/reminder_scheduler.rb +28 -14
  26. data/lib/solid_objects/test_helper.rb +16 -0
  27. data/lib/solid_objects/turbo_stream_renderer.rb +3 -3
  28. data/lib/solid_objects/version.rb +1 -1
  29. data/lib/solid_objects/web/action.rb +109 -0
  30. data/lib/solid_objects/web/application.rb +239 -0
  31. data/lib/solid_objects/web/csrf_protection.rb +130 -0
  32. data/lib/solid_objects/web/helpers.rb +227 -0
  33. data/lib/solid_objects/web/paginator.rb +64 -0
  34. data/lib/solid_objects/web/route.rb +55 -0
  35. data/lib/solid_objects/web/router.rb +46 -0
  36. data/lib/solid_objects/web/statistics.rb +78 -0
  37. data/lib/solid_objects/web.rb +222 -0
  38. data/sig/generated/lib/solid_objects/actor.rbs +2 -2
  39. data/sig/generated/lib/solid_objects/actor_definition.rbs +9 -2
  40. data/sig/generated/lib/solid_objects/errors.rbs +3 -0
  41. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +9 -6
  42. data/sig/generated/lib/solid_objects/test_helper.rbs +3 -0
  43. data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
  44. data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
  45. data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
  46. data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
  47. data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
  48. data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
  49. data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
  50. data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
  51. data/sig/generated/lib/solid_objects/web.rbs +129 -0
  52. data/sig/generated/models/solid_objects/broadcast.rbs +2 -0
  53. data/web/assets/javascripts/application.js +118 -0
  54. data/web/assets/javascripts/charts.js +190 -0
  55. data/web/assets/stylesheets/application.css +527 -0
  56. data/web/views/_messages.erb +29 -0
  57. data/web/views/_navigation.erb +15 -0
  58. data/web/views/_paging.erb +17 -0
  59. data/web/views/_status_filter.erb +8 -0
  60. data/web/views/_summary.erb +39 -0
  61. data/web/views/broadcasts.erb +35 -0
  62. data/web/views/dashboard.erb +128 -0
  63. data/web/views/dead_letter.erb +40 -0
  64. data/web/views/dead_letters.erb +42 -0
  65. data/web/views/effects.erb +35 -0
  66. data/web/views/instance.erb +139 -0
  67. data/web/views/instances.erb +49 -0
  68. data/web/views/layout.erb +22 -0
  69. data/web/views/mailbox.erb +35 -0
  70. data/web/views/message.erb +34 -0
  71. data/web/views/processes.erb +37 -0
  72. data/web/views/reminders.erb +37 -0
  73. metadata +55 -2
@@ -0,0 +1,55 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class Web
5
+ class Route
6
+ # A named segment stops at the next slash, so `/instances/:id` never
7
+ # swallows `/instances/1/pause` and route order cannot hide a page.
8
+ NAMED_SEGMENT = %r{/([^/]*):([^.:$/]+)}
9
+ SEGMENT_CAPTURE = '/\1(?<\2>[^$/]+)'
10
+
11
+ # @rbs @matcher: String | Regexp
12
+ # @rbs @request_method: String
13
+ # @rbs @pattern: String
14
+ # @rbs @policy: Hash[Symbol, String]
15
+ # @rbs @handler: Proc
16
+
17
+ attr_reader :request_method, :pattern, :policy, :handler
18
+
19
+ # @rbs (request_method: String, pattern: String, policy: Hash[Symbol, String], handler: Proc) -> void
20
+ def initialize(request_method:, pattern:, policy:, handler:)
21
+ @request_method = request_method
22
+ @pattern = pattern
23
+ @policy = policy
24
+ @handler = handler
25
+ @matcher = compile(pattern)
26
+ end
27
+
28
+ # @rbs (String) -> bool
29
+ def match?(path)
30
+ return @matcher == path if @matcher.is_a?(String)
31
+
32
+ @matcher.match?(path)
33
+ end
34
+
35
+ # @rbs (String) -> Hash[Symbol, String?]
36
+ def capture(path)
37
+ return {} if @matcher.is_a?(String)
38
+
39
+ match = @matcher.match(path)
40
+ return {} unless match
41
+
42
+ match.named_captures.transform_keys(&:to_sym)
43
+ end
44
+
45
+ private
46
+
47
+ # @rbs (String) -> (String | Regexp)
48
+ def compile(pattern)
49
+ return pattern unless pattern.match?(NAMED_SEGMENT)
50
+
51
+ Regexp.new("\\A#{pattern.gsub(NAMED_SEGMENT, SEGMENT_CAPTURE)}\\z")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class Web
5
+ # Declares the pages of the dashboard. Every route carries the
6
+ # administration policy it needs, and a route declared without one raises
7
+ # at load time. A new page therefore cannot reach the database before an
8
+ # application has said who may read it.
9
+ module Router
10
+ # @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
11
+ def head(path, policy: nil, &handler)
12
+ route("HEAD", path, policy:, &handler)
13
+ end
14
+
15
+ # @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
16
+ def get(path, policy: nil, &handler)
17
+ route("GET", path, policy:, &handler)
18
+ end
19
+
20
+ # @rbs (String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
21
+ def post(path, policy: nil, &handler)
22
+ route("POST", path, policy:, &handler)
23
+ end
24
+
25
+ # @rbs (String, String, policy: Hash[Symbol, String]?) { () -> untyped } -> void
26
+ def route(request_method, path, policy: nil, &handler)
27
+ raise ArgumentError, "route #{path} requires an authorization policy" unless policy
28
+ raise ArgumentError, "route #{path} requires a handler" unless handler
29
+ raise ArgumentError, "route #{path} requires a policy action" unless policy[:action]
30
+ raise ArgumentError, "route #{path} requires a policy resource" unless policy[:resource]
31
+
32
+ routes[request_method] << Route.new(request_method:, pattern: path, policy:, handler:)
33
+ end
34
+
35
+ # @rbs () -> Hash[String, Array[Route]]
36
+ def routes
37
+ @routes ||= Hash.new { |store, request_method| store[request_method] = [] }
38
+ end
39
+
40
+ # @rbs (String, String) -> Route?
41
+ def match(request_method, path)
42
+ routes[request_method].find { |route| route.match?(path) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class Web
5
+ # The counts behind the dashboard and behind `GET /stats`. Both read the
6
+ # same object, so the polled JSON and the rendered page can never disagree
7
+ # about what a number means.
8
+ class Statistics
9
+ EFFECT_STATUSES = %w[pending processing completed dead].freeze
10
+ BROADCAST_STATUSES = %w[pending processing delivered dead].freeze
11
+ REMINDER_STATUSES = %w[scheduled paused completed].freeze
12
+ PROCESS_STATES = %w[running draining stopped].freeze
13
+
14
+ # @rbs @now: Time
15
+
16
+ attr_reader :now
17
+
18
+ # @rbs (?now: Time) -> void
19
+ def initialize(now: SolidObjects.database_adapter.database_now)
20
+ @now = now
21
+ end
22
+
23
+ # @rbs () -> Hash[Symbol, untyped]
24
+ def to_h
25
+ {
26
+ instances:,
27
+ mailbox:,
28
+ effects: grouped(Effect, :status, EFFECT_STATUSES),
29
+ broadcasts: grouped(Broadcast, :status, BROADCAST_STATUSES),
30
+ reminders:,
31
+ dead_letters: { total: DeadLetter.count },
32
+ processes: grouped(Process, :shutdown_state, PROCESS_STATES),
33
+ server_time: now.utc.iso8601
34
+ }
35
+ end
36
+
37
+ # @rbs () -> Hash[Symbol, Integer]
38
+ def instances
39
+ {
40
+ total: Instance.count,
41
+ paused: Instance.where.not(paused_at: nil).count,
42
+ activated: Instance.where(activation_expires_at: now..).count
43
+ }
44
+ end
45
+
46
+ # The oldest ready message that is already due is the queue latency of
47
+ # this runtime: how far behind the workers are, in seconds.
48
+ # @rbs () -> Hash[Symbol, untyped]
49
+ def mailbox
50
+ due = ReadyMessage.where(available_at: ..now)
51
+ oldest = due.minimum(:available_at)
52
+ {
53
+ ready: ReadyMessage.count,
54
+ due: due.count,
55
+ claimed: ClaimedMessage.count,
56
+ latency: oldest ? (now - oldest).round(3) : 0.0
57
+ }
58
+ end
59
+
60
+ # @rbs () -> Hash[Symbol, Integer]
61
+ def reminders
62
+ grouped(Reminder, :status, REMINDER_STATUSES).merge(
63
+ due: Reminder.where(status: "scheduled", next_run_at: ..now).count
64
+ )
65
+ end
66
+
67
+ private
68
+
69
+ # A status the schema allows but the table does not currently hold still
70
+ # reports zero, so a row of counts keeps the same shape between polls.
71
+ # @rbs (untyped, Symbol, Array[String]) -> Hash[Symbol, Integer]
72
+ def grouped(model, column, statuses)
73
+ counts = model.group(column).count
74
+ statuses.to_h { |status| [ status.to_sym, counts.fetch(status, 0) ] }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -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
@@ -87,8 +87,8 @@ module SolidObjects
87
87
  # @rbs (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler
88
88
  def self.query: (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler
89
89
 
90
- # @rbs (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
91
- def self.observable: (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
90
+ # @rbs (Symbol | String, ?broadcast: Symbol) ?{ () -> untyped } -> ActorDefinition::Handler
91
+ def self.observable: (Symbol | String, ?broadcast: Symbol) ?{ () -> untyped } -> ActorDefinition::Handler
92
92
 
93
93
  # @rbs (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
94
94
  def self.broadcast_payload: (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
@@ -44,6 +44,8 @@ module SolidObjects
44
44
 
45
45
  @payload_broadcasts: Hash[Symbol, Handler]
46
46
 
47
+ @observable_broadcasts: Hash[Symbol, Symbol]
48
+
47
49
  @observables: Hash[Symbol, Handler]
48
50
 
49
51
  @queries: Hash[Symbol, Handler]
@@ -82,8 +84,11 @@ module SolidObjects
82
84
  # @rbs (Symbol | String, Proc) -> Handler
83
85
  def add_query: (Symbol | String, Proc) -> Handler
84
86
 
85
- # @rbs (Symbol | String, Proc?) -> Handler
86
- def add_observable: (Symbol | String, Proc?) -> Handler
87
+ # @rbs (Symbol | String, Proc?, broadcast: Symbol) -> Handler
88
+ def add_observable: (Symbol | String, Proc?, broadcast: Symbol) -> Handler
89
+
90
+ # @rbs (Symbol | String) -> bool
91
+ def broadcasts_observable_value?: (Symbol | String) -> bool
87
92
 
88
93
  # @rbs (Symbol | String, Proc) -> Handler
89
94
  def add_payload_broadcast: (Symbol | String, Proc) -> Handler
@@ -109,6 +114,8 @@ module SolidObjects
109
114
 
110
115
  attr_reader method_messages: untyped
111
116
 
117
+ attr_reader observable_broadcasts: untyped
118
+
112
119
  # @rbs (Symbol) -> Handler
113
120
  def add_method_message: (Symbol) -> Handler
114
121
 
@@ -179,6 +179,9 @@ module SolidObjects
179
179
  class UnknownCommitAction < NonRetryableError
180
180
  end
181
181
 
182
+ class InvalidRejectionCode < NonRetryableError
183
+ end
184
+
182
185
  class Rejected < Error
183
186
  @code: String
184
187
 
@@ -13,8 +13,8 @@ module SolidObjects
13
13
  # @rbs (?process_registry: ProcessRegistry, ?database_adapter: DatabaseAdapter) -> void
14
14
  def initialize: (?process_registry: ProcessRegistry, ?database_adapter: DatabaseAdapter) -> void
15
15
 
16
- # @rbs () -> bool
17
- def run_once: () -> bool
16
+ # @rbs (?now: Time?) -> bool
17
+ def run_once: (?now: Time?) -> bool
18
18
 
19
19
  # @rbs () -> void
20
20
  def stop: () -> void
@@ -37,11 +37,11 @@ module SolidObjects
37
37
 
38
38
  attr_reader database_adapter: untyped
39
39
 
40
- # @rbs () -> Reminder?
41
- def claim_next: () -> Reminder?
40
+ # @rbs (now: Time?) -> Reminder?
41
+ def claim_next: (now: Time?) -> Reminder?
42
42
 
43
- # @rbs (Reminder) -> MessageReference?
44
- def enqueue: (Reminder) -> MessageReference?
43
+ # @rbs (Reminder, now: Time?) -> MessageReference?
44
+ def enqueue: (Reminder, now: Time?) -> MessageReference?
45
45
 
46
46
  # @rbs (Reminder) -> void
47
47
  def release: (Reminder) -> void
@@ -51,5 +51,8 @@ module SolidObjects
51
51
 
52
52
  # @rbs (Reminder, Time) -> Time
53
53
  def next_run_at: (Reminder, Time) -> Time
54
+
55
+ # @rbs (Time?) -> Time?
56
+ def normalize_test_time: (Time?) -> Time?
54
57
  end
55
58
  end
@@ -26,6 +26,9 @@ module SolidObjects
26
26
  # @rbs (?roles: Array[Symbol], ?max_passes: Integer) -> Integer
27
27
  def drain_solid_objects: (?roles: Array[Symbol], ?max_passes: Integer) -> Integer
28
28
 
29
+ # @rbs (now: Time, ?max_reminders: Integer) -> Integer
30
+ def run_due_reminders: (now: Time, ?max_reminders: Integer) -> Integer
31
+
29
32
  private
30
33
 
31
34
  # @rbs (Array[Symbol]) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
@@ -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
@@ -0,0 +1,55 @@
1
+ # Generated from lib/solid_objects/web/csrf_protection.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class Web
5
+ # A state changing request must carry the token of the session that asked
6
+ # for the form. The token a form receives is masked with a fresh one-time
7
+ # pad on every request, so the bytes on the wire differ each time and a
8
+ # compression side channel cannot recover the session token.
9
+ class CsrfProtection
10
+ SAFE_METHODS: untyped
11
+
12
+ TOKEN_BYTES: ::Integer
13
+
14
+ MISSING_SESSION: ::String
15
+
16
+ # @rbs (untyped) -> void
17
+ def initialize: (untyped) -> void
18
+
19
+ # @rbs (Hash[String, untyped]) -> Array[untyped]
20
+ def call: (Hash[String, untyped]) -> Array[untyped]
21
+
22
+ private
23
+
24
+ # @rbs (Hash[String, untyped]) -> bool
25
+ def accept?: (Hash[String, untyped]) -> bool
26
+
27
+ # @rbs (Hash[String, untyped], String?) -> bool
28
+ def valid?: (Hash[String, untyped], String?) -> bool
29
+
30
+ # @rbs (String, String) -> bool
31
+ def matches?: (String, String) -> bool
32
+
33
+ # @rbs (String) -> String
34
+ def mask: (String) -> String
35
+
36
+ # @rbs (String) -> String
37
+ def unmask: (String) -> String
38
+
39
+ # @rbs (String, String) -> String
40
+ def exclusive_or: (String, String) -> String
41
+
42
+ # @rbs (String) -> String
43
+ def encode: (String) -> String
44
+
45
+ # @rbs (String) -> String?
46
+ def decode: (String) -> String?
47
+
48
+ # @rbs (Hash[String, untyped]) -> Hash[untyped, untyped]
49
+ def session!: (Hash[String, untyped]) -> Hash[untyped, untyped]
50
+
51
+ # @rbs () -> Array[untyped]
52
+ def forbidden: () -> Array[untyped]
53
+ end
54
+ end
55
+ end