solid_objects 0.5.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/app/controllers/solid_objects/components_controller.rb +29 -2
- data/docs/correctness.md +15 -1
- data/docs/operations.md +20 -2
- data/lib/solid_objects/database_adapters/sqlite.rb +43 -5
- data/lib/solid_objects/doctor.rb +46 -8
- data/lib/solid_objects/synchronous_invocation.rb +16 -1
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/controllers/solid_objects/components_controller.rbs +6 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +9 -0
- data/sig/generated/lib/solid_objects/doctor.rbs +12 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e251e3648db71bc5e325e1d8a20be55f927ca08208052acebd973eb78e15497
|
|
4
|
+
data.tar.gz: 9362cf7145cb58c02273411dc619a775d7693d238d3f2212d7c4f6336abc30cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de728511cbd174ba841030f3e29e3dd4245ccd4dd8168523bf132100a8e4ee0c14f02781bf0b29c08a184f3688708f01d8227662068e6a25ebac1ea2b948c72e
|
|
7
|
+
data.tar.gz: a9d34e2ae1defd4288bd9322b3d717ae1fbcc99836f96d76ae2a09d62355e17349089e4b53d9febcefe98aa788c5753b5abcbeee1c9dce08e6968b38297f336e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 - 2026-08-07
|
|
4
|
+
|
|
5
|
+
- Restore the SQLite busy wait that a synchronous invocation suspends for its
|
|
6
|
+
deadline. Rails installs the busy wait as a Ruby busy handler through the
|
|
7
|
+
sqlite3 `timeout` configuration, which `PRAGMA busy_timeout` reports as zero
|
|
8
|
+
and silently replaces, so the previous save and restore left pooled
|
|
9
|
+
connections with no busy handler at all. Every later writer on that
|
|
10
|
+
connection, inside or outside Solid Objects, then failed immediately with
|
|
11
|
+
`SQLite3::BusyException` instead of waiting for the lock. Suspend the busy
|
|
12
|
+
wait only when the adapter can identify how to restore it, so an Active
|
|
13
|
+
Record release that stops exposing the configured timeout loosens
|
|
14
|
+
synchronous deadline bounds instead of stripping lock waiting from a shared
|
|
15
|
+
pooled connection.
|
|
16
|
+
|
|
17
|
+
- Run the doctor round-trip probe on a dedicated caller process, and accept an
|
|
18
|
+
explicit process registry in `SynchronousInvocation`, so the probe can no
|
|
19
|
+
longer stop and delete a shared application caller process, release its
|
|
20
|
+
activations, and unclaim its messages.
|
|
21
|
+
- Report doctor probe cleanup failures as a failed or warned check instead of
|
|
22
|
+
raising a database lock error out of the command and leaking the probe
|
|
23
|
+
caller process.
|
|
24
|
+
- Instrument component refreshes with actor identity, component name, key,
|
|
25
|
+
dependencies, refresh method, revision, and outcome, excluding locals.
|
|
26
|
+
|
|
3
27
|
## 0.5.0 - 2026-08-07
|
|
4
28
|
|
|
5
29
|
- Add repeatable reactive components with signed string or integer keys and
|
|
@@ -8,12 +8,25 @@ module SolidObjects
|
|
|
8
8
|
|
|
9
9
|
# @rbs () -> void
|
|
10
10
|
def show
|
|
11
|
+
SolidObjects.instrument(:"component.refreshed") { |payload| refresh(payload) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @rbs (Hash[Symbol, untyped]) -> void
|
|
17
|
+
def refresh(payload)
|
|
11
18
|
registration = ComponentRegistration.from_token(
|
|
12
19
|
params.require(:token)
|
|
13
20
|
)
|
|
21
|
+
payload.merge!(registration_payload(registration))
|
|
14
22
|
requested_revision = requested_revision_key
|
|
15
23
|
snapshot = ActorSnapshot.new(registration.reference)
|
|
16
|
-
|
|
24
|
+
payload[:instance_id] = snapshot.instance_id
|
|
25
|
+
payload[:revision] = snapshot.revision
|
|
26
|
+
if newer_than_snapshot?(requested_revision, snapshot)
|
|
27
|
+
payload[:outcome] = "conflict"
|
|
28
|
+
return head :conflict
|
|
29
|
+
end
|
|
17
30
|
|
|
18
31
|
authorization_context = SolidObjects
|
|
19
32
|
.configuration
|
|
@@ -26,18 +39,32 @@ module SolidObjects
|
|
|
26
39
|
authorization_context:
|
|
27
40
|
).call
|
|
28
41
|
response.headers["Cache-Control"] = "private, no-store"
|
|
42
|
+
payload[:outcome] = "rendered"
|
|
29
43
|
render html: component_frame(registration, snapshot, rendered)
|
|
30
44
|
rescue Unauthorized
|
|
45
|
+
payload[:outcome] = "unauthorized"
|
|
31
46
|
head :forbidden
|
|
32
47
|
rescue UnknownComponent
|
|
48
|
+
payload[:outcome] = "unknown_component"
|
|
33
49
|
head :not_found
|
|
34
50
|
rescue ActionController::ParameterMissing,
|
|
35
51
|
ArgumentError,
|
|
36
52
|
InvalidComponentToken
|
|
53
|
+
payload[:outcome] = "invalid_token"
|
|
37
54
|
head :bad_request
|
|
38
55
|
end
|
|
39
56
|
|
|
40
|
-
|
|
57
|
+
# @rbs (ComponentRegistration) -> Hash[Symbol, untyped]
|
|
58
|
+
def registration_payload(registration)
|
|
59
|
+
{
|
|
60
|
+
actor_type: registration.reference.actor_type,
|
|
61
|
+
actor_id: registration.reference.actor_id,
|
|
62
|
+
component_name: registration.component_name,
|
|
63
|
+
component_key: registration.component_key,
|
|
64
|
+
dependencies: registration.dependencies,
|
|
65
|
+
refresh_method: registration.refresh_method
|
|
66
|
+
}
|
|
67
|
+
end
|
|
41
68
|
|
|
42
69
|
# @rbs () -> Array[Integer]
|
|
43
70
|
def requested_revision_key
|
data/docs/correctness.md
CHANGED
|
@@ -167,7 +167,21 @@ result. Adapter lock/query deadlines cover the durable enqueue, caller-process
|
|
|
167
167
|
registration and heartbeat, activation coordination, and result observation.
|
|
168
168
|
SQLite retries busy coordination operations only within the original call
|
|
169
169
|
deadline and reports `waiting_on=database_contention` when the database cannot
|
|
170
|
-
be inspected at timeout.
|
|
170
|
+
be inspected at timeout. To keep those retries in Ruby, the SQLite adapter
|
|
171
|
+
suspends the connection's busy wait for the duration of each deadline-bound
|
|
172
|
+
transaction and restores it afterwards. Restoration reinstalls the Ruby busy
|
|
173
|
+
handler Rails configures from the sqlite3 `timeout` setting, which
|
|
174
|
+
`PRAGMA busy_timeout` neither reports nor preserves, so a synchronous call
|
|
175
|
+
leaves the connection's lock waiting behaviour exactly as it found it for
|
|
176
|
+
later writers inside and outside Solid Objects.
|
|
177
|
+
|
|
178
|
+
The adapter suspends the busy wait only when it can identify how to restore
|
|
179
|
+
it. When a future Active Record release stops exposing the configured
|
|
180
|
+
timeout, the adapter leaves the connection untouched: synchronous deadlines
|
|
181
|
+
lose their tight bound and wait as long as the configured busy wait allows,
|
|
182
|
+
rather than stripping lock waiting from a pooled connection the rest of the
|
|
183
|
+
application shares. A test asserts the timeout stays discoverable so the
|
|
184
|
+
looser bound cannot be adopted silently. If enqueue cannot commit, `SyncEnqueueTimeout` is
|
|
171
185
|
raised and no message reference exists. MySQL lock waits have one-second InnoDB
|
|
172
186
|
granularity. Ruby handlers that already started are not preempted.
|
|
173
187
|
|
data/docs/operations.md
CHANGED
|
@@ -15,6 +15,14 @@ so the schema check compares the required shape instead of a fixed timestamp.
|
|
|
15
15
|
Warnings such as an all-deny neutral policy do not fail the command because a
|
|
16
16
|
context-aware production policy may correctly deny the probe.
|
|
17
17
|
|
|
18
|
+
The round-trip probe runs on its own dedicated caller process rather than the
|
|
19
|
+
shared application caller process, and removes that record together with its
|
|
20
|
+
temporary actor when it finishes. Running the doctor inside a process that
|
|
21
|
+
already serves synchronous calls therefore leaves the application caller
|
|
22
|
+
process, its activations, and its claimed messages untouched, including when an
|
|
23
|
+
application call overlaps the probe. A database busy enough to block cleanup
|
|
24
|
+
reports a failed or warned check rather than raising out of the command.
|
|
25
|
+
|
|
18
26
|
## Runtime
|
|
19
27
|
|
|
20
28
|
Start all configured roles:
|
|
@@ -147,10 +155,20 @@ transaction rejection, commit-action start/completion/failure, effect and
|
|
|
147
155
|
broadcast enqueue/completion, reminder enqueue, actor destruction/expiration,
|
|
148
156
|
retention pruning, process cleanup, and supervisor lifecycle.
|
|
149
157
|
|
|
158
|
+
`solid_objects.component.refreshed` covers every authorized component refresh
|
|
159
|
+
request. Its payload carries the actor identity, `component_name`,
|
|
160
|
+
`component_key`, declared `dependencies`, `refresh_method`, the rendered
|
|
161
|
+
`instance_id` and `revision`, and an `outcome` of `rendered`, `conflict`,
|
|
162
|
+
`unauthorized`, `unknown_component`, or `invalid_token`. Use it to watch
|
|
163
|
+
refresh rate per key, authorization denials, superseded requests, and render
|
|
164
|
+
duration. A rejected token reports only the outcome, since no signed identity
|
|
165
|
+
was recovered.
|
|
166
|
+
|
|
150
167
|
Payloads contain stable runtime identifiers, actor identity, sequence,
|
|
151
168
|
attempts, ownership generations, and safe exception summaries where relevant.
|
|
152
|
-
Arguments, actor state, results, and outbox payloads are
|
|
153
|
-
log subscriber turns the same notifications into
|
|
169
|
+
Arguments, component locals, actor state, results, and outbox payloads are
|
|
170
|
+
excluded. The bundled log subscriber turns the same notifications into
|
|
171
|
+
structured logger hashes.
|
|
154
172
|
|
|
155
173
|
## Retention and backups
|
|
156
174
|
|
|
@@ -67,11 +67,49 @@ module SolidObjects
|
|
|
67
67
|
def with_transaction_deadline(connection)
|
|
68
68
|
return yield unless SyncDeadline.active?
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
busy_wait = restorable_busy_wait(connection)
|
|
71
|
+
return yield unless busy_wait
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
connection.execute("PRAGMA busy_timeout = 0")
|
|
75
|
+
yield
|
|
76
|
+
ensure
|
|
77
|
+
restore_busy_wait(connection, busy_wait)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @rbs (untyped) -> Hash[Symbol, untyped]?
|
|
82
|
+
def restorable_busy_wait(connection)
|
|
83
|
+
pragma_timeout = connection.select_value("PRAGMA busy_timeout").to_i
|
|
84
|
+
return { pragma_timeout: } if pragma_timeout.positive?
|
|
85
|
+
|
|
86
|
+
handler_timeout = configured_busy_handler_timeout(connection)
|
|
87
|
+
return nil unless handler_timeout
|
|
88
|
+
|
|
89
|
+
{ pragma_timeout:, handler_timeout: }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @rbs (untyped, Hash[Symbol, untyped]) -> void
|
|
93
|
+
def restore_busy_wait(connection, busy_wait)
|
|
94
|
+
handler_timeout = busy_wait[:handler_timeout]
|
|
95
|
+
if handler_timeout
|
|
96
|
+
connection.raw_connection.busy_handler_timeout = handler_timeout
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
connection.execute("PRAGMA busy_timeout = #{busy_wait.fetch(:pragma_timeout)}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @rbs (untyped) -> Integer?
|
|
104
|
+
def configured_busy_handler_timeout(connection)
|
|
105
|
+
return nil unless connection.respond_to?(:raw_connection)
|
|
106
|
+
return nil unless connection.raw_connection.respond_to?(:busy_handler_timeout=)
|
|
107
|
+
|
|
108
|
+
pool = connection.respond_to?(:pool) ? connection.pool : nil
|
|
109
|
+
return nil unless pool.respond_to?(:db_config)
|
|
110
|
+
|
|
111
|
+
timeout = pool.db_config.configuration_hash[:timeout]
|
|
112
|
+
timeout&.to_i
|
|
75
113
|
end
|
|
76
114
|
|
|
77
115
|
# @rbs (Exception) -> bool
|
data/lib/solid_objects/doctor.rb
CHANGED
|
@@ -215,25 +215,63 @@ module SolidObjects
|
|
|
215
215
|
# @rbs () -> Check
|
|
216
216
|
def check_sync_round_trip
|
|
217
217
|
actor_id = SecureRandom.uuid
|
|
218
|
+
probe_registry = ProcessRegistry.new
|
|
219
|
+
check = run_sync_probe(actor_id, probe_registry)
|
|
220
|
+
leftovers = remove_probe_records(actor_id:, probe_registry:)
|
|
221
|
+
return check if leftovers.empty? || check.failed?
|
|
222
|
+
|
|
223
|
+
warn_check(
|
|
224
|
+
:sync_round_trip,
|
|
225
|
+
"#{check.message}; could not remove the #{leftovers.join(" and ")}"
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @rbs (String, ProcessRegistry) -> Check
|
|
230
|
+
def run_sync_probe(actor_id, probe_registry)
|
|
231
|
+
probe_registry.register(kind: "caller", metadata: { execution: "doctor" })
|
|
218
232
|
value = SecureRandom.hex(8)
|
|
219
|
-
process_registry = SolidObjects.caller_process.process_registry
|
|
220
|
-
reference = ProbeActor.ref(actor_id)
|
|
221
233
|
message_reference = Mailbox.new.enqueue(
|
|
222
|
-
|
|
234
|
+
ProbeActor.ref(actor_id),
|
|
223
235
|
:ping,
|
|
224
236
|
{ value: },
|
|
225
237
|
kind: "sync"
|
|
226
238
|
)
|
|
227
|
-
result = SynchronousInvocation
|
|
239
|
+
result = SynchronousInvocation
|
|
240
|
+
.new(process_registry: probe_registry)
|
|
241
|
+
.call(message_reference, timeout: 5.seconds)
|
|
228
242
|
raise Error, "unexpected round-trip result" unless result == value
|
|
229
243
|
|
|
230
244
|
pass(:sync_round_trip, "durable synchronous actor call completed without a worker")
|
|
231
245
|
rescue => error
|
|
232
246
|
fail_check(:sync_round_trip, "#{error.class}: #{error.message}")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @rbs (actor_id: String, probe_registry: ProcessRegistry) -> Array[String]
|
|
250
|
+
def remove_probe_records(actor_id:, probe_registry:)
|
|
251
|
+
leftovers = []
|
|
252
|
+
leftovers << "probe actor" unless delete_probe_actor(actor_id)
|
|
253
|
+
leftovers << "probe caller process" unless delete_probe_caller_process(probe_registry)
|
|
254
|
+
leftovers
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# @rbs (String) -> bool
|
|
258
|
+
def delete_probe_actor(actor_id)
|
|
259
|
+
Instance.where(actor_type: ProbeActor.actor_type, actor_id:).delete_all
|
|
260
|
+
true
|
|
261
|
+
rescue
|
|
262
|
+
false
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @rbs (ProcessRegistry) -> bool
|
|
266
|
+
def delete_probe_caller_process(probe_registry)
|
|
267
|
+
process_record = probe_registry.process_record
|
|
268
|
+
return true unless process_record
|
|
269
|
+
|
|
270
|
+
probe_registry.stop
|
|
271
|
+
process_record.delete
|
|
272
|
+
true
|
|
273
|
+
rescue
|
|
274
|
+
false
|
|
237
275
|
end
|
|
238
276
|
|
|
239
277
|
# @rbs (Check, Check) -> bool
|
|
@@ -6,6 +6,13 @@ require "solid_objects/sync_diagnostics"
|
|
|
6
6
|
|
|
7
7
|
module SolidObjects
|
|
8
8
|
class SynchronousInvocation
|
|
9
|
+
# @rbs @dedicated_process_registry: ProcessRegistry?
|
|
10
|
+
|
|
11
|
+
# @rbs (?process_registry: ProcessRegistry?) -> void
|
|
12
|
+
def initialize(process_registry: nil)
|
|
13
|
+
@dedicated_process_registry = process_registry
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
10
17
|
def call(message_reference, timeout:)
|
|
11
18
|
return call_before_deadline(message_reference, timeout:) if SyncDeadline.active?
|
|
@@ -92,9 +99,17 @@ module SolidObjects
|
|
|
92
99
|
)
|
|
93
100
|
end
|
|
94
101
|
|
|
102
|
+
# @rbs () -> ProcessRegistry
|
|
103
|
+
def process_registry
|
|
104
|
+
dedicated_registry = @dedicated_process_registry
|
|
105
|
+
return SolidObjects.caller_process.process_registry unless dedicated_registry
|
|
106
|
+
|
|
107
|
+
dedicated_registry.tap(&:heartbeat)
|
|
108
|
+
end
|
|
109
|
+
|
|
95
110
|
# @rbs (Message, deadline: Float) -> Integer
|
|
96
111
|
def assist(message, deadline:)
|
|
97
|
-
process_registry =
|
|
112
|
+
process_registry = self.process_registry
|
|
98
113
|
activation = ActivationManager
|
|
99
114
|
.new(owner_id: process_registry.process_record.id)
|
|
100
115
|
.claim(instance_id: message.instance_id)
|
|
@@ -7,6 +7,12 @@ module SolidObjects
|
|
|
7
7
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
+
# @rbs (Hash[Symbol, untyped]) -> void
|
|
11
|
+
def refresh: (Hash[Symbol, untyped]) -> void
|
|
12
|
+
|
|
13
|
+
# @rbs (ComponentRegistration) -> Hash[Symbol, untyped]
|
|
14
|
+
def registration_payload: (ComponentRegistration) -> Hash[Symbol, untyped]
|
|
15
|
+
|
|
10
16
|
# @rbs () -> Array[Integer]
|
|
11
17
|
def requested_revision_key: () -> Array[Integer]
|
|
12
18
|
|
|
@@ -26,6 +26,15 @@ module SolidObjects
|
|
|
26
26
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
27
27
|
def with_transaction_deadline: (untyped) { () -> untyped } -> untyped
|
|
28
28
|
|
|
29
|
+
# @rbs (untyped) -> Hash[Symbol, untyped]?
|
|
30
|
+
def restorable_busy_wait: (untyped) -> Hash[Symbol, untyped]?
|
|
31
|
+
|
|
32
|
+
# @rbs (untyped, Hash[Symbol, untyped]) -> void
|
|
33
|
+
def restore_busy_wait: (untyped, Hash[Symbol, untyped]) -> void
|
|
34
|
+
|
|
35
|
+
# @rbs (untyped) -> Integer?
|
|
36
|
+
def configured_busy_handler_timeout: (untyped) -> Integer?
|
|
37
|
+
|
|
29
38
|
# @rbs (Exception) -> bool
|
|
30
39
|
def deadline_error?: (Exception) -> bool
|
|
31
40
|
|
|
@@ -78,6 +78,18 @@ module SolidObjects
|
|
|
78
78
|
# @rbs () -> Check
|
|
79
79
|
def check_sync_round_trip: () -> Check
|
|
80
80
|
|
|
81
|
+
# @rbs (String, ProcessRegistry) -> Check
|
|
82
|
+
def run_sync_probe: (String, ProcessRegistry) -> Check
|
|
83
|
+
|
|
84
|
+
# @rbs (actor_id: String, probe_registry: ProcessRegistry) -> Array[String]
|
|
85
|
+
def remove_probe_records: (actor_id: String, probe_registry: ProcessRegistry) -> Array[String]
|
|
86
|
+
|
|
87
|
+
# @rbs (String) -> bool
|
|
88
|
+
def delete_probe_actor: (String) -> bool
|
|
89
|
+
|
|
90
|
+
# @rbs (ProcessRegistry) -> bool
|
|
91
|
+
def delete_probe_caller_process: (ProcessRegistry) -> bool
|
|
92
|
+
|
|
81
93
|
# @rbs (Check, Check) -> bool
|
|
82
94
|
def ready_for_round_trip?: (Check, Check) -> bool
|
|
83
95
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class SynchronousInvocation
|
|
5
|
+
@dedicated_process_registry: ProcessRegistry?
|
|
6
|
+
|
|
7
|
+
# @rbs (?process_registry: ProcessRegistry?) -> void
|
|
8
|
+
def initialize: (?process_registry: ProcessRegistry?) -> void
|
|
9
|
+
|
|
5
10
|
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
6
11
|
def call: (MessageReference, timeout: Numeric) -> untyped
|
|
7
12
|
|
|
@@ -22,6 +27,9 @@ module SolidObjects
|
|
|
22
27
|
# @rbs (Message) -> bot
|
|
23
28
|
def raise_rejection: (Message) -> bot
|
|
24
29
|
|
|
30
|
+
# @rbs () -> ProcessRegistry
|
|
31
|
+
def process_registry: () -> ProcessRegistry
|
|
32
|
+
|
|
25
33
|
# @rbs (Message, deadline: Float) -> Integer
|
|
26
34
|
def assist: (Message, deadline: Float) -> Integer
|
|
27
35
|
|
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.5.
|
|
4
|
+
version: 0.5.1
|
|
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-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|