Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,72 @@
# Changelog

## 0.17.0 - 2026-10-03

- Publish RBS types for portable events, metric samples, actor diagnostics, and
event observers in `sig/public/telemetry.rbs`, and a `json_value` type for
message results and actor state. Observer blocks, diagnostics, and results now
type-check against these contracts instead of `untyped`.
- **Breaking:** `solid_objects.activation.started` now fires before the actor's
`activate` hook. Before, it fired after a successful hook. The new
`solid_objects.activation.completed` event takes that meaning, and
`solid_objects.activation.failed` reports a failed hook. JavaScript changes
the same events. Move a subscriber that reads `activation.started` as a
finished activation to `activation.completed`.
- **Breaking:** Active Support payloads no longer carry `error_message`. This
applies to `commit_action.failed`, `activation.deactivation_failed`,
`supervisor.monitor_failed`, `supervisor.retention_failed`,
`supervisor.redrive_failed`, and `wake_up.failed`. The
`solid_objects.worker.error` log entry also omits it. Each keeps
`error_class`. Exception text can contain actor state, so JavaScript already
reports only the error name.

- **Breaking:** rename `solid_objects.payload_broadcast_failed` to
`solid_objects.payload_broadcast.failed`, the dotted form that every other
event uses. Update Active Support subscribers to the new name. The portable
event names the payload `payload`.
- Match portable event attributes to JavaScript through the shared
`compatibility/telemetry-events.json` contract. Message events carry
`operation` and `deliveryMode`, `message.failed` carries `retryable` and
`outcome`, commit action events carry the message fields and `commitAction`,
and `reminder.enqueued` carries `operation`. `outbox.age` carries the effect or
broadcast identity, `sync.enqueue_timeout` carries `timeoutMilliseconds`, and
polling intervals are integers. `realtime.connected` carries only actor fields.
- Log `solid_objects.instrumentation.failed` when an exporter or observer raises.
Observers require a block, a process accepts at most 1,000 observers, and
`SolidObjects.reset!` removes them. Pin reserved JSON keys through actor
arguments, state, and retained results.

- Guard personalized payload projections against state changes, staged work,
and application database writes. Each payload gets an isolated actor from
the committed snapshot and honors `max_payload_bytes`, matching JavaScript.
- Preserve timeout wait reasons, activation owner IDs, and activation generations
in portable telemetry using the shared camelCase fields and reason values.
- Use a yielding SQLite busy handler for background transactions so concurrent
writers can finish on Rails 7.1 and 7.2. Preserve configured wait limits and
synchronous deadlines; cover contention with a coordinated lock regression.

- **Breaking:** reject query and observable state mutation and staged durable
work with terminal `QueryMutatedState` errors. Cover individual snapshot
projections and preserve ordinary operations' already-staged work while
reading projections, including replacements that leave the intent count
unchanged.
- Pin reserved JSON property names with shared Ruby/JS fixtures. Document the
reminder-name limit difference and the authorized dead-transmit retry API.

- **Breaking:** reauthorize every message-reference status, result, and outcome
read against the original invocation. Pass `authorization_context:` on every
read.
- **Breaking:** retain immutable JSON results for background and internal
messages as well as synchronous calls. All operations now enforce result
serialization and size limits; return `nil` explicitly when an operation does
not need a result. `result` raises terminal rejection/failure errors;
`outcome` exposes them as data.
- Preserve polling transition intervals in milliseconds and string reasons in
portable telemetry. Pin transmit staging order and null-argument validation
against the shared JavaScript contract.

- Add portable telemetry, isolated observer hooks, metric definitions, and bounded authorized actor diagnostics matching JavaScript.

## 0.16.1 - 2026-10-02

- Fix the SQLite join order of the claimed-message scan. The query in
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
solid_objects (0.16.1)
solid_objects (0.17.0)
actioncable (>= 7.1)
actionpack (>= 7.1)
actionview (>= 7.1)
Expand Down Expand Up @@ -384,7 +384,7 @@ CHECKSUMS
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
solid_objects (0.16.1)
solid_objects (0.17.0)
sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Exactly once is not hiding in a more advanced configuration. Read the
- [Five-minute Rails guide](https://solidobjects.dev/5min/rails)
- [Choosing Solid Objects](docs/fit.md)
- [Operations and recovery](docs/operations.md)
- [Observability and diagnostics](docs/observability.md)
- [Reminders](docs/reminders.md)
- [Reactive ERB](docs/realtime.md)
- [Detailed architecture](docs/architecture.md)
Expand Down
15 changes: 15 additions & 0 deletions app/models/solid_objects/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ def dead?
dead_letter.present?
end

# @rbs () -> json_value
def result!
if rejected?
raise Rejected.new(
code: rejection.fetch("code"),
message: rejection.fetch("message"),
details: rejection.fetch("details"),
message_id: id
)
end
raise MessageFailed.new("actor message failed permanently", message_id: id, details: error || {}) if dead?

Serialization.readonly_copy(result)
end

private

# @rbs () -> void
Expand Down
14 changes: 14 additions & 0 deletions compatibility/json-values.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"name": "object prototype key",
"value": { "__proto__": { "role": "ordinary data" }, "value": 1 }
},
{ "name": "scalar prototype key", "value": { "__proto__": "ordinary data" } },
{ "name": "null prototype key", "value": { "__proto__": null } },
{
"name": "nested reserved names",
"value": {
"items": [{ "__proto__": { "constructor": "data" }, "prototype": true, "hasOwnProperty": 1 }]
}
}
]
10 changes: 10 additions & 0 deletions compatibility/sync-timeout.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{ "rubyReason": "actor_paused", "waitingOn": "actorPaused" },
{ "rubyReason": "activation_held", "waitingOn": "activationHeld" },
{ "rubyReason": "earlier_message", "waitingOn": "earlierMessage" },
{ "rubyReason": "message_claimed", "waitingOn": "messageClaimed" },
{ "rubyReason": "not_yet_available", "waitingOn": "notYetAvailable" },
{ "rubyReason": "ready_unclaimed", "waitingOn": "readyUnclaimed" },
{ "rubyReason": "database_contention", "waitingOn": "databaseContention" },
{ "rubyReason": "unknown", "waitingOn": "unknown" }
]
Loading
Loading