solid_objects 0.13.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1043a832a75d5e7d2c9e59b332c3c6bce85a322204fdc2a056dc5198018d34d
4
- data.tar.gz: 4b1505d23bab1aeb57dc722a8a613fd3c39911d2756e24c37714466e4c821b22
3
+ metadata.gz: a64ced341585dcb4b7e9240b2076e1f47a352b4786806c1bec08c10d91c7d187
4
+ data.tar.gz: 6d53730c69b2f86cde60d3fdb1ffcefe10dd362dc98652b4afe2b367e1134f32
5
5
  SHA512:
6
- metadata.gz: 2d7c216e135b8e34c19b9130a0fcad0df6246191ee097f9a560497d0dacc47fc480e549c1ddd6ab2bc6fe9bc513245236aa47cc1650a08906a04367d0e6bd008
7
- data.tar.gz: d1dc2001e0b43a2df5eb24c5cd79c6e1df99b2d6c46c90146a349dd09a557db9dcdaa7c51e51a47f5db1667b364c2dfe64d5ba73fe877248a0143e022045c764
6
+ metadata.gz: acc3e7a6bb436e0dbadf5a38478fc21d3b072f11fcee0cfed26960cba9c1d8eb790a99cf3b065457de8c2ced86d01718c854635d210ba23ffe571e009eab95f7
7
+ data.tar.gz: 91faab1647c20ba461dcf065612b549b2f6dbd955f2a2222f89a2556d377b99b4a648dbb71ba441ca01ba1281607292f3f21300256ad65f55c821374dac85d7e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
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
+
37
+ ## 0.13.3 - 2026-08-18
38
+
39
+ - Stop loading `ActiveRecord::Base` when the gem is required. The engine now
40
+ loads `SolidObjects::Record` from an `ActiveSupport.on_load(:active_record)`
41
+ hook, so a host application keeps the normal timing of its own
42
+ `on_load(:active_record)` and `on_load(:active_record_encryption)` hooks. An
43
+ application that assigns its Active Record encryption keys in
44
+ `config/initializers` no longer loses them.
45
+ - Apply the configured `connects_to` in the record class body, so the
46
+ connection follows the class through a development reload.
47
+ - Lower the supported Rails floor from 8.0 to 7.1. The gem dependencies, the
48
+ bundled migrations, and the compatibility CI matrix now cover Rails 7.1, 7.2,
49
+ 8.0, and 8.1. The migrations declare `ActiveRecord::Migration[7.1]`, which
50
+ builds the same schema as `[8.0]` because the compatibility layer between the
51
+ two only changes `remove_foreign_key`, which no Solid Objects migration calls.
52
+ Rails 7.0 stays out of range: its SQLite adapter requires `sqlite3 ~> 1.4`,
53
+ and the busy-handler control this gem needs arrived in `sqlite3` 2.x.
54
+
3
55
  ## 0.13.2 - 2026-08-17
4
56
 
5
57
  - Accept a `key:` on `schedule`, naming a reminder for the item it is waiting
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Solid Objects
2
2
 
