solid-objects 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +11 -2
- package/dist/actor-runtime.d.ts +7 -0
- package/dist/actor-runtime.d.ts.map +1 -1
- package/dist/actor.d.ts +1 -0
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +3 -0
- package/dist/actor.js.map +1 -1
- package/dist/cloudflare/engine.d.ts +1 -0
- package/dist/cloudflare/engine.d.ts.map +1 -1
- package/dist/cloudflare/engine.js +118 -15
- package/dist/cloudflare/engine.js.map +1 -1
- package/dist/cloudflare/records.d.ts +2 -0
- package/dist/cloudflare/records.d.ts.map +1 -1
- package/dist/cloudflare/runtime.d.ts +7 -0
- package/dist/cloudflare/runtime.d.ts.map +1 -1
- package/dist/cloudflare/runtime.js +13 -0
- package/dist/cloudflare/runtime.js.map +1 -1
- package/dist/configuration.d.ts +10 -0
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js.map +1 -1
- package/dist/diagnostics.d.ts +37 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +73 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +14 -1
- package/dist/realtime.js.map +1 -1
- package/dist/reference.d.ts +11 -0
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +22 -0
- package/dist/reference.js.map +1 -1
- package/dist/repository.d.ts +1 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +7 -6
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +108 -32
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +20 -5
- package/dist/schema.js.map +1 -1
- package/dist/serialization.js +3 -5
- package/dist/serialization.js.map +1 -1
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +126 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/transmit.js +2 -2
- package/dist/transmit.js.map +1 -1
- package/dist/turn.js +2 -2
- package/dist/turn.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wake-up-notification.d.ts.map +1 -1
- package/dist/wake-up-notification.js +9 -1
- package/dist/wake-up-notification.js.map +1 -1
- package/docs/agents.md +300 -0
- package/docs/api.md +25 -3
- package/docs/authorization.md +6 -0
- package/docs/comparisons.md +36 -0
- package/docs/observability.md +192 -0
- package/docs/parity.md +85 -72
- package/docs/releasing.md +12 -2
- package/docs/virtual-actors.md +242 -0
- package/examples/ticket-sale.ts +57 -0
- package/package.json +8 -5
package/docs/comparisons.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This guide compares coordination models so an application can choose the
|
|
4
4
|
smallest mechanism that meets its requirements. It does not rank the projects.
|
|
5
|
+
Solid Objects is a virtual actor library; the
|
|
6
|
+
[virtual actor guide](virtual-actors.md) defines the term. Facts about other
|
|
7
|
+
projects were checked on October 7, 2026.
|
|
5
8
|
|
|
6
9
|
| Approach | State and serialization unit | Deployment and durable substrate | Separate service | Replay versus state | Realtime and edge placement | Cross-identity transaction | Data access |
|
|
7
10
|
| --------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
@@ -12,7 +15,25 @@ smallest mechanism that meets its requirements. It does not rank the projects.
|
|
|
12
15
|
| celld | Object class and object name | celld nodes plus one object-storage bucket; each object is its own SQLite database | Yes, the celld daemon on every node | The new owner restores the object's SQLite database from the bucket and resumes | Cloudflare Workers APIs; an object runs on one node of your fleet, not at an edge location | No | Per-object SQLite through the Workers storage APIs, plus the bucket |
|
|
13
16
|
| Rivet Actors | Addressable actor | Rivet Engine or managed compute with actor state, KV, or per-actor SQLite | Rivet Engine | Actor persistence and lifecycle; workflows add recorded steps | Actor events and deployment-dependent placement | No general transaction across actors | Actor APIs and selected persistence model |
|
|
14
17
|
| DBOS | Workflow ID and checkpointed steps | Application processes plus PostgreSQL system database | No orchestration server for the library; Conductor is recommended for distributed recovery | Deterministic workflow replay skips checkpointed steps | Workflow events; application placement | PostgreSQL transactions remain separate from workflow identity | PostgreSQL system database, client, CLI, and optional Conductor |
|
|
18
|
+
| Dapr actors | Actor type and ID, one turn at a time | Application services plus Dapr sidecars and a transactional actor state store | Yes: the Dapr sidecar plus the placement and Scheduler services | Actor state persists in the state store; actors do not replay workflow steps | Sidecar-routed actor placement; no edge placement | No transaction across actors | Dapr actor APIs and the configured state store |
|
|
15
19
|
| Restate | Service handler or keyed virtual object | Application services plus Restate's durable log and state store | Yes | Durable execution journals handler progress and object state | Service protocol and Restate deployment | No shared SQL transaction across object keys | Restate APIs, state tools, snapshots, and backups |
|
|
20
|
+
| Temporal | Workflow execution ID | Worker processes plus a Temporal Service, self-hosted or Temporal Cloud | Yes, the Temporal Service | Deterministic workflow code replays its event history | Workflow signals and queries; application placement | No shared SQL transaction across workflows | Temporal SDK, CLI, and web UI |
|
|
21
|
+
|
|
22
|
+
## Comparison vocabulary
|
|
23
|
+
|
|
24
|
+
Each row above differs on the same dimensions. For Solid Objects they are:
|
|
25
|
+
|
|
26
|
+
| Dimension | Solid Objects |
|
|
27
|
+
| ---------------- | ---------------------------------------------------------------------------------- |
|
|
28
|
+
| Identity | An actor class and an application-defined ID |
|
|
29
|
+
| Activation | On demand, behind a fenced lease, released when idle |
|
|
30
|
+
| State storage | A JSON document in SQLite, PostgreSQL, or MySQL |
|
|
31
|
+
| Serialization | One durable mailbox for each identity |
|
|
32
|
+
| Delivery | At least once, in order for each identity |
|
|
33
|
+
| Reminders | Durable and keyed for each actor |
|
|
34
|
+
| Recovery | Another process claims the work after a lease expires; committed work waits in SQL |
|
|
35
|
+
| Deployment | A library inside the application's Node.js processes |
|
|
36
|
+
| Operational cost | One retained message row for each call, plus database load |
|
|
16
37
|
|
|
17
38
|
## Primary references
|
|
18
39
|
|
|
@@ -37,5 +58,20 @@ smallest mechanism that meets its requirements. It does not rank the projects.
|
|
|
37
58
|
and its storage requirements in the
|
|
38
59
|
[self-hosted server overview](https://docs.restate.dev/server/overview).
|
|
39
60
|
|
|
61
|
+
- Dapr documents virtual actors, the actor state store, and Scheduler-backed
|
|
62
|
+
reminders in [Actors overview](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/)
|
|
63
|
+
and [Actor timers and reminders](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-timers-reminders/).
|
|
64
|
+
The [JavaScript SDK](https://docs.dapr.io/developing-applications/sdks/js/js-actors/)
|
|
65
|
+
supports actors.
|
|
66
|
+
- Temporal documents deterministic replay in
|
|
67
|
+
[Workflow definition](https://docs.temporal.io/workflow-definition) and its
|
|
68
|
+
service in [What is Temporal?](https://docs.temporal.io/temporal).
|
|
69
|
+
- Orleans documents virtual actors, turn-based scheduling, and reminders in its
|
|
70
|
+
[overview](https://learn.microsoft.com/en-us/dotnet/orleans/overview),
|
|
71
|
+
[Request scheduling](https://learn.microsoft.com/en-us/dotnet/orleans/grains/request-scheduling),
|
|
72
|
+
and [Timers and reminders](https://learn.microsoft.com/en-us/dotnet/orleans/grains/timers-and-reminders).
|
|
73
|
+
- Node.js documents CPU-bound parallelism in
|
|
74
|
+
[Worker threads](https://nodejs.org/api/worker_threads.html).
|
|
75
|
+
|
|
40
76
|
External systems evolve. Recheck these primary sources before relying on one
|
|
41
77
|
row as a procurement or architecture decision.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Portable observability
|
|
2
|
+
|
|
3
|
+
Configure `instrumentation(event)` to receive structured events. No exporter SDK
|
|
4
|
+
is required. The same JSON envelope is emitted by Ruby, SQLite, PostgreSQL, MySQL,
|
|
5
|
+
and the Durable Objects host. Existing JavaScript `name`, `occurredAt`, and
|
|
6
|
+
`attributes` fields remain available. Ruby's Active Support notifications remain
|
|
7
|
+
available with their existing snake_case payloads.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { configure } from "solid-objects"
|
|
11
|
+
|
|
12
|
+
const runtime = configure({
|
|
13
|
+
...applicationConfiguration,
|
|
14
|
+
instrumentation: (event) => {
|
|
15
|
+
console.info(JSON.stringify(event))
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
JavaScript passes `instrumentation` to `configure()`. Ruby sets
|
|
21
|
+
`configuration.instrumentation` inside `SolidObjects.configure`.
|
|
22
|
+
|
|
23
|
+
## Schema version 1
|
|
24
|
+
|
|
25
|
+
Every event has `schemaVersion`, `name` (prefixed with `solid_objects.`),
|
|
26
|
+
`occurredAt` (UTC ISO 8601), `adapter`, `actorType`, `actorId`, `incarnation`,
|
|
27
|
+
`revision`, `messageId`, `attempt`, `attributes`, and `metrics`.
|
|
28
|
+
Unavailable identifiers are null; `attempt` is zero outside a message attempt.
|
|
29
|
+
Ruby publishes the RBS types `SolidObjects::portable_event`,
|
|
30
|
+
`SolidObjects::actor_diagnostics`, and `SolidObjects::event_observer`. JavaScript
|
|
31
|
+
exports `InstrumentationEvent`, `ActorDiagnostics`, and `EventObserver`.
|
|
32
|
+
An incarnation identifies a persisted actor instance, independently of its lease
|
|
33
|
+
generation. Revisions and IDs are strings. Process-wide events have null actor
|
|
34
|
+
identity. A message ID or revision correlates actor work where applicable.
|
|
35
|
+
|
|
36
|
+
Only known scalar metadata fields enter the portable envelope. Arguments, state,
|
|
37
|
+
results, credentials, backtraces, exception text, nested provider data, and unknown
|
|
38
|
+
attributes are excluded. Actor IDs remain correlation data: applications should
|
|
39
|
+
use opaque actor identifiers and apply their own retention policy to event logs.
|
|
40
|
+
Events and metric samples are immutable. Throwing observers, rejected observer
|
|
41
|
+
promises, and a failing instrumentation error logger cannot change a turn's
|
|
42
|
+
result. When an exporter or observer raises, both runtimes log
|
|
43
|
+
`solid_objects.instrumentation.failed` with the event name and the error class.
|
|
44
|
+
Delivery is best effort and synchronous callbacks should be short;
|
|
45
|
+
JavaScript does not await exporters. Telemetry is not a durable audit trail.
|
|
46
|
+
|
|
47
|
+
| Event | Meaning |
|
|
48
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
49
|
+
| `activation.started/completed/failed` | Local actor activation hook lifecycle |
|
|
50
|
+
| `message.started/completed/failed/rejected` | One attempt's execution outcome |
|
|
51
|
+
| `message.retry` | Failed attempt durably queued for another attempt |
|
|
52
|
+
| `dead_letter.created` | Message exhausted retries or failed permanently |
|
|
53
|
+
| `commit_action.started/completed/failed` | One registered commit action inside the commit transaction |
|
|
54
|
+
| `mailbox.depth` | On-demand diagnostic sample; `depth` is null when the sample is truncated |
|
|
55
|
+
| `reminder.enqueued` | Due reminder dispatch; lateness is measured from due time |
|
|
56
|
+
| `outbox.age` | Delivery observation; age is time since the item's current availability time |
|
|
57
|
+
| `recovery.reclaimed` | A previously claimed, interrupted message begins another attempt |
|
|
58
|
+
| `recovery.completed/failed` | Durable effect recovery callback commits or enters the dead-letter queue |
|
|
59
|
+
| `snapshot.read` | Authorized snapshot constructed without exposing its contents |
|
|
60
|
+
| `realtime.connected/disconnected` | Actor subscription added or removed |
|
|
61
|
+
| `payload_broadcast.failed` | One personalized payload failed; `payload` names it |
|
|
62
|
+
|
|
63
|
+
Events describe local observations. Concurrent deletion, crashes, and failed
|
|
64
|
+
exporters can omit events. Never infer exactly-once delivery from event counts.
|
|
65
|
+
Additional existing runtime events retain their names.
|
|
66
|
+
|
|
67
|
+
## Event attributes
|
|
68
|
+
|
|
69
|
+
`compatibility/telemetry-events.json` holds the attribute allowlist and the exact
|
|
70
|
+
attribute keys of each core event. Both test suites compare the events of the SQL
|
|
71
|
+
runtimes with this file. Message events carry `operation` and `deliveryMode`.
|
|
72
|
+
`message.failed` also carries a boolean `retryable` and an `outcome` of
|
|
73
|
+
`retrying` or `dead`. Commit action events carry `commitAction` and
|
|
74
|
+
`activationGeneration`. Activation events carry `generation` and `ownerId`.
|
|
75
|
+
|
|
76
|
+
Ruby activates an actor instance before it claims a message, so its activation
|
|
77
|
+
events have no message fields. JavaScript activates an actor in the turn that
|
|
78
|
+
claims a message, so its activation events also carry the `messageId`,
|
|
79
|
+
`requestId`, `sequence`, `attempt`, `operation`, and `deliveryMode` of that
|
|
80
|
+
message. The contract file records these keys as JavaScript-only.
|
|
81
|
+
|
|
82
|
+
The Durable Objects host sends the same envelope, but some of its events carry
|
|
83
|
+
fewer attributes. The contract file does not apply to that host.
|
|
84
|
+
|
|
85
|
+
Portable `polling.interval_changed` events carry `previousIntervalMilliseconds`,
|
|
86
|
+
`currentIntervalMilliseconds`, and a string `reason`. Ruby converts its native
|
|
87
|
+
second-based notification values while preserving the original notification.
|
|
88
|
+
|
|
89
|
+
## Synchronous timeout diagnostics
|
|
90
|
+
|
|
91
|
+
`solid_objects.sync.timeout` includes `waitingOn`, `activationOwnerId`, and
|
|
92
|
+
`activationGeneration` in `attributes`. Generations are decimal strings;
|
|
93
|
+
unavailable activation fields are null. Both runtimes use the same wait reasons:
|
|
94
|
+
`actorPaused`, `activationHeld`, `earlierMessage`, `messageClaimed`,
|
|
95
|
+
`notYetAvailable`, `readyUnclaimed`, `databaseContention`, and `unknown`.
|
|
96
|
+
Ruby's exception attributes and Active Support notifications retain their native
|
|
97
|
+
snake_case names and reason values. The portable instrumentation envelope uses
|
|
98
|
+
the shared camelCase contract. The Durable Objects host does not send
|
|
99
|
+
`sync.timeout`. A `call` timeout there reports `waitingOn: "unknown"`.
|
|
100
|
+
|
|
101
|
+
## Metrics and tracing
|
|
102
|
+
|
|
103
|
+
Metrics are sample descriptions. Exporting them is opt-in: the runtime does not
|
|
104
|
+
register meters, allocate per-actor metric series, or install a vendor SDK.
|
|
105
|
+
|
|
106
|
+
| Name | Kind | Unit | Aggregation |
|
|
107
|
+
| --------------------------------- | --------- | ---- | ------------------------------------------------------ |
|
|
108
|
+
| `solid_objects.events` | counter | `1` | Sum one per event |
|
|
109
|
+
| `solid_objects.duration` | histogram | `ms` | Distribution of observed attempt duration |
|
|
110
|
+
| `solid_objects.reminder.lateness` | histogram | `ms` | Distribution of reminder dispatch delay |
|
|
111
|
+
| `solid_objects.outbox.age` | histogram | `ms` | Distribution of delivery delay since availability |
|
|
112
|
+
| `solid_objects.mailbox.depth` | gauge | `1` | Last exact sampled actor depth; omit truncated samples |
|
|
113
|
+
|
|
114
|
+
Labels contain only event name, adapter family, and declared actor type. Keep the
|
|
115
|
+
actor type registry finite. Never add actor ID, incarnation, message ID, operation
|
|
116
|
+
arguments, request IDs, or error text to metric labels. A gauge without an actor
|
|
117
|
+
label represents the most recently observed actor; it is not total fleet backlog.
|
|
118
|
+
For tracing, correlate start/outcome events using `adapter`, `incarnation`,
|
|
119
|
+
`messageId`, and `attempt`, and close or expire spans when no outcome arrives.
|
|
120
|
+
|
|
121
|
+
## Actor observers and diagnostics
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const cart = ShoppingCart.ref("demo-cart")
|
|
125
|
+
const stop = await cart.observe({
|
|
126
|
+
authorizationContext: operator,
|
|
127
|
+
onEvent: (event) => {
|
|
128
|
+
console.info(JSON.stringify(event))
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
const summary = await cart.diagnostics({ authorizationContext: operator, limit: 50 })
|
|
132
|
+
stop()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Ruby uses `reference.observe(authorization_context:) { |event| ... }` and
|
|
136
|
+
`reference.diagnostics(authorization_context:, limit: 50)`. Stop observing by
|
|
137
|
+
calling the returned proc. `on` filters one event name, such as `message.retry`.
|
|
138
|
+
Observers receive only this actor's events in the current runtime/process; they
|
|
139
|
+
are not subscriptions to workers on other hosts. Dispose them when the caller's
|
|
140
|
+
session ends or authorization is revoked. Each runtime or process accepts at most
|
|
141
|
+
1,000 local observers. More observers raise `RangeError` in JavaScript and
|
|
142
|
+
`ArgumentError` in Ruby. An observer needs a callback or a block. JavaScript
|
|
143
|
+
rejects a missing `onEvent` with `TypeError`, and Ruby raises `ArgumentError`
|
|
144
|
+
without a block. Both checks run before authorization.
|
|
145
|
+
|
|
146
|
+
The JavaScript Durable Objects host does not support process-local reference
|
|
147
|
+
observers. On that host, `observe` and `on` raise `UnsupportedCapability`, and
|
|
148
|
+
remote `reference.diagnostics` works. To observe a remote actor, configure
|
|
149
|
+
`instrumentation` on the actor host and filter the events by actor identity.
|
|
150
|
+
Ruby has no Durable Objects host.
|
|
151
|
+
|
|
152
|
+
Both APIs default to denied. Set `authorizeAdministration` / `authorize_administration`
|
|
153
|
+
to allow action `observe` or `inspect`, resource `actor_diagnostics`, and resource ID
|
|
154
|
+
`JSON.stringify([actorType, actorId])`. Ruby receives a symbol action. Authorization
|
|
155
|
+
runs before reading summaries or registering observers; possessing an actor ID
|
|
156
|
+
confers no permission.
|
|
157
|
+
|
|
158
|
+
Diagnostics read at most `limit + 1` rows per queue source, with a hard limit
|
|
159
|
+
of 100 rows. Each category returns `sampled`, `truncated`, and
|
|
160
|
+
`oldestAgeMilliseconds`.
|
|
161
|
+
The limit applies to each combined category: one effect plus one broadcast with
|
|
162
|
+
`limit: 1` returns `sampled: 1, truncated: true`, even when both source queries
|
|
163
|
+
returned all their rows. The extra row proves that the category exceeds its cap.
|
|
164
|
+
The last value measures nonnegative time since availability (or terminal failure
|
|
165
|
+
for recovery callbacks); future reminders have zero age. No payloads or row
|
|
166
|
+
identifiers are returned. Samples are observations across several queries,
|
|
167
|
+
not an atomic fleet snapshot. Large queues can still require database scanning;
|
|
168
|
+
the bound limits materialized rows and response size, not query execution time.
|
|
169
|
+
|
|
170
|
+
Categories are mailbox (ready and claimed), outbox (pending and processing effects
|
|
171
|
+
and broadcasts), reminders (scheduled and paused), retries (failed messages still
|
|
172
|
+
eligible to run), and recoveryFailures (dead internal effect recovery
|
|
173
|
+
callback messages). Durable Objects does not implement process-heartbeat effect recovery;
|
|
174
|
+
its recoveryFailures category is empty. Recovery failures are durable records,
|
|
175
|
+
not a history of transient database or exporter exceptions.
|
|
176
|
+
|
|
177
|
+
## A query that works across adapters
|
|
178
|
+
|
|
179
|
+
Write one envelope per line to `events.jsonl`, using the same instrumentation hook
|
|
180
|
+
with SQLite and PostgreSQL. This query reports completed attempts by adapter:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
jq -s 'map(select(.name == "solid_objects.message.completed"))
|
|
184
|
+
| group_by(.adapter)
|
|
185
|
+
| map({adapter: .[0].adapter, completed: length,
|
|
186
|
+
mean_ms: (map(.attributes.durationMilliseconds) | add / length)})' events.jsonl
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A dashboard can chart the event counter by adapter and outcome and the duration,
|
|
190
|
+
reminder lateness, and outbox age distributions using exactly the same fields.
|
|
191
|
+
Compare workloads with the same actor types and sampling policy. Counts measure
|
|
192
|
+
observations and cannot replace a database query for authoritative queue state.
|