solid-objects 0.14.7 → 0.14.9

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/docs/api.md CHANGED
@@ -54,8 +54,11 @@ authorization, capability boundaries, and release validation.
54
54
  - `reference.live`: read-only live signals for an actor, enabled by the
55
55
  `solid-objects/signals` entry point documented below.
56
56
  - `ActorClass`, `ActorReference`, `ActorMessageSender`, `ActorSnapshot`,
57
- `ActorOperationNames`, `ActorQueryNames`, `StagedOperations`, and
58
- `ScheduledOperations`: inferred actor-class and fluent-dispatch types.
57
+ `ActorOperationNames`, `ActorQueryNames`, `StagedOperations`,
58
+ `ScheduledOperationsFor`, and `ScheduledOperations`: inferred actor-class and
59
+ fluent-dispatch types, plus the legacy dynamic scheduling map.
60
+ - `EffectOptions`: effect arguments and independently checked success/failure
61
+ callback names. Effect names themselves belong to the runtime's global registry.
59
62
  - `SnapshotWithIncarnation`: the `{ snapshot, instanceId, revision,
60
63
  createdAtMs }` shape returned by `SolidObjectsRuntime.snapshotWithIncarnation`.
61
64
  - `MessageReference`: immutable durable message identity with `id`,
@@ -69,6 +72,10 @@ createdAtMs }` shape returned by `SolidObjectsRuntime.snapshotWithIncarnation`.
69
72
  `PayloadBroadcasts`, and `PayloadBroadcastValue` describe actor-declared
70
73
  transactional work and typed personalized projections.
71
74
 
75
+ `EffectFailurePayload<Arguments>`, `EffectSuccessPayload<Arguments, Result>`,
76
+ and `SerializedError` describe effect callback messages. They are also exported
77
+ from the browser-safe `solid-objects/core` entry point.
78
+
72
79
  `observables()` returns a flat object. Unwrapped values are invalidation-only:
73
80
  their real values participate in change detection, but only their names enter
74
81
  the durable envelope. Use an explicit marker when wire behavior matters:
@@ -126,7 +133,7 @@ operation. If you arm one alarm per queued item, only the last one remains:
126
133
  // Wrong. Every entry overwrites the previous entry's alarm.
127
134
  add({ entry }: { entry: Entry }): void {
128
135
  this.entries = [...this.entries, entry]
129
- this.schedule({ at: new Date(entry.waitUntil) }).deliver!()
136
+ this.schedule({ at: new Date(entry.waitUntil) }).deliver()
130
137
  }
131
138
  ```
132
139
 
@@ -136,7 +143,7 @@ own identifier for the item and names that item's alarm, so each item gets one:
136
143
  ```typescript
137
144
  add({ entry }: { entry: Entry }): void {
138
145
  this.entries = [...this.entries, entry]
139
- this.schedule({ at: new Date(entry.waitUntil), key: entry.id }).deliver!()
146
+ this.schedule({ at: new Date(entry.waitUntil), key: entry.id }).deliver()
140
147
  }
141
148
  ```
142
149
 
@@ -155,6 +162,115 @@ row per item. It also cannot strand an entry when the runtime coalesces an
155
162
  occurrence. Prefer it for a large queue of interchangeable items. Prefer `key`
156
163
  when one item needs an alarm that you can move on its own.
157
164
 
165
+ ### Typing your onFailure handler
166
+
167
+ An effect callback is an ordinary actor operation. Its payload always includes
168
+ the stable `effectId` and the original serialized `arguments`, including `{}`
169
+ when the effect was emitted without arguments. Use the exported types when a
170
+ watchdog or failure handler needs to correlate work with the current generation:
171
+
172
+ ```typescript
173
+ import { Actor, type EffectFailurePayload, type EffectSuccessPayload } from "solid-objects"
174
+
175
+ type RunArguments = { generation: number }
176
+
177
+ class ChatRun extends Actor {
178
+ static override readonly actorType = "ChatRun"
179
+ generation = 0
180
+ status = "idle"
181
+ reply = ""
182
+
183
+ start(): void {
184
+ this.emit("run_model", {
185
+ arguments: { generation: ++this.generation },
186
+ onSuccess: "finishTurn",
187
+ onFailure: "failTurn",
188
+ })
189
+ }
190
+
191
+ failTurn({ arguments: original, error }: EffectFailurePayload<RunArguments>): void {
192
+ if (original.generation !== this.generation) return
193
+ this.status = `${error.name}: ${error.message}`
194
+ }
195
+
196
+ finishTurn({
197
+ arguments: original,
198
+ result,
199
+ }: EffectSuccessPayload<RunArguments, { reply: string }>): void {
200
+ if (original.generation !== this.generation) return
201
+ this.status = "finished"
202
+ this.reply = result.reply
203
+ }
204
+ }
205
+ ```
206
+
207
+ Failure payloads contain `error: SerializedError`, with string `name` and
208
+ `message` fields. They do not include a stack or cause. Success payloads contain
209
+ `result`, which can be any JSON value; an undefined effect return becomes
210
+ `null`. The default argument type is `JsonObject` and the default success result
211
+ type is `JsonValue`. Declare argument shapes with a JSON-compatible type alias.
212
+
213
+ These types describe the SQL and Cloudflare callback envelopes. Error messages
214
+ for non-Error throws retain each backend's existing serialization behavior.
215
+ The generic parameters express your application's contract; they do not add
216
+ runtime validation or infer types from `registerEffect()`. Keep registered
217
+ effect results and the handler's declared argument/result types in agreement.
218
+
219
+ ### Typed operation references
220
+
221
+ `schedule` and `transmit` infer this actor's operation names and arguments, including
222
+ inside actor methods and for inherited application operations. The returned
223
+ `ScheduledOperationsFor<ActorType>` values return `void` and preserve required,
224
+ optional, and zero-argument operation signatures. No non-null assertion is needed:
225
+
226
+ ```typescript
227
+ class ChatRun extends Actor {
228
+ generation = 0
229
+ status = "idle"
230
+
231
+ start({ generation }: { generation: number }): void {
232
+ this.generation = generation
233
+ this.schedule({ at: new Date(Date.now() + 60_000), key: "watchdog" }).recoverIfStuck({
234
+ generation,
235
+ })
236
+ this.emit("run_model", { arguments: { generation }, onFailure: "failTurn" })
237
+ }
238
+
239
+ recoverIfStuck({ generation }: { generation: number }): void {
240
+ if (generation !== this.generation) return
241
+ this.status = "recovering"
242
+ }
243
+
244
+ failTurn({ error }: { error: { message: string } }): void {
245
+ this.status = error.message
246
+ }
247
+ }
248
+ ```
249
+
250
+ Misspelled operations/callbacks, state properties, queries, and Actor infrastructure
251
+ are rejected. `emit` checks each callback independently: widening one callback to
252
+ `string` does not disable literal checking of the other. A deliberately widened
253
+ `string` callback retains runtime validation. Object properties can also widen to
254
+ `string`; preserve literals with `as const` or specialize `EffectOptions` to keep
255
+ static checking when options are stored in a variable. Effect and commit-action names remain
256
+ strings because their registries are runtime-wide; inferring registered names needs
257
+ a separate registry typing design.
258
+
259
+ For deliberately dynamic scheduling, retain the exported legacy map explicitly:
260
+
261
+ ```typescript
262
+ const dynamicActor: Actor = this
263
+ const operations: ScheduledOperations = dynamicActor.schedule({ at: deadline })
264
+ operations[operationName]!({ generation })
265
+ ```
266
+
267
+ This opts out of operation-name and argument inference and retains the existing
268
+ runtime operation checks. Direct calls, queries, and `sendTo` keep their inference.
269
+ Subclasses that override `schedule` or `transmit` with an explicit legacy
270
+ `ScheduledOperations` return annotation must update their override signatures to
271
+ match the generic Actor methods. This is a compile-time compatibility change;
272
+ runtime scheduling and transmission behavior are unchanged.
273
+
158
274
  ### Runtime managers
159
275
 
160
276
  Every manager below is available as a property on `SolidObjectsRuntime`; the
package/docs/parity.md CHANGED
@@ -29,6 +29,13 @@ such boundary between a gem and its dependents.
29
29
 
30
30
  ## Status vocabulary
31
31
 