3
- [![CI](https://github.com/cardmagic/solid_objects/actions/workflows/ci.yml/badge.svg)](https://github.com/cardmagic/solid_objects/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/cardmagic/solid-objects-ruby/actions/workflows/ci.yml/badge.svg)](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 brings the Durable Objects programming model—addressable objects,
8
- durable state, serialized turns, alarms, and live clients—to ordinary Rails
9
- applications. It runs on the MySQL, PostgreSQL, or SQLite database the
10
- application already has, following the database-backed operating model of the
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 excellent tools for jobs, records, and realtime transport.
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
 
@@ -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 8.0 or newer.
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
 
@@ -1272,7 +1273,7 @@ for staged cutovers.
1272
1273
  | --- | --- |
1273
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. |
1274
1275
  | Active Job | Jobs are independent work units. Solid Objects adds addressable identity, durable state, explicit per-identity order, activation leases, and fencing. |
1275
- | Solid Queue | Solid Queue is an excellent 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. |
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. |
1276
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. |
1277
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. |
1278
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
@@ -13,5 +13,7 @@ module SolidObjects
13
13
  connects_to(**connection_configuration)
14
14
  end
15
15
  end
16
+
17
+ configure_connection
16
18
  end
17
19
  end
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  SolidObjects::Engine.routes.draw do
4
+ post :transmit, to: "transmissions#create"
4
5
  get :components, to: "components#show"
5
6
  get "components/batch", to: "components#batch"
6
7
  resources :instances, only: %i[index show]
@@ -1,6 +1,6 @@
1
1
  # rbs_inline: enabled
2
2
 
3
- class CreateSolidObjectsTables < ActiveRecord::Migration[8.0]
3
+ class CreateSolidObjectsTables < ActiveRecord::Migration[7.1]
4
4
  # @rbs () -> void
5
5
  def change
6
6
  create_processes
@@ -1,6 +1,6 @@
1
1
  # rbs_inline: enabled
2
2
 
3
- class AddStateRevisionToSolidObjectsInstances < ActiveRecord::Migration[8.0]
3
+ class AddStateRevisionToSolidObjectsInstances < ActiveRecord::Migration[7.1]
4
4
  # @rbs () -> void
5
5
  def change
6
6
  add_column SolidObjects.table_name(:instances),
@@ -1,6 +1,6 @@
1
1
  # rbs_inline: enabled
2
2
 
3
- class RenameMessageDispatchColumns < ActiveRecord::Migration[8.0]
3
+ class RenameMessageDispatchColumns < ActiveRecord::Migration[7.1]
4
4
  # @rbs () -> void
5
5
  def up
6
6
  remove_check_constraint messages_table, name: "chk_so_messages_kind"
data/docs/development.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Requirements
4
4
 
5
5
  - Ruby 3.3 or newer
6
- - Rails 8.0 or newer
6
+ - Rails 7.1 or newer
7
7
  - SQLite 3.35+, PostgreSQL 14+, and MySQL 8.0/InnoDB for the full matrix
8
8
 
9
9
  Install dependencies:
@@ -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 requires SQLite 3.35 or newer for modern DML support.
530
- - Rails documentation and source inspected cover Rails 8.1. Solid Objects requires Rails 8.0 or newer because Rails 8 changed the SQLite adapter's default write transaction from deferred to immediate, which the SQLite coordination contract relies on.
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
@@ -66,8 +66,26 @@
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 range is
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
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
71
89
  - A JavaScript suite covering every browser module, run in CI with Node's test
72
90
  runner and jsdom, plus a browser suite running the same modules against real
73
91
  Chromium and a real Turbo build, with every GitHub Actions reference pinned to
@@ -125,7 +143,10 @@
125
143
  of who pressed what, and bulk-safe tools: retry is one dead letter at a time,
126
144
  because `DeadLetterManager` exposes no bulk operation. Pause is an operator
127
145
  brake and not a stop, since a pass already in flight finishes its turn and a
128
- synchronous caller waiting on a paused instance times out. The page cost was
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
129
150
  reasoned about rather than measured: the summary bar issues a fixed set of
130
151
  indexed aggregate queries per page, which is why `HEAD /` exists for uptime
131
152
  monitors, but no dashboard latency has been benchmarked against a large
@@ -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 evolution after state belongs to Solid Objects. For moving
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/solid_objects/blob/main/docs/authorization.md
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
 
@@ -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
 
@@ -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
- SolidObjects::Record.configure_connection
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
@@ -19,6 +19,9 @@ module SolidObjects
19
19
  class UnknownMessage < Error
20
20
  end
21
21
 
22
+ class InvalidTransmission < Error
23
+ end
24
+
22
25
  class InvalidPayload < Error
23
26
  end
24
27
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.13.2"
4
+ VERSION = "0.14.0"
5
5
  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
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Engine < ::Rails::Engine
5
+ RECORD_PATH: untyped
6
+
5
7
  include SolidObjects::ActorHelper
6
8
  end
7
9
  end
@@ -19,6 +19,9 @@ module SolidObjects
19
19
  class UnknownMessage < Error
20
20
  end
21
21
 
22
+ class InvalidTransmission < Error
23
+ end
24
+
22
25
  class InvalidPayload < Error
23
26
  end
24
27
 
@@ -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
 
@@ -21,6 +21,9 @@ end
21
21
  module ActionController
22
22
  class Base
23
23
  end
24
+
25
+ class API
26
+ end
24
27
  end
25
28
 
26
29
  module Rails
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.2
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-17 00:00:00.000000000 Z
11
+ date: 2026-08-23 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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
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: '8.0'
110
+ version: '7.1'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: thor
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -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/solid_objects/issues
579
- changelog_uri: https://github.com/cardmagic/solid_objects/blob/main/CHANGELOG.md
580
- documentation_uri: https://github.com/cardmagic/solid_objects#readme
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/solid_objects
588
+ source_code_uri: https://github.com/cardmagic/solid-objects-ruby
584
589
  post_install_message:
585
590
  rdoc_options: []
586
591
  require_paths: