Skip to content
Open
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
# Changelog

## Unreleased

- 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`.
- `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`.
- 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.

- 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.

- 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.

- Reauthorize every message-reference status, result, and outcome read against
the original invocation. Pass `authorization_context:` on every read.
- 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.0 - 2026-09-23

- Find a message whose reference a caller lost.
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