solid_objects 0.13.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +7 -7
- data/app/controllers/solid_objects/transmissions_controller.rb +32 -0
- data/config/routes.rb +1 -0
- data/docs/research/solid_queue.md +2 -2
- data/docs/roadmap.md +18 -1
- data/docs/state-migrations.md +1 -1
- data/docs/transmission.md +198 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +17 -1
- data/lib/solid_objects/actor.rb +11 -0
- data/lib/solid_objects/configuration.rb +7 -1
- data/lib/solid_objects/errors.rb +3 -0
- data/lib/solid_objects/transmission.rb +139 -0
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +16 -0
- data/sig/generated/controllers/solid_objects/transmissions_controller.rbs +13 -0
- data/sig/generated/lib/solid_objects/actor.rbs +3 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
- data/sig/generated/lib/solid_objects/errors.rbs +3 -0
- data/sig/generated/lib/solid_objects/transmission.rbs +34 -0
- data/sig/generated/lib/solid_objects.rbs +3 -0
- data/sig/support/framework.rbs +3 -0
- metadata +11 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a64ced341585dcb4b7e9240b2076e1f47a352b4786806c1bec08c10d91c7d187
|
|
4
|
+
data.tar.gz: 6d53730c69b2f86cde60d3fdb1ffcefe10dd362dc98652b4afe2b367e1134f32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acc3e7a6bb436e0dbadf5a38478fc21d3b072f11fcee0cfed26960cba9c1d8eb790a99cf3b065457de8c2ced86d01718c854635d210ba23ffe571e009eab95f7
|
|
7
|
+
data.tar.gz: 91faab1647c20ba461dcf065612b549b2f6dbd955f2a2222f89a2556d377b99b4a648dbb71ba441ca01ba1281607292f3f21300256ad65f55c821374dac85d7e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-08-22
|
|
4
|
+
|
|
5
|
+
- Add `SolidObjects::Transmission.receive(envelope)`, the server ingest for
|
|
6
|
+
the browser transmit family in solid-objects-js. It validates a camelCase
|
|
7
|
+
transmit envelope, resolves the actor type through an optional
|
|
8
|
+
`resolve_actor_type:` proc, and enqueues one internal message with the
|
|
9
|
+
idempotency key `transmit:<effectId>`, so a replayed envelope applies
|
|
10
|
+
once. Malformed envelopes raise the new
|
|
11
|
+
`SolidObjects::InvalidTransmission`. Internal delivery skips
|
|
12
|
+
`authorize_message`, so the host application must authenticate the
|
|
13
|
+
request before it calls `receive`; see `docs/transmission.md` for the
|
|
14
|
+
controller boundary. On a registry miss under Rails, `receive` loads the
|
|
15
|
+
application's actor classes once and retries, because a lazy-loading web
|
|
16
|
+
process has no other reason to have loaded the target class. Golden
|
|
17
|
+
fixtures in `compatibility/transmit-envelopes.json` pin the wire contract
|
|
18
|
+
shared with the JS runtime.
|
|
19
|
+
- Add `Actor#transmit` and `SolidObjects.register_transmit`, the staging
|
|
20
|
+
side of the transmit family. `transmit.increment(amount:)` stages a
|
|
21
|
+
`solid-objects.transmit` effect in the same commit as the state change;
|
|
22
|
+
`register_transmit` drains staged effects into camelCase envelopes and
|
|
23
|
+
hands each to the delivery block, which raises to retry. A claimed
|
|
24
|
+
transmit effect delivers every undelivered sibling for its actor up to
|
|
25
|
+
its own mailbox sequence, oldest first, so per-actor order survives a
|
|
26
|
+
failed delivery, and the receiving side dedups on `transmit:<effectId>`.
|
|
27
|
+
A raw `emit "solid-objects.transmit"` with explicit `actorType` and
|
|
28
|
+
`actorId` targets a different actor, matching the JS staging surface.
|
|
29
|
+
- Mount `POST /solid_objects/transmit` in the engine, an ingest route
|
|
30
|
+
behind the new deny-by-default `authorize_transmission` policy. The
|
|
31
|
+
policy receives the parsed envelope and the controller, an unauthorized
|
|
32
|
+
envelope gets 403, and a permanently unappliable one gets 422, so a
|
|
33
|
+
sending outbox dead-letters it instead of retrying forever. The new
|
|
34
|
+
`transmission_actor_type_resolver` configuration maps diverged actor
|
|
35
|
+
type names for the engine route.
|
|
36
|
+
|
|
3
37
|
## 0.13.3 - 2026-08-18
|
|
4
38
|
|
|
5
39
|
- Stop loading `ActiveRecord::Base` when the gem is required. The engine now
|
data/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Solid Objects
|
|
2
2
|
|
|
3
|
-
[](https://github.com/cardmagic/solid-objects-ruby/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
|
-
Solid Objects
|
|
8
|
-
durable state, serialized turns, alarms,
|
|
9
|
-
|
|
10
|
-
application already has,
|
|
7
|
+
Solid Objects ports the Durable Objects programming model to ordinary Rails
|
|
8
|
+
applications: addressable objects, durable state, serialized turns, alarms,
|
|
9
|
+
and live clients. It runs on the MySQL, PostgreSQL, or SQLite database that
|
|
10
|
+
the application already has, in the database-backed operating model of the
|
|
11
11
|
Solid family. No Redis, Cloudflare account, or separate actor service is
|
|
12
12
|
required.
|
|
13
13
|
|
|
@@ -113,7 +113,7 @@ maps those ideas into Rails:
|
|
|
113
113
|
| Storage deletion | Authorized `reference.destroy` |
|
|
114
114
|
| Cloudflare Workers platform | Your Rails processes and SQL database |
|
|
115
115
|
|
|
116
|
-
Rails already has
|
|
116
|
+
Rails already has tools for jobs, records, and realtime transport.
|
|
117
117
|
None of those primitives alone provides this complete stateful-object shape.
|
|
118
118
|
Solid Objects adds five capabilities:
|
|
119
119
|
|
|
@@ -1273,7 +1273,7 @@ for staged cutovers.
|
|
|
1273
1273
|
| --- | --- |
|
|
1274
1274
|
| Cloudflare Durable Objects | Solid Objects ports the named, stateful, serialized-object model to Ruby and Rails. It uses your SQL database and Rails workers rather than Cloudflare's globally distributed serverless runtime, placement, and storage APIs. |
|
|
1275
1275
|
| Active Job | Jobs are independent work units. Solid Objects adds addressable identity, durable state, explicit per-identity order, activation leases, and fencing. |
|
|
1276
|
-
| Solid Queue | Solid Queue is
|
|
1276
|
+
| Solid Queue | Solid Queue is a database backend for Active Job. Its concurrency controls cap overlap but do not guarantee order. Solid Objects provides actor mailboxes, state, fencing, per-identity reminders, and state-driven views. |
|
|
1277
1277
|
| Action Cable | Cable transports transient realtime messages. Solid Objects owns durable state and work; Cable is an optional delivery path for committed observable projections. |
|
|
1278
1278
|
| Orleans | Orleans provides the virtual-actor lineage behind the model, with grains, reminders, and activation lifecycle. Solid Objects is a smaller Rails-native runtime and does not match Orleans clustering or placement breadth. |
|
|
1279
1279
|
| Active Record service object | A service object runs directly against records. Solid Objects adds durable asynchronous ordering, retries, activation fencing, reminders, and outboxes at greater operational cost. |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require "action_controller/api"
|
|
4
|
+
|
|
5
|
+
module SolidObjects
|
|
6
|
+
class TransmissionsController < ActionController::API
|
|
7
|
+
# @rbs () -> void
|
|
8
|
+
def create
|
|
9
|
+
envelope = JSON.parse(request.body.read)
|
|
10
|
+
return head :forbidden unless authorized_transmission?(envelope)
|
|
11
|
+
|
|
12
|
+
Transmission.receive(
|
|
13
|
+
envelope,
|
|
14
|
+
resolve_actor_type: SolidObjects.configuration.transmission_actor_type_resolver
|
|
15
|
+
)
|
|
16
|
+
head :ok
|
|
17
|
+
rescue JSON::ParserError, InvalidTransmission, UnknownActorType, UnknownMessage,
|
|
18
|
+
PayloadTooLarge, IdempotencyConflict
|
|
19
|
+
head :unprocessable_entity
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @rbs (untyped) -> bool
|
|
25
|
+
def authorized_transmission?(envelope)
|
|
26
|
+
SolidObjects.configuration.authorize_transmission.call(
|
|
27
|
+
envelope:,
|
|
28
|
+
authorization_context: self
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -526,8 +526,8 @@ Source: [Cloudflare Durable Objects overview](https://developers.cloudflare.com/
|
|
|
526
526
|
- Solid Queue findings are tied to v1.6.0. Earlier releases did not have all current async/fiber supervision and concurrency finalization behavior.
|
|
527
527
|
- PostgreSQL documentation inspected was current PostgreSQL 18 documentation. `SKIP LOCKED` has existed since PostgreSQL 9.5, but Solid Objects supports PostgreSQL 14 and newer.
|
|
528
528
|
- MySQL documentation inspected was MySQL 8.4. Solid Objects supports MySQL 8.0 and newer with InnoDB.
|
|
529
|
-
- SQLite documentation inspected covers current SQLite behavior. Solid Objects
|
|
530
|
-
- Rails documentation and source inspected cover Rails 8.1. Solid Objects requires Rails
|
|
529
|
+
- SQLite documentation inspected covers current SQLite behavior. Solid Objects supports SQLite 3.35 or newer, the oldest server version the adapter accepts.
|
|
530
|
+
- Rails documentation and source inspected cover Rails 8.1. Solid Objects requires Rails 7.1 or newer. Rails 8 changed the SQLite adapter's default write transaction from deferred to immediate, and the test suite also passes on Rails 7.1 and 7.2 with the deferred default. Rails 7.0 stays out of range because its SQLite adapter requires sqlite3 1.4, and the busy-handler control this gem depends on arrived in sqlite3 2.x.
|
|
531
531
|
- Orleans documentation describes current Orleans behavior, not a compatibility promise for this Ruby implementation.
|
|
532
532
|
- `LISTEN/NOTIFY` and Action Cable are optimizations and delivery channels, never durable truth.
|
|
533
533
|
- A separate actor database is compatible only when all rows participating in an atomic actor commit, including message, state, effects, and broadcasts, live in that same actor database.
|
data/docs/roadmap.md
CHANGED
|
@@ -72,6 +72,20 @@
|
|
|
72
72
|
Rails 7.1 and 7.2 is unmeasured against those servers. Rails 7.0 is out of
|
|
73
73
|
range because its SQLite adapter requires `sqlite3 ~> 1.4`, and this gem needs
|
|
74
74
|
the busy-handler control that arrived in `sqlite3` 2.x
|
|
75
|
+
- The transmit family, both sides. `SolidObjects::Transmission.receive` is
|
|
76
|
+
the ingest: envelope validation, actor type resolution with a per-call
|
|
77
|
+
`resolve_actor_type:` escape hatch, and an internal idempotent enqueue
|
|
78
|
+
keyed `transmit:<effectId>`. `Actor#transmit` and
|
|
79
|
+
`SolidObjects.register_transmit` are the staging side: a transactional
|
|
80
|
+
`solid-objects.transmit` effect and a drain that delivers every
|
|
81
|
+
undelivered sibling for the actor up to the claimed effect's mailbox
|
|
82
|
+
sequence, oldest first, so per-actor order survives a failed delivery.
|
|
83
|
+
The wire contract is pinned by golden fixtures in
|
|
84
|
+
`compatibility/transmit-envelopes.json`. The engine mounts
|
|
85
|
+
`POST /solid_objects/transmit` behind a deny-by-default
|
|
86
|
+
`authorize_transmission` policy with a configurable actor type resolver.
|
|
87
|
+
Bidirectional replication as a declared surface, with echo suppression,
|
|
88
|
+
is not implemented
|
|
75
89
|
- A JavaScript suite covering every browser module, run in CI with Node's test
|
|
76
90
|
runner and jsdom, plus a browser suite running the same modules against real
|
|
77
91
|
Chromium and a real Turbo build, with every GitHub Actions reference pinned to
|
|
@@ -129,7 +143,10 @@
|
|
|
129
143
|
of who pressed what, and bulk-safe tools: retry is one dead letter at a time,
|
|
130
144
|
because `DeadLetterManager` exposes no bulk operation. Pause is an operator
|
|
131
145
|
brake and not a stop, since a pass already in flight finishes its turn and a
|
|
132
|
-
synchronous caller waiting on a paused instance times out.
|
|
146
|
+
synchronous caller waiting on a paused instance times out. Retry also only
|
|
147
|
+
exists for message dead letters: a dead effect or broadcast has no retry
|
|
148
|
+
API, which matters for transmit effects because a dead one is a lost
|
|
149
|
+
replay until an operator returns its row to pending. The page cost was
|
|
133
150
|
reasoned about rather than measured: the summary bar issues a fixed set of
|
|
134
151
|
indexed aggregate queries per page, which is why `HEAD /` exists for uptime
|
|
135
152
|
monitors, but no dashboard latency has been benchmarked against a large
|
data/docs/state-migrations.md
CHANGED
|
@@ -47,6 +47,6 @@ A safe destructive rollout normally uses:
|
|
|
47
47
|
Never update actor JSON in a bulk SQL migration. Use actor messages so fencing,
|
|
48
48
|
ordering, observables, and outboxes remain intact.
|
|
49
49
|
|
|
50
|
-
This guide covers
|
|
50
|
+
This guide covers changes to state that Solid Objects already owns. To move
|
|
51
51
|
existing Redis, key-value, or relational state into actors without downtime,
|
|
52
52
|
use the [legacy-state migration cookbook](migrating-existing-state.md).
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# The transmit family
|
|
2
|
+
|
|
3
|
+
The transmit family replays one runtime's actor operations onto another
|
|
4
|
+
runtime over a shared wire contract. The Ruby gem holds both sides:
|
|
5
|
+
|
|
6
|
+
- `Actor#transmit` and `SolidObjects.register_transmit` stage and deliver
|
|
7
|
+
envelopes. This is the sending side.
|
|
8
|
+
- `SolidObjects::Transmission.receive(envelope)` ingests envelopes. This is
|
|
9
|
+
the receiving side.
|
|
10
|
+
|
|
11
|
+
[solid-objects-js](https://github.com/cardmagic/solid-objects-js) holds the
|
|
12
|
+
same two sides for the browser and Node. A browser actor stages a transmit
|
|
13
|
+
intent with `this.transmit().increment({ amount })` in the same transaction
|
|
14
|
+
as its state change; its effect worker drains that outbox with
|
|
15
|
+
at-least-once delivery, per-actor order, and retry backoff, and posts one
|
|
16
|
+
JSON envelope per effect to a route the host application owns. A Rails
|
|
17
|
+
actor does the same with `transmit.increment(amount:)`. Either ingest
|
|
18
|
+
accepts either sender, so Rails-to-Rails, Rails-to-Node, Node-to-Rails,
|
|
19
|
+
and browser-to-Rails replication all ride one contract.
|
|
20
|
+
|
|
21
|
+
## The sending side
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class Counter < SolidObjects::Actor
|
|
25
|
+
actor_type "counters"
|
|
26
|
+
|
|
27
|
+
attribute :count, default: 0
|
|
28
|
+
|
|
29
|
+
def increment(amount: 1)
|
|
30
|
+
self.count += amount
|
|
31
|
+
transmit.increment(amount:)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
SolidObjects.register_transmit do |envelope|
|
|
36
|
+
DeliverToUpstream.call(envelope)
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`transmit` returns the same fluent dispatcher `schedule` returns. It stages
|
|
41
|
+
one `solid-objects.transmit` effect in the same commit as the state change,
|
|
42
|
+
targeting the same operation on the same actor in the receiving runtime. For
|
|
43
|
+
a different target, stage the effect directly:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
emit "solid-objects.transmit",
|
|
47
|
+
operation: "increment",
|
|
48
|
+
arguments: { amount: 2 },
|
|
49
|
+
actorType: "other-counters",
|
|
50
|
+
actorId: "counter-1"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`SolidObjects.register_transmit(&deliver)` registers the drain handler for
|
|
54
|
+
that effect. The block receives one camelCase envelope per staged effect.
|
|
55
|
+
Raise inside the block while the upstream is unreachable; the effect
|
|
56
|
+
retries with backoff and dead-letters on exhaustion, like any other effect.
|
|
57
|
+
|
|
58
|
+
The drain keeps per-actor order across failures: a claimed transmit effect
|
|
59
|
+
delivers every undelivered sibling for its actor up to its own mailbox
|
|
60
|
+
sequence, oldest first. The receiving side dedups on `transmit:<effectId>`,
|
|
61
|
+
so a redelivered envelope applies once.
|
|
62
|
+
|
|
63
|
+
Delivery is at-least-once by design, and the drain accepts redundant sends
|
|
64
|
+
as the price of ordering without cross-worker coordination. A drained
|
|
65
|
+
sibling's own effect row stays pending, because completing it would
|
|
66
|
+
require taking over another worker's claim; when its own claim runs, it
|
|
67
|
+
delivers again and the receiving side drops the replay. The same is true
|
|
68
|
+
when two workers claim effects for one actor concurrently. Both runtimes
|
|
69
|
+
share this behavior, and the Ruby suite pins it with a race test: order
|
|
70
|
+
holds, duplicates apply nothing, and every effect completes.
|
|
71
|
+
|
|
72
|
+
## Retry budget and offline tolerance
|
|
73
|
+
|
|
74
|
+
A raised delivery follows the effect retry policy: `max_attempts` (default
|
|
75
|
+
5) and `retry_delay` (default `2 ** (attempt - 1)` seconds, capped at 60).
|
|
76
|
+
The defaults give roughly fifteen seconds of offline tolerance before an
|
|
77
|
+
envelope dead-letters. An application that transmits across real outages
|
|
78
|
+
must raise both:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
SolidObjects.configure do |configuration|
|
|
82
|
+
configuration.max_attempts = 30
|
|
83
|
+
configuration.retry_delay = ->(attempt) { [ 2**(attempt - 1), 300 ].min.to_f }
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
These settings apply to every effect, not only transmits. A dead transmit
|
|
88
|
+
effect has no retry API; the dashboard lists it, and recovery means
|
|
89
|
+
returning its row to `pending` with a cleared `attempt_count`. Order
|
|
90
|
+
survives that recovery, because the drain orders by mailbox sequence, not
|
|
91
|
+
by retry time.
|
|
92
|
+
|
|
93
|
+
## Wire contract
|
|
94
|
+
|
|
95
|
+
The JS side owns the envelope format. The Ruby ingest accepts it verbatim.
|
|
96
|
+
|
|
97
|
+
- Keys arrive camelCase: `effectId`, `actorType`, `actorId`, `operation`,
|
|
98
|
+
and an optional `arguments` object. There is no snake_case dialect.
|
|
99
|
+
- The idempotency key is `transmit:<effectId>`, byte-identical to the JS
|
|
100
|
+
server ingest. A replayed envelope applies once.
|
|
101
|
+
- Delivery is at-least-once and per-actor ordered by the browser's drain.
|
|
102
|
+
The server preserves mailbox order and adds no ordering of its own.
|
|
103
|
+
|
|
104
|
+
`compatibility/transmit-envelopes.json` pins the contract. Both runtimes
|
|
105
|
+
run a consuming test against the same fixture file.
|
|
106
|
+
|
|
107
|
+
## What `receive` does
|
|
108
|
+
|
|
109
|
+
1. It validates the envelope shape. A malformed envelope raises
|
|
110
|
+
`SolidObjects::InvalidTransmission`.
|
|
111
|
+
2. It resolves the actor type and looks it up in the registry. On a miss
|
|
112
|
+
under Rails it loads the application's actor classes once and retries,
|
|
113
|
+
because a lazy-loading process has no other reason to have loaded the
|
|
114
|
+
target class. A type that is still unknown raises
|
|
115
|
+
`SolidObjects::UnknownActorType`. An undeclared operation raises
|
|
116
|
+
`SolidObjects::UnknownMessage`.
|
|
117
|
+
3. It enqueues one internal message with the idempotency key
|
|
118
|
+
`transmit:<effectId>`. Oversized arguments raise
|
|
119
|
+
`SolidObjects::PayloadTooLarge` before persistence.
|
|
120
|
+
|
|
121
|
+
The enqueue uses `delivery_mode: "internal"`, the same mode the effect
|
|
122
|
+
executor uses. Internal delivery skips `authorize_message` by construction.
|
|
123
|
+
The host application must authenticate the request before it calls
|
|
124
|
+
`receive`.
|
|
125
|
+
|
|
126
|
+
## The engine route
|
|
127
|
+
|
|
128
|
+
The engine mounts `POST /solid_objects/transmit` (under wherever the host
|
|
129
|
+
mounts `SolidObjects::Engine`). It parses the body, authorizes it through
|
|
130
|
+
`authorize_transmission`, and passes it to `Transmission.receive` with the
|
|
131
|
+
configured `transmission_actor_type_resolver`. The policy denies by
|
|
132
|
+
default, because the ingest skips `authorize_message` by design; an
|
|
133
|
+
unauthorized envelope gets 403, and an envelope the server can never apply
|
|
134
|
+
gets 422:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
SolidObjects.configure do |configuration|
|
|
138
|
+
configuration.authorize_transmission = lambda do |envelope:, authorization_context:|
|
|
139
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
|
140
|
+
authorization_context.request.headers["Authorization"].to_s,
|
|
141
|
+
"Bearer #{Rails.application.credentials.transmit_token}"
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The policy receives the parsed envelope and the controller as
|
|
148
|
+
`authorization_context:`, so it can bind `actorType` and `actorId` to the
|
|
149
|
+
authenticated caller, not only check a shared token. Rate limits stay with
|
|
150
|
+
the host (Rack::Attack or the proxy), the same boundary the dashboard
|
|
151
|
+
draws.
|
|
152
|
+
|
|
153
|
+
## A hand-rolled route
|
|
154
|
+
|
|
155
|
+
An application that wants its own controller keeps the same shape:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class TransmitController < ApplicationController
|
|
159
|
+
skip_forgery_protection
|
|
160
|
+
|
|
161
|
+
def create
|
|
162
|
+
head :forbidden and return unless authenticated_device?
|
|
163
|
+
|
|
164
|
+
SolidObjects::Transmission.receive(JSON.parse(request.body.read))
|
|
165
|
+
head :ok
|
|
166
|
+
rescue SolidObjects::InvalidTransmission, SolidObjects::UnknownActorType,
|
|
167
|
+
SolidObjects::UnknownMessage, SolidObjects::PayloadTooLarge,
|
|
168
|
+
SolidObjects::IdempotencyConflict, JSON::ParserError
|
|
169
|
+
head :unprocessable_entity
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Return 422 for an envelope the server can never apply. The browser outbox
|
|
175
|
+
dead-letters that effect instead of retrying it forever. Return a 5xx for a
|
|
176
|
+
transient server fault, so the browser retries with backoff.
|
|
177
|
+
`SolidObjects::IdempotencyConflict` belongs in the 422 list: it means the
|
|
178
|
+
effect id was replayed with a different invocation, and no retry can ever
|
|
179
|
+
make that envelope apply.
|
|
180
|
+
|
|
181
|
+
## Actor type mapping
|
|
182
|
+
|
|
183
|
+
When both runtimes use the same actor type strings, no configuration is
|
|
184
|
+
needed. When the names diverge, pass `resolve_actor_type:` per call:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
SolidObjects::Transmission.receive(
|
|
188
|
+
envelope,
|
|
189
|
+
resolve_actor_type: ->(actor_type) { actor_type.sub("browser-", "server-") }
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Scope
|
|
194
|
+
|
|
195
|
+
Bidirectional replication as a first-class surface, where two runtimes
|
|
196
|
+
declare a replica pair and echo suppression keeps a replayed operation
|
|
197
|
+
from transmitting back, is a separate feature. The transmit family gives
|
|
198
|
+
it the mechanism; the declaration API does not exist yet.
|
|
@@ -37,7 +37,7 @@ SolidObjects.configure do |configuration|
|
|
|
37
37
|
#
|
|
38
38
|
# Prefer policies that bind actor_type and actor_id to a trusted
|
|
39
39
|
# authorization_context. See:
|
|
40
|
-
# https://github.com/cardmagic/
|
|
40
|
+
# https://github.com/cardmagic/solid-objects-ruby/blob/main/docs/authorization.md
|
|
41
41
|
# and run:
|
|
42
42
|
#
|
|
43
43
|
# bin/rails solid_objects:doctor
|
|
@@ -49,6 +49,22 @@ SolidObjects.configure do |configuration|
|
|
|
49
49
|
configuration.authorize_subscription = ->(**) { false }
|
|
50
50
|
configuration.authorize_administration = ->(**) { false }
|
|
51
51
|
|
|
52
|
+
# The engine route POST /solid_objects/transmit ingests transmit envelopes
|
|
53
|
+
# from another Solid Objects runtime. It stays denied until its callers are
|
|
54
|
+
# authenticated, because the ingest skips authorize_message by design:
|
|
55
|
+
#
|
|
56
|
+
# configuration.authorize_transmission = lambda do |envelope:, authorization_context:|
|
|
57
|
+
# ActiveSupport::SecurityUtils.secure_compare(
|
|
58
|
+
# authorization_context.request.headers["Authorization"].to_s,
|
|
59
|
+
# "Bearer #{Rails.application.credentials.transmit_token}"
|
|
60
|
+
# )
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# When the sending runtime names actor types differently, map them here:
|
|
64
|
+
#
|
|
65
|
+
# configuration.transmission_actor_type_resolver = ->(actor_type) { actor_type.sub("browser-", "server-") }
|
|
66
|
+
configuration.authorize_transmission = ->(**) { false }
|
|
67
|
+
|
|
52
68
|
# Configure component_authorization_context to return the authenticated
|
|
53
69
|
# principal used for reactive component refreshes.
|
|
54
70
|
|
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -190,6 +190,17 @@ module SolidObjects
|
|
|
190
190
|
nil
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# @rbs () -> OperationDispatcher
|
|
194
|
+
def transmit
|
|
195
|
+
OperationDispatcher.new(
|
|
196
|
+
actor_type: self.class.actor_type,
|
|
197
|
+
handlers: self.class.definition.messages
|
|
198
|
+
) do |operation, arguments|
|
|
199
|
+
emit(Transmission::EFFECT_NAME, operation: operation.to_s, arguments:)
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
193
204
|
# @rbs (Symbol | String, **untyped) -> nil
|
|
194
205
|
def commit_action(name, **arguments)
|
|
195
206
|
CommitActionIntent.new(
|
|
@@ -47,6 +47,8 @@ module SolidObjects
|
|
|
47
47
|
# @rbs @authorize_destroy: Proc
|
|
48
48
|
# @rbs @authorize_subscription: Proc
|
|
49
49
|
# @rbs @authorize_administration: Proc
|
|
50
|
+
# @rbs @authorize_transmission: Proc
|
|
51
|
+
# @rbs @transmission_actor_type_resolver: Proc
|
|
50
52
|
|
|
51
53
|
attr_accessor :table_name_prefix,
|
|
52
54
|
:polling_interval,
|
|
@@ -92,7 +94,9 @@ module SolidObjects
|
|
|
92
94
|
:authorize_query,
|
|
93
95
|
:authorize_destroy,
|
|
94
96
|
:authorize_subscription,
|
|
95
|
-
:authorize_administration
|
|
97
|
+
:authorize_administration,
|
|
98
|
+
:authorize_transmission,
|
|
99
|
+
:transmission_actor_type_resolver
|
|
96
100
|
|
|
97
101
|
# @rbs @additional_components: Array[untyped]
|
|
98
102
|
attr_reader :additional_components
|
|
@@ -148,6 +152,8 @@ module SolidObjects
|
|
|
148
152
|
@authorize_destroy = ->(**) { false }
|
|
149
153
|
@authorize_subscription = ->(**) { false }
|
|
150
154
|
@authorize_administration = ->(**) { false }
|
|
155
|
+
@authorize_transmission = ->(**) { false }
|
|
156
|
+
@transmission_actor_type_resolver = ->(actor_type) { actor_type }
|
|
151
157
|
@additional_components = []
|
|
152
158
|
end
|
|
153
159
|
|
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
module Transmission
|
|
5
|
+
REQUIRED_FIELDS = %w[effectId actorType actorId operation].freeze
|
|
6
|
+
IDEMPOTENCY_PREFIX = "transmit:"
|
|
7
|
+
EFFECT_NAME = "solid-objects.transmit"
|
|
8
|
+
UNDELIVERED_STATUSES = %w[pending processing].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# @rbs (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
|
|
12
|
+
def receive(envelope, resolve_actor_type: :itself.to_proc, mailbox: Mailbox.new, actor_loader: method(:load_application_actors))
|
|
13
|
+
validate!(envelope)
|
|
14
|
+
|
|
15
|
+
actor_type = resolve_actor_type.call(envelope["actorType"]).to_s
|
|
16
|
+
actor_class = fetch_actor_class(actor_type, actor_loader)
|
|
17
|
+
operation = envelope["operation"].to_sym
|
|
18
|
+
unless actor_class.definition.messages.key?(operation)
|
|
19
|
+
raise UnknownMessage, "unknown operation #{envelope["operation"].inspect}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
mailbox.enqueue(
|
|
23
|
+
reference: Reference.new(actor_type:, actor_id: envelope["actorId"]),
|
|
24
|
+
operation:,
|
|
25
|
+
arguments: envelope.fetch("arguments", {}),
|
|
26
|
+
delivery_mode: "internal",
|
|
27
|
+
idempotency_key: "#{IDEMPOTENCY_PREFIX}#{envelope["effectId"]}"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @rbs (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
|
|
32
|
+
def deliver_through(effect_name:, arguments:, context:, deliver:)
|
|
33
|
+
staged_envelope(
|
|
34
|
+
arguments,
|
|
35
|
+
effect_id: context.id,
|
|
36
|
+
actor_type: context.actor_type,
|
|
37
|
+
actor_id: context.actor_id
|
|
38
|
+
)
|
|
39
|
+
undelivered_envelopes_through(effect_name:, context:).each do |envelope|
|
|
40
|
+
deliver.call(envelope)
|
|
41
|
+
end
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# @rbs (String, ^() -> bool) -> Class
|
|
48
|
+
def fetch_actor_class(actor_type, actor_loader)
|
|
49
|
+
SolidObjects.registry.fetch(actor_type)
|
|
50
|
+
rescue UnknownActorType
|
|
51
|
+
raise unless actor_loader.call
|
|
52
|
+
|
|
53
|
+
SolidObjects.registry.fetch(actor_type)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs () -> bool
|
|
57
|
+
def load_application_actors
|
|
58
|
+
return false unless defined?(Rails.application) && Rails.application
|
|
59
|
+
|
|
60
|
+
ApplicationActorLoader.new.call
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @rbs (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
|
|
65
|
+
def staged_envelope(arguments, effect_id:, actor_type:, actor_id:)
|
|
66
|
+
operation = arguments["operation"]
|
|
67
|
+
unless operation.is_a?(String) && !operation.empty?
|
|
68
|
+
raise InvalidTransmission, "transmit effect arguments require a non-empty operation"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
target_arguments = arguments["arguments"]
|
|
72
|
+
target_arguments = {} if target_arguments.nil?
|
|
73
|
+
unless target_arguments.is_a?(Hash)
|
|
74
|
+
raise InvalidTransmission, %(transmit effect arguments must hold a JSON object in "arguments")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
target_type = arguments["actorType"].nil? ? actor_type : arguments["actorType"]
|
|
78
|
+
target_id = arguments["actorId"].nil? ? actor_id : arguments["actorId"]
|
|
79
|
+
{ "actorType" => target_type, "actorId" => target_id }.each do |field, value|
|
|
80
|
+
next if value.is_a?(String) && !value.empty?
|
|
81
|
+
|
|
82
|
+
raise InvalidTransmission, "transmit effect #{field} must be a non-empty string"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
"effectId" => effect_id,
|
|
87
|
+
"actorType" => target_type,
|
|
88
|
+
"actorId" => target_id,
|
|
89
|
+
"operation" => operation,
|
|
90
|
+
"arguments" => target_arguments
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @rbs (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
|
|
95
|
+
def undelivered_envelopes_through(effect_name:, context:)
|
|
96
|
+
source_sequence = Message.find(context.source_message_id).sequence
|
|
97
|
+
effects = Effect
|
|
98
|
+
.joins(:message)
|
|
99
|
+
.where(name: effect_name, status: UNDELIVERED_STATUSES)
|
|
100
|
+
.merge(
|
|
101
|
+
Message.where(
|
|
102
|
+
actor_type: context.actor_type,
|
|
103
|
+
actor_id: context.actor_id,
|
|
104
|
+
sequence: ..source_sequence
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
.order(Message.arel_table[:sequence].asc, :id)
|
|
108
|
+
|
|
109
|
+
effects.filter_map do |effect|
|
|
110
|
+
staged_envelope(
|
|
111
|
+
effect.arguments,
|
|
112
|
+
effect_id: effect.effect_id,
|
|
113
|
+
actor_type: context.actor_type,
|
|
114
|
+
actor_id: context.actor_id
|
|
115
|
+
)
|
|
116
|
+
rescue InvalidTransmission
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs (untyped) -> void
|
|
122
|
+
def validate!(envelope)
|
|
123
|
+
raise InvalidTransmission, "envelope must be a JSON object" unless envelope.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
REQUIRED_FIELDS.each do |field|
|
|
126
|
+
value = envelope[field]
|
|
127
|
+
next if value.is_a?(String) && !value.empty?
|
|
128
|
+
|
|
129
|
+
raise InvalidTransmission, "envelope field #{field.inspect} must be a non-empty string"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
arguments = envelope.fetch("arguments", {})
|
|
133
|
+
return if arguments.is_a?(Hash)
|
|
134
|
+
|
|
135
|
+
raise InvalidTransmission, %(envelope field "arguments" must be a JSON object)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/solid_objects.rb
CHANGED
|
@@ -63,6 +63,8 @@ require "solid_objects/lease_renewer"
|
|
|
63
63
|
# was reachable only through the caller path, so requiring the gem was not
|
|
64
64
|
# enough to run a role that uses it.
|
|
65
65
|
require "solid_objects/mailbox"
|
|
66
|
+
require "solid_objects/application_actor_loader"
|
|
67
|
+
require "solid_objects/transmission"
|
|
66
68
|
require "solid_objects/worker"
|
|
67
69
|
require "solid_objects/effect_executor"
|
|
68
70
|
require "solid_objects/reminder_scheduler"
|
|
@@ -100,6 +102,20 @@ module SolidObjects
|
|
|
100
102
|
effect_registry.register(name, handler)
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
# @rbs (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
|
|
106
|
+
def register_transmit(effect_name: Transmission::EFFECT_NAME, &deliver)
|
|
107
|
+
raise ArgumentError, "register_transmit requires a delivery block" unless deliver
|
|
108
|
+
|
|
109
|
+
register_effect(effect_name) do |arguments, context|
|
|
110
|
+
Transmission.deliver_through(
|
|
111
|
+
effect_name: effect_name.to_s,
|
|
112
|
+
arguments:,
|
|
113
|
+
context:,
|
|
114
|
+
deliver:
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
103
119
|
# @rbs () -> CommitActionRegistry
|
|
104
120
|
def commit_action_registry
|
|
105
121
|
@commit_action_registry ||= CommitActionRegistry.new
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from app/controllers/solid_objects/transmissions_controller.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class TransmissionsController < ActionController::API
|
|
5
|
+
# @rbs () -> void
|
|
6
|
+
def create: () -> void
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# @rbs (untyped) -> bool
|
|
11
|
+
def authorized_transmission?: (untyped) -> bool
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -161,6 +161,9 @@ module SolidObjects
|
|
|
161
161
|
# @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
|
|
162
162
|
def emit: (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
|
|
163
163
|
|
|
164
|
+
# @rbs () -> OperationDispatcher
|
|
165
|
+
def transmit: () -> OperationDispatcher
|
|
166
|
+
|
|
164
167
|
# @rbs (Symbol | String, **untyped) -> nil
|
|
165
168
|
def commit_action: (Symbol | String, **untyped) -> nil
|
|
166
169
|
|
|
@@ -4,8 +4,6 @@ module SolidObjects
|
|
|
4
4
|
class Configuration
|
|
5
5
|
@table_name_prefix: String
|
|
6
6
|
|
|
7
|
-
@process_alive_threshold: Float
|
|
8
|
-
|
|
9
7
|
@shutdown_timeout: Float
|
|
10
8
|
|
|
11
9
|
@supervisor_monitor_interval: Float
|
|
@@ -58,6 +56,10 @@ module SolidObjects
|
|
|
58
56
|
|
|
59
57
|
@authorize_administration: Proc
|
|
60
58
|
|
|
59
|
+
@authorize_transmission: Proc
|
|
60
|
+
|
|
61
|
+
@transmission_actor_type_resolver: Proc
|
|
62
|
+
|
|
61
63
|
@polling_interval: Float
|
|
62
64
|
|
|
63
65
|
@idle_polling_interval: Float
|
|
@@ -92,6 +94,8 @@ module SolidObjects
|
|
|
92
94
|
|
|
93
95
|
@process_heartbeat_interval: Float
|
|
94
96
|
|
|
97
|
+
@process_alive_threshold: Float
|
|
98
|
+
|
|
95
99
|
attr_accessor table_name_prefix: untyped
|
|
96
100
|
|
|
97
101
|
attr_accessor polling_interval: untyped
|
|
@@ -182,6 +186,10 @@ module SolidObjects
|
|
|
182
186
|
|
|
183
187
|
attr_accessor authorize_administration: untyped
|
|
184
188
|
|
|
189
|
+
attr_accessor authorize_transmission: untyped
|
|
190
|
+
|
|
191
|
+
attr_accessor transmission_actor_type_resolver: untyped
|
|
192
|
+
|
|
185
193
|
# @rbs @additional_components: Array[untyped]
|
|
186
194
|
attr_reader additional_components: untyped
|
|
187
195
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/transmission.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
module Transmission
|
|
5
|
+
REQUIRED_FIELDS: untyped
|
|
6
|
+
|
|
7
|
+
IDEMPOTENCY_PREFIX: ::String
|
|
8
|
+
|
|
9
|
+
EFFECT_NAME: ::String
|
|
10
|
+
|
|
11
|
+
UNDELIVERED_STATUSES: untyped
|
|
12
|
+
|
|
13
|
+
# @rbs (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
|
|
14
|
+
def self.receive: (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
|
|
15
|
+
|
|
16
|
+
# @rbs (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
|
|
17
|
+
def self.deliver_through: (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
|
|
18
|
+
|
|
19
|
+
# @rbs (String, ^() -> bool) -> Class
|
|
20
|
+
private def self.fetch_actor_class: (String, ^() -> bool) -> Class
|
|
21
|
+
|
|
22
|
+
# @rbs () -> bool
|
|
23
|
+
private def self.load_application_actors: () -> bool
|
|
24
|
+
|
|
25
|
+
# @rbs (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
|
|
26
|
+
private def self.staged_envelope: (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
|
|
27
|
+
|
|
28
|
+
# @rbs (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
|
|
29
|
+
private def self.undelivered_envelopes_through: (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
|
|
30
|
+
|
|
31
|
+
# @rbs (untyped) -> void
|
|
32
|
+
private def self.validate!: (untyped) -> void
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -18,6 +18,9 @@ module SolidObjects
|
|
|
18
18
|
# @rbs (String | Symbol) { (Hash[String, untyped], EffectContext) -> untyped } -> Proc
|
|
19
19
|
def self.register_effect: (String | Symbol) { (Hash[String, untyped], EffectContext) -> untyped } -> Proc
|
|
20
20
|
|
|
21
|
+
# @rbs (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
|
|
22
|
+
def self.register_transmit: (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
|
|
23
|
+
|
|
21
24
|
# @rbs () -> CommitActionRegistry
|
|
22
25
|
def self.commit_action_registry: () -> CommitActionRegistry
|
|
23
26
|
|
data/sig/support/framework.rbs
CHANGED
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.
|
|
4
|
+
version: 0.14.0
|
|
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-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -297,6 +297,7 @@ files:
|
|
|
297
297
|
- app/controllers/solid_objects/components_controller.rb
|
|
298
298
|
- app/controllers/solid_objects/dead_letters_controller.rb
|
|
299
299
|
- app/controllers/solid_objects/instances_controller.rb
|
|
300
|
+
- app/controllers/solid_objects/transmissions_controller.rb
|
|
300
301
|
- app/helpers/solid_objects/actor_helper.rb
|
|
301
302
|
- app/models/solid_objects/broadcast.rb
|
|
302
303
|
- app/models/solid_objects/claimed_message.rb
|
|
@@ -358,6 +359,7 @@ files:
|
|
|
358
359
|
- docs/roadmap.md
|
|
359
360
|
- docs/security.md
|
|
360
361
|
- docs/state-migrations.md
|
|
362
|
+
- docs/transmission.md
|
|
361
363
|
- examples/application/README.md
|
|
362
364
|
- examples/application/app/actors/chat_room_actor.rb
|
|
363
365
|
- examples/application/app/actors/shopping_cart_actor.rb
|
|
@@ -436,6 +438,7 @@ files:
|
|
|
436
438
|
- lib/solid_objects/sync_diagnostics.rb
|
|
437
439
|
- lib/solid_objects/synchronous_invocation.rb
|
|
438
440
|
- lib/solid_objects/test_helper.rb
|
|
441
|
+
- lib/solid_objects/transmission.rb
|
|
439
442
|
- lib/solid_objects/turbo_stream_renderer.rb
|
|
440
443
|
- lib/solid_objects/version.rb
|
|
441
444
|
- lib/solid_objects/wake_up.rb
|
|
@@ -457,6 +460,7 @@ files:
|
|
|
457
460
|
- sig/generated/controllers/solid_objects/components_controller.rbs
|
|
458
461
|
- sig/generated/controllers/solid_objects/dead_letters_controller.rbs
|
|
459
462
|
- sig/generated/controllers/solid_objects/instances_controller.rbs
|
|
463
|
+
- sig/generated/controllers/solid_objects/transmissions_controller.rbs
|
|
460
464
|
- sig/generated/helpers/solid_objects/actor_helper.rbs
|
|
461
465
|
- sig/generated/lib/generators/solid_objects/install_generator.rbs
|
|
462
466
|
- sig/generated/lib/solid_objects.rbs
|
|
@@ -523,6 +527,7 @@ files:
|
|
|
523
527
|
- sig/generated/lib/solid_objects/sync_diagnostics.rbs
|
|
524
528
|
- sig/generated/lib/solid_objects/synchronous_invocation.rbs
|
|
525
529
|
- sig/generated/lib/solid_objects/test_helper.rbs
|
|
530
|
+
- sig/generated/lib/solid_objects/transmission.rbs
|
|
526
531
|
- sig/generated/lib/solid_objects/turbo_stream_renderer.rbs
|
|
527
532
|
- sig/generated/lib/solid_objects/version.rbs
|
|
528
533
|
- sig/generated/lib/solid_objects/wake_up.rbs
|
|
@@ -575,12 +580,12 @@ licenses:
|
|
|
575
580
|
- MIT
|
|
576
581
|
metadata:
|
|
577
582
|
allowed_push_host: https://rubygems.org
|
|
578
|
-
bug_tracker_uri: https://github.com/cardmagic/
|
|
579
|
-
changelog_uri: https://github.com/cardmagic/
|
|
580
|
-
documentation_uri: https://github.com/cardmagic/
|
|
583
|
+
bug_tracker_uri: https://github.com/cardmagic/solid-objects-ruby/issues
|
|
584
|
+
changelog_uri: https://github.com/cardmagic/solid-objects-ruby/blob/main/CHANGELOG.md
|
|
585
|
+
documentation_uri: https://github.com/cardmagic/solid-objects-ruby#readme
|
|
581
586
|
homepage_uri: https://solidobjects.dev
|
|
582
587
|
rubygems_mfa_required: 'true'
|
|
583
|
-
source_code_uri: https://github.com/cardmagic/
|
|
588
|
+
source_code_uri: https://github.com/cardmagic/solid-objects-ruby
|
|
584
589
|
post_install_message:
|
|
585
590
|
rdoc_options: []
|
|
586
591
|
require_paths:
|