32
+ Operation-reference typing is runtime-specific: TypeScript infers scheduled and
33
+ transmitted operations from the concrete receiver, and checks literal effect
34
+ callback names. Ruby offers opt-in RBS generation from declared application types
35
+ in [solid-objects-ruby#66](https://github.com/cardmagic/solid-objects-ruby/pull/66).
36
+ Both preserve runtime operation validation and global effect/commit-action names;
37
+ this does not imply automatic TypeScript-style inference in Ruby.
38
+
32
39
  - **Native**: the TypeScript runtime provides the capability in a Node-native
33
40
  shape.
34
41
  - **Partial**: the core exists, but an important Ruby guarantee or operational
@@ -48,13 +55,13 @@ is needed for the JavaScript stale-result race fix.
48
55
  | ------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
56
  | Actor registry, durable identity, JSON state, and adjacent state migrations | Native | Ordinary classes, static actor types, inferred state, explicit migrations, and isolated runtime context across every actor-instance callback. |
50
57
  | Fluent committed calls and background delivery | Native | `await reference.operation()` and `reference.send.operation()`. |
51
- | Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, and adapter-appropriate sequence locking. |
58
+ | Ordered mailbox, sequence allocation, idempotency, retries, dead letters, leases, renewal, and fenced commits | Native | Relational ready/claimed membership tables, distinct generated request IDs and caller idempotency keys, durable history, adapter-appropriate sequence locking, and PostgreSQL/MySQL row locks held from fence validation through commit. |
52
59
  | Domain rejection and strict poison ordering | Native | Rejections accept JavaScript identifier-style codes and roll back without retry; invalid codes fail terminally, while retryable failures block later operations until completion or dead-lettering. |
53
60
  | Bounded activation passes and hot-actor fairness | Native | Configurable turn-count and elapsed-time budgets bound each pass, then move only that actor's already-due memberships behind actors already waiting. |
54
61
  | Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. |
55
62
  | Backpressure and payload caps | Partial | Serialization enforces a shared maximum JSON nesting depth, raising `InvalidPayload`, and an optional caller-supplied `maxBytes` limit, raising `PayloadTooLarge`; reminder names are bounded to 255 characters. Distributed per-actor rate limits and global admission control do not exist yet, matching the open Ruby roadmap item. |
56
63
  | Idle activation cache | Native | Long-running workers retain hydrated actors under renewable fenced leases, restore public state after failed turns, and release on timeout, fairness yield, lease loss, or shutdown. |
57
- | Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure operations also receive the originally staged arguments for correlation. |
64
+ | Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure operations also receive the originally staged arguments for correlation. Typed callback envelopes are exported. |
58
65
  | Actor-to-actor delivery | Native | `sendTo(reference).operation()` stages delivery in the source actor commit. |
59
66
  | One-shot and recurring reminders | Native | Scheduling, replacement events, catch-up policy, stale-claim recovery, pausing, authorized inspection, and idempotent resume are implemented. |
60
67
  | Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection. |
@@ -65,6 +72,11 @@ is needed for the JavaScript stale-result race fix.
65
72
  | Result recovery and sync timeout diagnostics | Native | Status, result, and wait reauthorize the stored operation; terminal failure raises structured `MessageFailed`; whole-call adapter deadlines distinguish enqueue, wait, database, activation, and mailbox blockers. |
66
73
  | Result lookup by request ID | Planned | This is also an open Ruby roadmap item and will be implemented in both runtimes when its authorization shape is settled. |
67
74
 
75
+ Effect callback envelopes are typed with `EffectFailurePayload`,
76
+ `EffectSuccessPayload`, and `SerializedError` in both SQL and Cloudflare.
77
+ Ruby RBS contracts are tracked in cardmagic/solid-objects-ruby#64 and preserve
78
+ Ruby field names; this does not change runtime delivery semantics.
79
+
68
80
  ## Operations
69
81
 
70
82
  | Capability | Status | TypeScript shape or remaining work |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-objects",
3
- "version": "0.14.7",
3
+ "version": "0.14.9",
4
4
  "description": "Race-free realtime state per application identity, backed by your SQL database",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -136,14 +136,14 @@
136
136
  "@types/node": "^24.0.0",
137
137
  "@types/pg": "^8.21.0",
138
138
  "@types/ws": "^8.18.1",
139
- "@vitest/coverage-v8": "^4.1.10",
139
+ "@vitest/coverage-v8": "^4.1.11",
140
140
  "mysql2": "^3.23.3",
141
141
  "pg": "^8.23.0",
142
142
  "prettier": "^3.9.6",
143
143
  "redis": "^6.2.1",
144
144
  "signal-polyfill": "^0.2.2",
145
145
  "typescript": "^5.9.0",
146
- "vitest": "^4.1.10",
146
+ "vitest": "^4.1.11",
147
147
  "wrangler": "^4.129.0",
148
148
  "ws": "^8.21.3"
149
149
  },