solid_objects 0.13.1 → 0.13.3
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/CHANGELOG.md +32 -0
- data/README.md +40 -9
- data/app/models/solid_objects/record.rb +2 -0
- data/db/migrate/20260805000000_create_solid_objects_tables.rb +1 -1
- data/db/migrate/20260806000000_add_state_revision_to_solid_objects_instances.rb +1 -1
- data/db/migrate/20260813000000_rename_message_dispatch_columns.rb +1 -1
- data/docs/development.md +1 -1
- data/docs/operations.md +8 -0
- data/docs/roadmap.md +6 -2
- data/lib/solid_objects/actor.rb +54 -4
- data/lib/solid_objects/administration.rb +44 -0
- data/lib/solid_objects/engine.rb +5 -3
- data/lib/solid_objects/executor.rb +5 -5
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +7 -0
- data/sig/generated/lib/solid_objects/actor.rbs +35 -6
- data/sig/generated/lib/solid_objects/administration.rbs +13 -0
- data/sig/generated/lib/solid_objects/engine.rbs +2 -0
- data/sig/generated/lib/solid_objects/executor.rbs +4 -4
- data/sig/generated/lib/solid_objects.rbs +3 -0
- metadata +16 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9045039767fa2e9ff5a4b5cd2bceb97ca5bc8ea0101b41aaac8fcefcf8a694ef
|
|
4
|
+
data.tar.gz: efed76c2524be2f7a9591a631c56355f95edaf39332cea846fb895363b8d8d68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 691972f84f5304cbe3f3be42b5889697b179f1eca38e55d76f87827e0ffc5c095ec97c0d591fa58680829920c807bde08619dc725add17be7f1215439775f064
|
|
7
|
+
data.tar.gz: 68bff7f63f6557e50a6262e22b33fd7633372ec77a33e35b37f95d074498b66b98b01527776b18acbe2ae6527133c6bf7588e50f98e501a0c5636101dac6f45d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.3 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- Stop loading `ActiveRecord::Base` when the gem is required. The engine now
|
|
6
|
+
loads `SolidObjects::Record` from an `ActiveSupport.on_load(:active_record)`
|
|
7
|
+
hook, so a host application keeps the normal timing of its own
|
|
8
|
+
`on_load(:active_record)` and `on_load(:active_record_encryption)` hooks. An
|
|
9
|
+
application that assigns its Active Record encryption keys in
|
|
10
|
+
`config/initializers` no longer loses them.
|
|
11
|
+
- Apply the configured `connects_to` in the record class body, so the
|
|
12
|
+
connection follows the class through a development reload.
|
|
13
|
+
- Lower the supported Rails floor from 8.0 to 7.1. The gem dependencies, the
|
|
14
|
+
bundled migrations, and the compatibility CI matrix now cover Rails 7.1, 7.2,
|
|
15
|
+
8.0, and 8.1. The migrations declare `ActiveRecord::Migration[7.1]`, which
|
|
16
|
+
builds the same schema as `[8.0]` because the compatibility layer between the
|
|
17
|
+
two only changes `remove_foreign_key`, which no Solid Objects migration calls.
|
|
18
|
+
Rails 7.0 stays out of range: its SQLite adapter requires `sqlite3 ~> 1.4`,
|
|
19
|
+
and the busy-handler control this gem needs arrived in `sqlite3` 2.x.
|
|
20
|
+
|
|
21
|
+
## 0.13.2 - 2026-08-17
|
|
22
|
+
|
|
23
|
+
- Accept a `key:` on `schedule`, naming a reminder for the item it is waiting
|
|
24
|
+
on rather than for its operation, so one actor can hold an alarm per queued
|
|
25
|
+
item. Scheduling the same key again moves that item's alarm and leaves the
|
|
26
|
+
others alone. Without a key the name is still the operation, so existing
|
|
27
|
+
reminders keep their names and their coalescing behaviour. A reminder
|
|
28
|
+
operation may no longer hold the colon that separates a key, which keeps
|
|
29
|
+
keyed and unkeyed names disjoint, and the length is checked on the composed
|
|
30
|
+
name rather than the key alone.
|
|
31
|
+
- Add an authorized `SolidObjects.administration.processes` query for
|
|
32
|
+
inspecting live and stale process rows through the runtime database adapter.
|
|
33
|
+
- Document rolling-deployment overlap as a reason for the polling-only warning.
|
|
34
|
+
|
|
3
35
|
## 0.13.1 - 2026-08-16
|
|
4
36
|
|
|
5
37
|
- Back idle actor, effect, reminder, and broadcast polling off exponentially
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/cardmagic/solid_objects/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Self-hosted, distributed Durable Objects in Rails without a daemon using your existing SQL database.**
|
|
6
6
|
|
|
7
7
|
Solid Objects brings the Durable Objects programming model—addressable objects,
|
|
8
8
|
durable state, serialized turns, alarms, and live clients—to ordinary Rails
|
|
@@ -341,7 +341,8 @@ mount SolidObjects::Engine => "/solid_objects"
|
|
|
341
341
|
|
|
342
342
|
## Installation
|
|
343
343
|
|
|
344
|
-
Solid Objects requires Ruby 3.3 or newer and Rails
|
|
344
|
+
Solid Objects requires Ruby 3.3 or newer and Rails 7.1 or newer. CI runs the
|
|
345
|
+
suite against Rails 7.1, 7.2, 8.0, and 8.1.
|
|
345
346
|
|
|
346
347
|
Add the gem, install its initializer and migration, then migrate:
|
|
347
348
|
|
|
@@ -891,7 +892,7 @@ database enforces this with a unique index on `(instance_id, name)`.
|
|
|
891
892
|
|
|
892
893
|
This is the same model as Orleans reminders and Durable Objects alarms, and it
|
|
893
894
|
is what makes a reminder safe to re-arm from a handler that may run more than
|
|
894
|
-
once.
|
|
895
|
+
once. Without a key the name is the operation, so this is a data-loss bug:
|
|
895
896
|
|
|
896
897
|
```ruby
|
|
897
898
|
# Wrong. Every entry overwrites the previous entry's alarm.
|
|
@@ -904,8 +905,38 @@ end
|
|
|
904
905
|
Two entries leave one reminder. The earlier wake-up never happens, nothing
|
|
905
906
|
raises, and nothing is logged except a `solid_objects.reminder.replaced` event.
|
|
906
907
|
|
|
907
|
-
|
|
908
|
-
|
|
908
|
+
### An alarm per item, with `key:`
|
|
909
|
+
|
|
910
|
+
Pass `key:` when an actor is waiting on several things at once. The key is your
|
|
911
|
+
own identifier for the item, and it names that item's alarm, so each item gets
|
|
912
|
+
one:
|
|
913
|
+
|
|
914
|
+
```ruby
|
|
915
|
+
def add(entry:)
|
|
916
|
+
self.entries = entries + [ entry ]
|
|
917
|
+
schedule(at: entry.fetch("wait_until"), key: entry.fetch("id")).deliver
|
|
918
|
+
end
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Two entries now leave two reminders. Scheduling the same key again moves that
|
|
922
|
+
item's alarm and leaves the others alone, which is what makes a keyed reminder
|
|
923
|
+
as safe to re-arm as an unkeyed one. The operation still decides which handler
|
|
924
|
+
runs; the key only decides which alarm is which.
|
|
925
|
+
|
|
926
|
+
A key must be non-empty, and the name it becomes must fit the 191-character
|
|
927
|
+
column, which is checked on the composed name rather than the key alone so a
|
|
928
|
+
long operation and a short key are caught too.
|
|
929
|
+
|
|
930
|
+
The key is separated from the operation by a colon, so an operation may not hold
|
|
931
|
+
one. Otherwise an unkeyed `deliver:item` and a `deliver` keyed `item` would be
|
|
932
|
+
one name, and the second would silently take the first one's alarm. A key may
|
|
933
|
+
hold colons of its own, because the operation before the first one cannot.
|
|
934
|
+
|
|
935
|
+
### One alarm for a whole queue
|
|
936
|
+
|
|
937
|
+
A key per item is not always what you want. An actor that only ever needs to
|
|
938
|
+
know "what is next" can keep one alarm and let the handler drain everything now
|
|
939
|
+
due before arming the next:
|
|
909
940
|
|
|
910
941
|
```ruby
|
|
911
942
|
def add(entry:)
|
|
@@ -931,10 +962,10 @@ def arm_next
|
|
|
931
962
|
end
|
|
932
963
|
```
|
|
933
964
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
965
|
+
That costs one reminder row instead of one per item, and a coalesced occurrence
|
|
966
|
+
cannot strand an entry because the handler drains by time rather than by alarm.
|
|
967
|
+
Prefer it when the queue is large and the items are interchangeable; prefer
|
|
968
|
+
`key:` when an item needs its own alarm that can be moved on its own.
|
|
938
969
|
|
|
939
970
|
Solid Objects has no `unschedule`. A reminder stops when its handler does not
|
|
940
971
|
re-arm it, and destroying an actor removes its reminders.
|
data/docs/development.md
CHANGED
data/docs/operations.md
CHANGED
|
@@ -98,6 +98,14 @@ adapter is configured, the runtime logs
|
|
|
98
98
|
need prompt delivery. Without one, newly committed work can wait up to the
|
|
99
99
|
current idle polling interval.
|
|
100
100
|
|
|
101
|
+
The warning excludes process rows with the current hostname and PID. It can
|
|
102
|
+
therefore appear during a rolling deployment or restart overlap when an older
|
|
103
|
+
and newer process briefly share the database. A process that stopped without
|
|
104
|
+
graceful cleanup remains live until its heartbeat exceeds
|
|
105
|
+
`process_alive_threshold`; inspect `SolidObjects.administration.processes` to
|
|
106
|
+
distinguish a live overlap from a stale row without opening a second SQLite
|
|
107
|
+
connection.
|
|
108
|
+
|
|
101
109
|
Each role exposes `current_polling_interval`.
|
|
102
110
|
`solid_objects.polling.interval_changed` reports the role, reason, previous
|
|
103
111
|
interval, and current interval. The polling-only warning is also emitted as
|
data/docs/roadmap.md
CHANGED
|
@@ -66,8 +66,12 @@
|
|
|
66
66
|
- Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact
|
|
67
67
|
RuboCop policy, and a warning-free Brakeman scan
|
|
68
68
|
- Compatibility CI across the supported span: Ruby 3.3, 3.4, and 4.0 against
|
|
69
|
-
Rails 8.0 and 8.1, pinned through `RAILS_VERSION` so the advertised
|
|
70
|
-
verified rather than assumed
|
|
69
|
+
Rails 7.1, 7.2, 8.0, and 8.1, pinned through `RAILS_VERSION` so the advertised
|
|
70
|
+
range is verified rather than assumed. The compatibility job runs SQLite only;
|
|
71
|
+
the PostgreSQL and MySQL jobs run on the newest Rails, so adapter behavior on
|
|
72
|
+
Rails 7.1 and 7.2 is unmeasured against those servers. Rails 7.0 is out of
|
|
73
|
+
range because its SQLite adapter requires `sqlite3 ~> 1.4`, and this gem needs
|
|
74
|
+
the busy-handler control that arrived in `sqlite3` 2.x
|
|
71
75
|
- A JavaScript suite covering every browser module, run in CI with Node's test
|
|
72
76
|
runner and jsdom, plus a browser suite running the same modules against real
|
|
73
77
|
Chromium and a real Turbo build, with every GitHub Actions reference pinned to
|
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -4,7 +4,11 @@ module SolidObjects
|
|
|
4
4
|
class Actor
|
|
5
5
|
EffectIntent = Data.define(:name, :arguments, :success_operation, :failure_operation)
|
|
6
6
|
CommitActionIntent = Data.define(:name, :arguments)
|
|
7
|
-
|
|
7
|
+
# The reminders table holds a name in 191 characters.
|
|
8
|
+
REMINDER_NAME_LIMIT = 191
|
|
9
|
+
REMINDER_KEY_SEPARATOR = ":"
|
|
10
|
+
|
|
11
|
+
ReminderIntent = Data.define(:name, :operation, :at, :arguments, :interval_seconds, :missed_policy)
|
|
8
12
|
OutboundMessageIntent = Data.define(:actor_type, :actor_id, :operation, :arguments, :available_at, :idempotency_key)
|
|
9
13
|
|
|
10
14
|
class << self
|
|
@@ -197,8 +201,13 @@ module SolidObjects
|
|
|
197
201
|
nil
|
|
198
202
|
end
|
|
199
203
|
|
|
200
|
-
#
|
|
201
|
-
|
|
204
|
+
# A reminder is identified by its name, and without a key that name is the
|
|
205
|
+
# operation, so one actor holds one alarm per operation. A key gives an actor
|
|
206
|
+
# an alarm per item it is waiting on, which is what an actor holding a queue
|
|
207
|
+
# of scheduled work needs; the key is the caller's own identifier for the
|
|
208
|
+
# item, and scheduling the same key again moves that item's alarm.
|
|
209
|
+
# @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher
|
|
210
|
+
def schedule(at:, every: nil, missed: :latest, key: nil)
|
|
202
211
|
interval_seconds = every&.to_f
|
|
203
212
|
if interval_seconds && !interval_seconds.positive?
|
|
204
213
|
raise ArgumentError, "reminder interval must be positive"
|
|
@@ -207,13 +216,15 @@ module SolidObjects
|
|
|
207
216
|
unless %w[all latest].include?(missed_policy)
|
|
208
217
|
raise ArgumentError, "missed reminder policy must be all or latest"
|
|
209
218
|
end
|
|
219
|
+
reminder_key = validated_reminder_key(key)
|
|
210
220
|
|
|
211
221
|
OperationDispatcher.new(
|
|
212
222
|
actor_type: self.class.actor_type,
|
|
213
223
|
handlers: self.class.definition.messages
|
|
214
224
|
) do |operation, arguments|
|
|
215
225
|
ReminderIntent.new(
|
|
216
|
-
name: operation
|
|
226
|
+
name: reminder_name(operation:, key: reminder_key),
|
|
227
|
+
operation: operation.to_s,
|
|
217
228
|
at:,
|
|
218
229
|
arguments: Serialization.dump(arguments),
|
|
219
230
|
interval_seconds:,
|
|
@@ -225,6 +236,45 @@ module SolidObjects
|
|
|
225
236
|
end
|
|
226
237
|
end
|
|
227
238
|
|
|
239
|
+
# @rbs ((String | Symbol | Integer)?) -> String?
|
|
240
|
+
def validated_reminder_key(key)
|
|
241
|
+
return nil if key.nil?
|
|
242
|
+
|
|
243
|
+
reminder_key = key.to_s
|
|
244
|
+
raise ArgumentError, "reminder key must not be empty" if reminder_key.empty?
|
|
245
|
+
|
|
246
|
+
reminder_key
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# A keyed name is the operation, a colon, and the key, so an operation
|
|
250
|
+
# holding a colon of its own would make two different schedules produce one
|
|
251
|
+
# name: an unkeyed "deliver:item" and a "deliver" keyed "item" would share a
|
|
252
|
+
# row, and the second would silently take the first one's alarm. Refusing a
|
|
253
|
+
# colon in the operation keeps unkeyed names free of colons, which leaves
|
|
254
|
+
# the two kinds of name disjoint and lets a key hold colons of its own.
|
|
255
|
+
#
|
|
256
|
+
# The length is checked on the composed name rather than the key alone,
|
|
257
|
+
# because a long operation and a short key can exceed the column just as
|
|
258
|
+
# easily as the reverse. Both are refused here rather than at the insert,
|
|
259
|
+
# once the turn is already doing work.
|
|
260
|
+
# @rbs (operation: Symbol | String, key: String?) -> String
|
|
261
|
+
def reminder_name(operation:, key:)
|
|
262
|
+
operation_name = operation.to_s
|
|
263
|
+
if operation_name.include?(REMINDER_KEY_SEPARATOR)
|
|
264
|
+
raise ArgumentError,
|
|
265
|
+
"reminder operation #{operation_name.inspect} must not contain #{REMINDER_KEY_SEPARATOR.inspect}"
|
|
266
|
+
end
|
|
267
|
+
return operation_name if key.nil?
|
|
268
|
+
|
|
269
|
+
name = "#{operation_name}#{REMINDER_KEY_SEPARATOR}#{key}"
|
|
270
|
+
if name.length > REMINDER_NAME_LIMIT
|
|
271
|
+
raise ArgumentError,
|
|
272
|
+
"reminder name #{name.length} characters exceeds the #{REMINDER_NAME_LIMIT} the database holds"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
name
|
|
276
|
+
end
|
|
277
|
+
|
|
228
278
|
# @rbs (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
|
|
229
279
|
def send_to(reference, available_at: nil, idempotency_key: nil)
|
|
230
280
|
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Administration
|
|
5
|
+
# @rbs (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
|
|
6
|
+
def processes(authorization_context: nil)
|
|
7
|
+
authorize!(authorization_context:)
|
|
8
|
+
now = SolidObjects.database_adapter.database_now
|
|
9
|
+
stale_at = now - SolidObjects.configuration.process_alive_threshold
|
|
10
|
+
|
|
11
|
+
Process.order(:kind, :started_at).map do |process_record|
|
|
12
|
+
{
|
|
13
|
+
id: process_record.id,
|
|
14
|
+
kind: process_record.kind,
|
|
15
|
+
hostname: process_record.hostname,
|
|
16
|
+
pid: process_record.pid,
|
|
17
|
+
metadata: Serialization.readonly_copy(process_record.metadata),
|
|
18
|
+
shutdown_state: process_record.shutdown_state,
|
|
19
|
+
shutdown_requested_at: process_record.shutdown_requested_at,
|
|
20
|
+
started_at: process_record.started_at,
|
|
21
|
+
last_heartbeat_at: process_record.last_heartbeat_at,
|
|
22
|
+
stopped_at: process_record.stopped_at,
|
|
23
|
+
stale: process_record.shutdown_state != "stopped" &&
|
|
24
|
+
process_record.last_heartbeat_at <= stale_at
|
|
25
|
+
}.freeze
|
|
26
|
+
end.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @rbs (?authorization_context: untyped) -> void
|
|
32
|
+
def authorize!(authorization_context: nil)
|
|
33
|
+
authorized = SolidObjects.configuration.authorize_administration.call(
|
|
34
|
+
action: :inspect,
|
|
35
|
+
resource: "processes",
|
|
36
|
+
resource_id: nil,
|
|
37
|
+
authorization_context:
|
|
38
|
+
)
|
|
39
|
+
return if authorized
|
|
40
|
+
|
|
41
|
+
raise Unauthorized, "actor administration is not authorized"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/solid_objects/engine.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# rbs_inline: enabled
|
|
2
2
|
|
|
3
|
-
require_relative "../../app/models/solid_objects/record"
|
|
4
|
-
|
|
5
3
|
module SolidObjects
|
|
6
4
|
class Engine < ::Rails::Engine
|
|
5
|
+
RECORD_PATH = File.expand_path("../../app/models/solid_objects/record.rb", __dir__)
|
|
6
|
+
|
|
7
7
|
isolate_namespace SolidObjects
|
|
8
8
|
|
|
9
9
|
config.generators do |generators|
|
|
@@ -16,7 +16,9 @@ module SolidObjects
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
initializer "solid_objects.database", after: :load_config_initializers do
|
|
19
|
-
|
|
19
|
+
ActiveSupport.on_load(:active_record) do
|
|
20
|
+
require RECORD_PATH
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
initializer "solid_objects.helpers" do
|
|
@@ -223,10 +223,10 @@ module SolidObjects
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# A reminder is one named alarm per actor, so scheduling a name that is
|
|
226
|
-
# already armed moves it rather than adding a second.
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
226
|
+
# already armed moves it rather than adding a second. Without a key that
|
|
227
|
+
# name is the operation, so an actor arming a reminder per queued item keeps
|
|
228
|
+
# only the last; passing schedule a key gives each item its own name and so
|
|
229
|
+
# its own alarm.
|
|
230
230
|
# Moves are returned rather than reported here, so the report happens after
|
|
231
231
|
# the turn commits. A rolled back turn would otherwise announce an alarm
|
|
232
232
|
# that never moved, which is the opposite of the visibility this event
|
|
@@ -239,7 +239,7 @@ module SolidObjects
|
|
|
239
239
|
reminder.assign_attributes(
|
|
240
240
|
actor_type: instance.actor_type,
|
|
241
241
|
actor_id: instance.actor_id,
|
|
242
|
-
operation: intent.
|
|
242
|
+
operation: intent.operation,
|
|
243
243
|
arguments: intent.arguments,
|
|
244
244
|
next_run_at: intent.at,
|
|
245
245
|
interval_seconds: intent.interval_seconds,
|
data/lib/solid_objects.rb
CHANGED
|
@@ -31,6 +31,7 @@ require "solid_objects/dead_letter_manager"
|
|
|
31
31
|
require "solid_objects/message_pruner"
|
|
32
32
|
require "solid_objects/instance_pruner"
|
|
33
33
|
require "solid_objects/process_pruner"
|
|
34
|
+
require "solid_objects/administration"
|
|
34
35
|
require "solid_objects/stream_name"
|
|
35
36
|
require "solid_objects/dom_identity"
|
|
36
37
|
require "solid_objects/stream_token"
|
|
@@ -137,6 +138,11 @@ module SolidObjects
|
|
|
137
138
|
@dead_letters ||= DeadLetterManager.new
|
|
138
139
|
end
|
|
139
140
|
|
|
141
|
+
# @rbs () -> Administration
|
|
142
|
+
def administration
|
|
143
|
+
@administration ||= Administration.new
|
|
144
|
+
end
|
|
145
|
+
|
|
140
146
|
# @rbs (String | Symbol) -> String
|
|
141
147
|
def table_name(name)
|
|
142
148
|
"#{configuration.table_name_prefix}#{name}"
|
|
@@ -164,6 +170,7 @@ module SolidObjects
|
|
|
164
170
|
@effect_registry = EffectRegistry.new
|
|
165
171
|
@commit_action_registry = CommitActionRegistry.new
|
|
166
172
|
@dead_letters = nil
|
|
173
|
+
@administration = nil
|
|
167
174
|
end
|
|
168
175
|
|
|
169
176
|
# @rbs () -> DatabaseAdapter
|
|
@@ -32,9 +32,16 @@ module SolidObjects
|
|
|
32
32
|
def members: () -> [ :name, :arguments ]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# The reminders table holds a name in 191 characters.
|
|
36
|
+
REMINDER_NAME_LIMIT: ::Integer
|
|
37
|
+
|
|
38
|
+
REMINDER_KEY_SEPARATOR: ::String
|
|
39
|
+
|
|
35
40
|
class ReminderIntent < Data
|
|
36
41
|
attr_reader name(): untyped
|
|
37
42
|
|
|
43
|
+
attr_reader operation(): untyped
|
|
44
|
+
|
|
38
45
|
attr_reader at(): untyped
|
|
39
46
|
|
|
40
47
|
attr_reader arguments(): untyped
|
|
@@ -43,12 +50,12 @@ module SolidObjects
|
|
|
43
50
|
|
|
44
51
|
attr_reader missed_policy(): untyped
|
|
45
52
|
|
|
46
|
-
def self.new: (untyped name, untyped at, untyped arguments, untyped interval_seconds, untyped missed_policy) -> instance
|
|
47
|
-
| (name: untyped, at: untyped, arguments: untyped, interval_seconds: untyped, missed_policy: untyped) -> instance
|
|
53
|
+
def self.new: (untyped name, untyped operation, untyped at, untyped arguments, untyped interval_seconds, untyped missed_policy) -> instance
|
|
54
|
+
| (name: untyped, operation: untyped, at: untyped, arguments: untyped, interval_seconds: untyped, missed_policy: untyped) -> instance
|
|
48
55
|
|
|
49
|
-
def self.members: () -> [ :name, :at, :arguments, :interval_seconds, :missed_policy ]
|
|
56
|
+
def self.members: () -> [ :name, :operation, :at, :arguments, :interval_seconds, :missed_policy ]
|
|
50
57
|
|
|
51
|
-
def members: () -> [ :name, :at, :arguments, :interval_seconds, :missed_policy ]
|
|
58
|
+
def members: () -> [ :name, :operation, :at, :arguments, :interval_seconds, :missed_policy ]
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
class OutboundMessageIntent < Data
|
|
@@ -157,8 +164,30 @@ module SolidObjects
|
|
|
157
164
|
# @rbs (Symbol | String, **untyped) -> nil
|
|
158
165
|
def commit_action: (Symbol | String, **untyped) -> nil
|
|
159
166
|
|
|
160
|
-
#
|
|
161
|
-
|
|
167
|
+
# A reminder is identified by its name, and without a key that name is the
|
|
168
|
+
# operation, so one actor holds one alarm per operation. A key gives an actor
|
|
169
|
+
# an alarm per item it is waiting on, which is what an actor holding a queue
|
|
170
|
+
# of scheduled work needs; the key is the caller's own identifier for the
|
|
171
|
+
# item, and scheduling the same key again moves that item's alarm.
|
|
172
|
+
# @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher
|
|
173
|
+
def schedule: (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher
|
|
174
|
+
|
|
175
|
+
# @rbs ((String | Symbol | Integer)?) -> String?
|
|
176
|
+
def validated_reminder_key: ((String | Symbol | Integer)?) -> String?
|
|
177
|
+
|
|
178
|
+
# A keyed name is the operation, a colon, and the key, so an operation
|
|
179
|
+
# holding a colon of its own would make two different schedules produce one
|
|
180
|
+
# name: an unkeyed "deliver:item" and a "deliver" keyed "item" would share a
|
|
181
|
+
# row, and the second would silently take the first one's alarm. Refusing a
|
|
182
|
+
# colon in the operation keeps unkeyed names free of colons, which leaves
|
|
183
|
+
# the two kinds of name disjoint and lets a key hold colons of its own.
|
|
184
|
+
#
|
|
185
|
+
# The length is checked on the composed name rather than the key alone,
|
|
186
|
+
# because a long operation and a short key can exceed the column just as
|
|
187
|
+
# easily as the reverse. Both are refused here rather than at the insert,
|
|
188
|
+
# once the turn is already doing work.
|
|
189
|
+
# @rbs (operation: Symbol | String, key: String?) -> String
|
|
190
|
+
def reminder_name: (operation: Symbol | String, key: String?) -> String
|
|
162
191
|
|
|
163
192
|
# @rbs (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
|
|
164
193
|
def send_to: (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/administration.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Administration
|
|
5
|
+
# @rbs (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
|
|
6
|
+
def processes: (?authorization_context: untyped) -> Array[Hash[Symbol, untyped]]
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# @rbs (?authorization_context: untyped) -> void
|
|
11
|
+
def authorize!: (?authorization_context: untyped) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -46,10 +46,10 @@ module SolidObjects
|
|
|
46
46
|
def enqueue_effects: (message: Message, instance: Instance, intents: Array[Actor::EffectIntent]) -> Array[Effect]
|
|
47
47
|
|
|
48
48
|
# A reminder is one named alarm per actor, so scheduling a name that is
|
|
49
|
-
# already armed moves it rather than adding a second.
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
49
|
+
# already armed moves it rather than adding a second. Without a key that
|
|
50
|
+
# name is the operation, so an actor arming a reminder per queued item keeps
|
|
51
|
+
# only the last; passing schedule a key gives each item its own name and so
|
|
52
|
+
# its own alarm.
|
|
53
53
|
# Moves are returned rather than reported here, so the report happens after
|
|
54
54
|
# the turn commits. A rolled back turn would otherwise announce an alarm
|
|
55
55
|
# that never moved, which is the opposite of the visibility this event
|
|
@@ -36,6 +36,9 @@ module SolidObjects
|
|
|
36
36
|
# @rbs () -> DeadLetterManager
|
|
37
37
|
def self.dead_letters: () -> DeadLetterManager
|
|
38
38
|
|
|
39
|
+
# @rbs () -> Administration
|
|
40
|
+
def self.administration: () -> Administration
|
|
41
|
+
|
|
39
42
|
# @rbs (String | Symbol) -> String
|
|
40
43
|
def self.table_name: (String | Symbol) -> String
|
|
41
44
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.13.
|
|
4
|
+
version: 0.13.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -16,70 +16,70 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '7.1'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '7.1'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: actionpack
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
33
|
+
version: '7.1'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '7.1'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: actionview
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
47
|
+
version: '7.1'
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '7.1'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: activerecord
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
61
|
+
version: '7.1'
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '7.1'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: activesupport
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
75
|
+
version: '7.1'
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '7.1'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: rack
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,14 +100,14 @@ dependencies:
|
|
|
100
100
|
requirements:
|
|
101
101
|
- - ">="
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
103
|
+
version: '7.1'
|
|
104
104
|
type: :runtime
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - ">="
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
110
|
+
version: '7.1'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: thor
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -382,6 +382,7 @@ files:
|
|
|
382
382
|
- lib/solid_objects/actor_registry.rb
|
|
383
383
|
- lib/solid_objects/actor_snapshot.rb
|
|
384
384
|
- lib/solid_objects/actor_view.rb
|
|
385
|
+
- lib/solid_objects/administration.rb
|
|
385
386
|
- lib/solid_objects/application_actor_loader.rb
|
|
386
387
|
- lib/solid_objects/application_write_guard.rb
|
|
387
388
|
- lib/solid_objects/broadcast_executor.rb
|
|
@@ -468,6 +469,7 @@ files:
|
|
|
468
469
|
- sig/generated/lib/solid_objects/actor_registry.rbs
|
|
469
470
|
- sig/generated/lib/solid_objects/actor_snapshot.rbs
|
|
470
471
|
- sig/generated/lib/solid_objects/actor_view.rbs
|
|
472
|
+
- sig/generated/lib/solid_objects/administration.rbs
|
|
471
473
|
- sig/generated/lib/solid_objects/application_actor_loader.rbs
|
|
472
474
|
- sig/generated/lib/solid_objects/application_write_guard.rbs
|
|
473
475
|
- sig/generated/lib/solid_objects/broadcast_executor.rbs
|