solid-objects 0.15.2 → 0.16.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.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/dist/actor-runtime.d.ts +9 -1
- package/dist/actor-runtime.d.ts.map +1 -1
- package/dist/actor.d.ts +22 -4
- package/dist/actor.d.ts.map +1 -1
- package/dist/actor.js +81 -2
- package/dist/actor.js.map +1 -1
- package/dist/broadcast-worker.js +1 -1
- package/dist/broadcast-worker.js.map +1 -1
- package/dist/cloudflare/configuration.d.ts +3 -1
- package/dist/cloudflare/configuration.d.ts.map +1 -1
- package/dist/cloudflare/configuration.js +4 -0
- package/dist/cloudflare/configuration.js.map +1 -1
- package/dist/cloudflare/engine.d.ts +4 -0
- package/dist/cloudflare/engine.d.ts.map +1 -1
- package/dist/cloudflare/engine.js +91 -24
- 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 +8 -0
- package/dist/cloudflare/runtime.d.ts.map +1 -1
- package/dist/cloudflare/runtime.js +52 -1
- package/dist/cloudflare/runtime.js.map +1 -1
- package/dist/cloudflare/storage.d.ts +2 -0
- package/dist/cloudflare/storage.d.ts.map +1 -1
- package/dist/cloudflare/storage.js +10 -0
- package/dist/cloudflare/storage.js.map +1 -1
- package/dist/configuration.d.ts +9 -5
- package/dist/configuration.d.ts.map +1 -1
- package/dist/configuration.js +43 -7
- package/dist/configuration.js.map +1 -1
- package/dist/database/types.d.ts +4 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/dead-letter-scopes.d.ts +75 -0
- package/dist/dead-letter-scopes.d.ts.map +1 -0
- package/dist/dead-letter-scopes.js +157 -0
- package/dist/dead-letter-scopes.js.map +1 -0
- package/dist/dead-letters.d.ts +5 -0
- package/dist/dead-letters.d.ts.map +1 -1
- package/dist/dead-letters.js +16 -0
- package/dist/dead-letters.js.map +1 -1
- package/dist/definition.d.ts +2 -0
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +1 -1
- package/dist/definition.js.map +1 -1
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +35 -2
- package/dist/doctor.js.map +1 -1
- package/dist/effect-recovery-coordinator.d.ts +2 -0
- package/dist/effect-recovery-coordinator.d.ts.map +1 -1
- package/dist/effect-recovery-coordinator.js +1 -1
- package/dist/effect-recovery-coordinator.js.map +1 -1
- package/dist/effect-worker.js +1 -1
- package/dist/effect-worker.js.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/outcome.d.ts +18 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +2 -0
- package/dist/outcome.js.map +1 -0
- package/dist/platform/uuid.d.ts +1 -0
- package/dist/platform/uuid.d.ts.map +1 -1
- package/dist/platform/uuid.js +5 -0
- package/dist/platform/uuid.js.map +1 -1
- package/dist/records.d.ts +6 -0
- package/dist/records.d.ts.map +1 -1
- package/dist/redrive.d.ts +60 -0
- package/dist/redrive.d.ts.map +1 -0
- package/dist/redrive.js +239 -0
- package/dist/redrive.js.map +1 -0
- package/dist/reference.d.ts +17 -3
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +10 -0
- package/dist/reference.js.map +1 -1
- package/dist/reminder-scheduler.js +1 -1
- package/dist/reminder-scheduler.js.map +1 -1
- package/dist/repository.d.ts +34 -4
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +124 -21
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +42 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +263 -6
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +97 -1
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wake-up/postgresql.d.ts +4 -2
- package/dist/wake-up/postgresql.d.ts.map +1 -1
- package/dist/wake-up/postgresql.js +9 -0
- package/dist/wake-up/postgresql.js.map +1 -1
- package/dist/wake-up/redis.d.ts +2 -1
- package/dist/wake-up/redis.d.ts.map +1 -1
- package/dist/wake-up/redis.js +6 -0
- package/dist/wake-up/redis.js.map +1 -1
- package/dist/wake-up-selection.d.ts +22 -0
- package/dist/wake-up-selection.d.ts.map +1 -0
- package/dist/wake-up-selection.js +198 -0
- package/dist/wake-up-selection.js.map +1 -0
- package/dist/wake-up.d.ts +15 -0
- package/dist/wake-up.d.ts.map +1 -1
- package/dist/wake-up.js +6 -0
- package/dist/wake-up.js.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/docs/api.md +202 -6
- package/docs/cloudflare.md +1 -0
- package/docs/configuration.md +52 -22
- package/docs/correctness.md +8 -0
- package/docs/dashboard.md +9 -0
- package/docs/fit.md +3 -1
- package/docs/operations.md +104 -7
- package/docs/parity.md +100 -25
- package/package.json +1 -1
package/docs/api.md
CHANGED
|
@@ -45,6 +45,9 @@ authorization, capability boundaries, and release validation.
|
|
|
45
45
|
- `Actor`: base class providing `ref()`, `actorId`, `currentMessage`,
|
|
46
46
|
`observables()`, `reject()`, `emit()`, `transmit()`, `commitAction()`,
|
|
47
47
|
`schedule()`, `sendTo()`, and protected lifecycle hooks.
|
|
48
|
+
`ScheduledOperationsFor` types the map `schedule()` returns, whose calls return a
|
|
49
|
+
`ReminderHandle`. `TransmittedOperationsFor` types the map `transmit()`
|
|
50
|
+
returns, whose calls return nothing.
|
|
48
51
|
- `broadcastValue(value)`: mark an observable so its changed value enters the
|
|
49
52
|
durable invalidation envelope.
|
|
50
53
|
- `broadcastInvalidation(value)`: compare the real observable value but put
|
|
@@ -67,10 +70,16 @@ createdAtMs }` shape returned by `SolidObjectsRuntime.snapshotWithIncarnation`.
|
|
|
67
70
|
`DestroyOptions`: the options for authorization, idempotency, time, and
|
|
68
71
|
schedule that the reference methods use.
|
|
69
72
|
|
|
73
|
+
`ScheduledReminder` is one armed reminder as an actor reads it, and
|
|
74
|
+
`ReminderReader` is how a runtime supplies them.
|
|
75
|
+
|
|
70
76
|
`ActorIntents`, `EffectIntent`, `CommitActionIntent`, `ReminderIntent`,
|
|
77
|
+
`UnscheduleIntent`, `UnscheduleAllIntent`, `ReminderMutation`,
|
|
71
78
|
`OutboundMessageIntent`, `ReminderOptions`, `OutboundMessageOptions`,
|
|
72
79
|
`PayloadBroadcasts`, and `PayloadBroadcastValue` describe actor-declared
|
|
73
|
-
transactional work and typed personalized projections.
|
|
80
|
+
transactional work and typed personalized projections. `ReminderMutation` is the
|
|
81
|
+
union of one scheduled reminder and the two cancellations, held in one list so
|
|
82
|
+
they apply in the order the turn called them.
|
|
74
83
|
|
|
75
84
|
`EffectFailurePayload<Arguments>`, `EffectSuccessPayload<Arguments, Result>`,
|
|
76
85
|
and `SerializedError` describe effect callback messages. They are also exported
|
|
@@ -162,6 +171,130 @@ row per item. It also cannot strand an entry when the runtime coalesces an
|
|
|
162
171
|
occurrence. Prefer it for a large queue of interchangeable items. Prefer `key`
|
|
163
172
|
when one item needs an alarm that you can move on its own.
|
|
164
173
|
|
|
174
|
+
#### Cancelling a reminder
|
|
175
|
+
|
|
176
|
+
`schedule()` returns a `ReminderHandle` (`{ name: string }`) naming the alarm it
|
|
177
|
+
armed. `unschedule()` cancels one alarm, by operation, by operation and key, or
|
|
178
|
+
by that handle. `unscheduleAll()` cancels every key of one operation.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const MONTH = 30 * 24 * 60 * 60 * 1000
|
|
182
|
+
|
|
183
|
+
class Subscription extends Actor {
|
|
184
|
+
static override readonly actorType = "subscriptions"
|
|
185
|
+
|
|
186
|
+
status = "trialing"
|
|
187
|
+
renewal: ReminderHandle | null = null
|
|
188
|
+
|
|
189
|
+
startTrial(): void {
|
|
190
|
+
this.schedule({ at: new Date(Date.now() + (14 * MONTH) / 30) }).trialExpired()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
convertToPaid(): void {
|
|
194
|
+
this.status = "active"
|
|
195
|
+
this.unschedule("trialExpired")
|
|
196
|
+
this.renewal = this.schedule({
|
|
197
|
+
at: new Date(Date.now() + MONTH),
|
|
198
|
+
everyMilliseconds: MONTH,
|
|
199
|
+
}).chargeRenewal()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
cancelled(): void {
|
|
203
|
+
this.status = "cancelled"
|
|
204
|
+
if (this.renewal) this.unschedule(this.renewal)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
trialExpired(): void {
|
|
208
|
+
this.status = "expired"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
chargeRenewal(): void {}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`this.unschedule(this.renewal)` and `this.unschedule("chargeRenewal")` cancel the
|
|
216
|
+
same alarm. Prefer the handle when the actor already stored one, because it
|
|
217
|
+
cannot drift from the name that armed the reminder.
|
|
218
|
+
|
|
219
|
+
A keyed alarm cancels by the key that armed it, and `unscheduleAll()` cancels
|
|
220
|
+
every key of one operation:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
class Shipment extends Actor {
|
|
224
|
+
static override readonly actorType = "shipments"
|
|
225
|
+
|
|
226
|
+
dispatch({ carrierIds }: { carrierIds: string[] }): void {
|
|
227
|
+
for (const carrierId of carrierIds) {
|
|
228
|
+
this.schedule({ at: new Date(Date.now() + MONTH / 30), key: carrierId }).chaseCarrier({
|
|
229
|
+
carrierId,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
shipped({ carrierId }: { carrierId: string }): void {
|
|
235
|
+
this.unschedule("chaseCarrier", { key: carrierId })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
stopChasing(): void {
|
|
239
|
+
this.unscheduleAll("chaseCarrier")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
chaseCarrier(_options: { carrierId: string }): void {}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`unschedule()` and `unscheduleAll()` refuse an operation the actor does not
|
|
247
|
+
declare, with the `UnknownOperation` that `schedule()` already throws, so a typo
|
|
248
|
+
fails the turn rather than cancelling nothing. A handle skips that check, because
|
|
249
|
+
the `schedule()` call that produced it was already checked.
|
|
250
|
+
|
|
251
|
+
#### Reading the schedule
|
|
252
|
+
|
|
253
|
+
`reminder()` returns the armed alarm as a `ScheduledReminder`, or `undefined`.
|
|
254
|
+
`reminders()` returns every key of one operation. Both are async, because an
|
|
255
|
+
actor reads its own rows rather than holding them in memory:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
async nextChargeAt(): Promise<number | null> {
|
|
259
|
+
return (await this.reminder("chargeRenewal"))?.runAtMilliseconds ?? null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async pendingCarriers(): Promise<(string | null)[]> {
|
|
263
|
+
return (await this.reminders("chaseCarrier")).map((reminder) => reminder.key)
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A read applies the intents staged so far in the turn, so an actor that schedules
|
|
268
|
+
and then reads sees what the commit will write, and one that cancels and then
|
|
269
|
+
reads sees the alarm gone.
|
|
270
|
+
|
|
271
|
+
`key` and `intervalMilliseconds` are `null` rather than `undefined` when absent,
|
|
272
|
+
so a `ScheduledReminder` returns from an operation without a serialization error.
|
|
273
|
+
|
|
274
|
+
Reading is available during a turn and from a snapshot projection, so an
|
|
275
|
+
observable can report what is armed. `reminder()` and `reminders()` refuse an
|
|
276
|
+
operation the actor does not declare, as `schedule()` and `unschedule()` do.
|
|
277
|
+
|
|
278
|
+
A one-shot that already fired is not reported. Its row stays as `completed`, and
|
|
279
|
+
an alarm that cannot fire again is not armed.
|
|
280
|
+
|
|
281
|
+
`ScheduledReminder` carries no occurrence count. The SQL backends track one and
|
|
282
|
+
Durable Objects does not, so it is left out rather than reported for one backend
|
|
283
|
+
only.
|
|
284
|
+
|
|
285
|
+
A cancellation is staged like a schedule, so it commits with the state change
|
|
286
|
+
that decided it and a turn that throws cancels nothing. Both apply in the order
|
|
287
|
+
the turn called them, so cancelling and then scheduling the same name leaves it
|
|
288
|
+
armed at the new time.
|
|
289
|
+
|
|
290
|
+
Cancelling an alarm that does not exist is not an error. `unschedule()` returns
|
|
291
|
+
nothing, because it stages an intent rather than applying one, and an answer
|
|
292
|
+
given at call time could be stale by the time the turn commits.
|
|
293
|
+
|
|
294
|
+
A handle is a plain object, so it survives in actor state and still cancels
|
|
295
|
+
after a deactivation. Passing a handle together with `key` is a `TypeError`,
|
|
296
|
+
because the handle already names the key.
|
|
297
|
+
|
|
165
298
|
### Recovering abandoned effects
|
|
166
299
|
|
|
167
300
|
`emit` returns an `EffectHandle` (`{ id: string }`) on every successful call. Save
|
|
@@ -358,9 +491,11 @@ effect results and the handler's declared argument/result types in agreement.
|
|
|
358
491
|
### Typed operation references
|
|
359
492
|
|
|
360
493
|
`schedule` and `transmit` infer this actor's operation names and arguments, including
|
|
361
|
-
inside actor methods and for inherited application operations.
|
|
362
|
-
|
|
363
|
-
|
|
494
|
+
inside actor methods and for inherited application operations. A scheduled operation
|
|
495
|
+
call returns a `ReminderHandle`, through `ScheduledOperationsFor<ActorType>`, and a
|
|
496
|
+
transmitted one returns `void`, through `TransmittedOperationsFor<ActorType>`. Both
|
|
497
|
+
preserve required, optional, and zero-argument operation signatures. No non-null
|
|
498
|
+
assertion is needed:
|
|
364
499
|
|
|
365
500
|
```typescript
|
|
366
501
|
class ChatRun extends Actor {
|
|
@@ -415,7 +550,49 @@ runtime scheduling and transmission behavior are unchanged.
|
|
|
415
550
|
Every manager below is available as a property on `SolidObjectsRuntime`; the
|
|
416
551
|
class and result types are also exported for integration typing.
|
|
417
552
|
|
|
553
|
+
- `runtime.findBy({ requestId })` and `reference.findBy({ idempotencyKey })`
|
|
554
|
+
rebuild a `MessageReference` for work whose reference a caller lost. A request
|
|
555
|
+
id is unique across the table, so the runtime answers it; an idempotency key
|
|
556
|
+
is unique per actor, so a reference supplies that scope.
|
|
557
|
+
`runtime.findBy({ reference, idempotencyKey })` is the explicit form. Naming
|
|
558
|
+
neither key, naming both, or naming an idempotency key without a reference
|
|
559
|
+
throws a `TypeError`. An absent row, an unregistered actor, and a caller the
|
|
560
|
+
policy refuses all return `undefined`.
|
|
561
|
+
- An actor remembers the idempotency keys of its own last
|
|
562
|
+
`retainedIdempotencyKeys` finished turns, so `reference.findBy({
|
|
563
|
+
idempotencyKey })` throws `MessagePruned` for a key the actor remembers and
|
|
564
|
+
whose message retention removed, and returns `undefined` for a key no caller
|
|
565
|
+
ever sent. An actor remembers the operation and original arguments beside each key, so the pruned
|
|
566
|
+
answer runs the same hook against the same operation and arguments a lookup of the
|
|
567
|
+
surviving row would, and a caller the policy refuses reads `undefined` for
|
|
568
|
+
both. `runtime.findBy({ requestId })` returns
|
|
569
|
+
`undefined` in both cases, because the runtime generates a request id and no
|
|
570
|
+
actor remembers one.
|
|
571
|
+
- `retainedIdempotencyKeysBytes` bounds the serialized memory as well, because
|
|
572
|
+
an idempotency key has no length limit and the memory outlives the message
|
|
573
|
+
row. An actor drops its oldest keys until the list fits, so a key long enough
|
|
574
|
+
to fill the limit by itself is never remembered and its lookup answers
|
|
575
|
+
`undefined` rather than throwing.
|
|
576
|
+
- Remembered arguments count toward the serialized memory limit and remain until
|
|
577
|
+
the entry is evicted or the instance is removed. Entries from older versions
|
|
578
|
+
that lack arguments return absence after pruning because their original
|
|
579
|
+
authorization cannot be reproduced.
|
|
580
|
+
- `messageReference.outcome()` returns an `Outcome`: the status, the result, an
|
|
581
|
+
`ErrorRecord` for a dead message, a `RejectionRecord` for a rejected one, and
|
|
582
|
+
the attempt count.
|
|
418
583
|
- `runtime.deadLetters` / `DeadLetterManager`: `all()` and idempotent `retry()`.
|
|
584
|
+
- `runtime.deadLetters.effects` and `runtime.deadLetters.broadcasts`: a
|
|
585
|
+
`DeadLetterScope` for one `DeadLetterKind`. `all()` lists its dead rows as
|
|
586
|
+
`DeadRow` values, `retry(id)` returns one to pending, and `redrive(options)`
|
|
587
|
+
moves a whole scope. `RedriveOptions` names `actorType`, `failedAfter`, and
|
|
588
|
+
`limit`, which become the `RedriveFilters` the task records. `UnknownDeadRow`
|
|
589
|
+
reports an id that does not exist.
|
|
590
|
+
- `runtime.redrives` / `RedriveManager`: `find(id)`, `all({ status })`,
|
|
591
|
+
`cancel(id)`, and `advance()`, which moves one bounded batch. A `RedriveTask`
|
|
592
|
+
carries its id, kind, filters, `RedriveStatus`, `moved`, `remaining`,
|
|
593
|
+
`startedAt`, `finishedAt`, and its own `cancel()`. `RedriveScheduler` is the
|
|
594
|
+
component that advances tasks inside `runtime.run()`. `UnknownRedrive` and
|
|
595
|
+
`RedriveNotStarted` report a missing task and a task that could not open.
|
|
419
596
|
- `runtime.reminders` / `ReminderManager`: cursor-paginated `all()` and
|
|
420
597
|
idempotent paused-alarm `resume()`.
|
|
421
598
|
- `runtime.processes` / `ProcessManager`: immutable role `all()` and stale-owner
|
|
@@ -529,13 +706,32 @@ wait. `WakeUpWatch.wait()` returns `true` for a notification and `false` for a
|
|
|
529
706
|
timeout or cancellation. A legacy `void` result remains accepted and preserves
|
|
530
707
|
the fast polling cadence.
|
|
531
708
|
|
|
709
|
+
`configuration.wakeUp` takes a name or an adapter. `WakeUpSetting` is that
|
|
710
|
+
union, `WakeUpName` is one of `WAKE_UP_NAMES`, and the default is
|
|
711
|
+
`"automatic"`. Selection prefers `SOLID_OBJECTS_REDIS_URL`, then PostgreSQL
|
|
712
|
+
notifications, then polling. An unknown name throws rather than polls.
|
|
713
|
+
|
|
714
|
+
`selectWakeUp(options)` runs that choice and returns a `SelectedWakeUp`, which
|
|
715
|
+
pairs the adapter with a `WakeUpCapability`. `WakeUpSelectionOptions` names the
|
|
716
|
+
inputs: the setting, the database, the idle polling interval, a logger, and an
|
|
717
|
+
optional Redis URL and probe timeout. `WakeUpCapability` reports
|
|
718
|
+
`WakeUpAdapterName`, whether the adapter crosses processes, the measured floor
|
|
719
|
+
in milliseconds, and the reason. `runtime.wakeUpAdapter()` and
|
|
720
|
+
`runtime.wakeUpCapability()` return the choice, which is made once per runtime.
|
|
721
|
+
|
|
722
|
+
An adapter may declare its own `defaultCapability`, which is what it reports
|
|
723
|
+
about itself. Selection keeps what a configured adapter declares rather than
|
|
724
|
+
assume, and `runtime.wakeUpCapability()` reports what selection installed. `NotificationWakeUpAdapter` adds
|
|
725
|
+
`channelFor(role)`, which a database that offers a notification channel
|
|
726
|
+
provides through `database.wakeUp(options)`.
|
|
727
|
+
|
|
532
728
|
### Errors
|
|
533
729
|
|
|
534
730
|
The root exports `SolidObjectsError` and its supported subclasses:
|
|
535
731
|
|
|
536
732
|
- policy and caller outcomes: `Unauthorized`, `Rejected`, `ActorDestroyed`,
|
|
537
|
-
`SyncEnqueueTimeout`, `SyncTimeout`, `SyncInsideTransaction`,
|
|
538
|
-
`MessageFailed`;
|
|
733
|
+
`SyncEnqueueTimeout`, `SyncTimeout`, `SyncInsideTransaction`,
|
|
734
|
+
`MessagePruned`, and `MessageFailed`;
|
|
539
735
|
- admission and payload failures: `MailboxFull`, `InvalidPayload`,
|
|
540
736
|
`PayloadTooLarge`, `IdempotencyConflict`, `InvalidPayloadBroadcast`, and
|
|
541
737
|
`UnknownPayloadBroadcast`;
|
package/docs/cloudflare.md
CHANGED
|
@@ -124,6 +124,7 @@ replay current committed projections, not every event missed while offline.
|
|
|
124
124
|
| Actor-scoped dead letters and reminder administration | Supported |
|
|
125
125
|
| `commitAction`, shared application SQL transactions | Unsupported |
|
|
126
126
|
| Global repository, reconciliation, process controls, SQL dashboard | Unsupported |
|
|
127
|
+
| `findBy({ requestId })` without a reference | Unsupported; name the actor |
|
|
127
128
|
| Process-local `runtime.realtime.connect()` / server `ref.live` | Unsupported; use browser subscriptions |
|
|
128
129
|
| SQL-to-Durable-Objects data migration | Not provided |
|
|
129
130
|
|
package/docs/configuration.md
CHANGED
|
@@ -49,19 +49,19 @@ as an instruction to divide the actor. See
|
|
|
49
49
|
|
|
50
50
|
## Runtime roles and supervision
|
|
51
51
|
|
|
52
|
-
| Option |
|
|
53
|
-
| ------------------------------------------- |
|
|
54
|
-
| `workerCount` |
|
|
55
|
-
| `effectWorkerCount` |
|
|
56
|
-
| `broadcastWorkerCount` |
|
|
57
|
-
| `reminderSchedulerCount` |
|
|
58
|
-
| `processHeartbeatIntervalMilliseconds` |
|
|
59
|
-
| `processAliveThresholdMilliseconds` |
|
|
60
|
-
| `shutdownTimeoutMilliseconds` |
|
|
61
|
-
| `supervisorRestartDelayMilliseconds` |
|
|
62
|
-
| `supervisorMaximumRestartDelayMilliseconds` |
|
|
63
|
-
| `wakeUp` |
|
|
64
|
-
| `logger` |
|
|
52
|
+
| Option | Default | Contract |
|
|
53
|
+
| ------------------------------------------- | --------------: | ----------------------------------------------------------------- |
|
|
54
|
+
| `workerCount` | `1` | Non-negative actor workers. |
|
|
55
|
+
| `effectWorkerCount` | `1` | Non-negative effect workers. |
|
|
56
|
+
| `broadcastWorkerCount` | `1` | Non-negative broadcast workers when realtime is enabled. |
|
|
57
|
+
| `reminderSchedulerCount` | `1` | Non-negative reminder schedulers. |
|
|
58
|
+
| `processHeartbeatIntervalMilliseconds` | `15_000` | Positive persisted heartbeat cadence. |
|
|
59
|
+
| `processAliveThresholdMilliseconds` | `60_000` | Positive age after which an owner is stale. |
|
|
60
|
+
| `shutdownTimeoutMilliseconds` | `15_000` | Positive shared graceful-shutdown budget. |
|
|
61
|
+
| `supervisorRestartDelayMilliseconds` | `100` | Positive initial failed-role replacement delay. |
|
|
62
|
+
| `supervisorMaximumRestartDelayMilliseconds` | `10_000` | Positive cap no smaller than the initial delay. |
|
|
63
|
+
| `wakeUp` | `"automatic"` | A name or an adapter implementing `watch`, `notify`, and `close`. |
|
|
64
|
+
| `logger` | console methods | Structured `debug`, `info`, `warn`, and `error` sink. |
|
|
65
65
|
|
|
66
66
|
Counts may be zero, but the complete configuration must leave at least one
|
|
67
67
|
runtime role enabled. Broadcast workers are started only when `broadcast` or
|
|
@@ -78,15 +78,20 @@ polling remains the correctness path.
|
|
|
78
78
|
|
|
79
79
|
## Retention and cleanup
|
|
80
80
|
|
|
81
|
-
| Option |
|
|
82
|
-
| ---------------------------------------- |
|
|
83
|
-
| `retentionIntervalMilliseconds` |
|
|
84
|
-
| `deadProcessCleanupIntervalMilliseconds` |
|
|
85
|
-
| `messageRetentionMilliseconds` |
|
|
86
|
-
| `messageRetentionByActorType` |
|
|
87
|
-
| `instanceRetentionByActorType` |
|
|
88
|
-
| `processRetentionMilliseconds` |
|
|
89
|
-
| `pruneBatchSize` |
|
|
81
|
+
| Option | Default | Contract |
|
|
82
|
+
| ---------------------------------------- | ----------------: | ---------------------------------------------------------------- |
|
|
83
|
+
| `retentionIntervalMilliseconds` | `3_600_000` | Non-negative automatic history-pruning cadence; `0` disables it. |
|
|
84
|
+
| `deadProcessCleanupIntervalMilliseconds` | `60_000` | Non-negative stale-owner recovery cadence; `0` disables it. |
|
|
85
|
+
| `messageRetentionMilliseconds` | 30 days | Positive default completed-message retention. |
|
|
86
|
+
| `messageRetentionByActorType` | `{}` | Positive per-type message-retention overrides. |
|
|
87
|
+
| `instanceRetentionByActorType` | `{}` | Positive per-type instance-expiration opt-ins. |
|
|
88
|
+
| `processRetentionMilliseconds` | 7 days | Positive stopped-process retention. |
|
|
89
|
+
| `pruneBatchSize` | `1_000` | Positive integer maximum rows rechecked per pruning transaction. |
|
|
90
|
+
| `retainedIdempotencyKeys` | `64` | Positive integer idempotency keys an actor remembers. |
|
|
91
|
+
| `retainedIdempotencyKeysBytes` | `16_384` | Positive integer serialized size those keys must fit. |
|
|
92
|
+
| `redriveBatchSize` | `100` | Positive integer rows a redrive moves per transaction. |
|
|
93
|
+
| `redriveBatchPauseMilliseconds` | `50` | Non-negative pause between redrive batches. |
|
|
94
|
+
| `administrationIdentity` | `String(context)` | Names the operator recorded in an administration event. |
|
|
90
95
|
|
|
91
96
|
Automatic retention prunes messages and stopped process records. Actor
|
|
92
97
|
instance expiration remains an explicit administration action even when a type
|
|
@@ -158,6 +163,31 @@ short capped backoff only when no synchronous deadline is active.
|
|
|
158
163
|
| `applicationName` | `"solid-objects"` |
|
|
159
164
|
| `onPoolError` | structured `console.error` |
|
|
160
165
|
|
|
166
|
+
`wakeUp` takes a name or an adapter, as `config.cache_store` does in Rails.
|
|
167
|
+
`"automatic"` is the default. It prefers `SOLID_OBJECTS_REDIS_URL`, then
|
|
168
|
+
PostgreSQL notifications, then polling. `"in_process"` opts out, `"postgresql"`
|
|
169
|
+
and `"redis"` force one, and an unknown name throws rather than polls quietly.
|
|
170
|
+
|
|
171
|
+
A requested adapter that the environment cannot provide polls instead and says
|
|
172
|
+
so. `"postgresql"` on a database with no notification channel, `"redis"` without
|
|
173
|
+
`SOLID_OBJECTS_REDIS_URL`, and a Redis URL without the `redis` package each log
|
|
174
|
+
`solid_objects.wake_up.unavailable` once and report the reason in the
|
|
175
|
+
capability. Only a name that does not exist is refused, because a typo cannot be
|
|
176
|
+
honoured at all.
|
|
177
|
+
|
|
178
|
+
Selection runs once per runtime, on first use. `runtime.wakeUpCapability()`
|
|
179
|
+
reports what it chose, whether that choice crosses processes, its measured
|
|
180
|
+
floor in milliseconds, and why. The doctor reports the same record, and the
|
|
181
|
+
polling-only warning fires on what was installed rather than on whether a
|
|
182
|
+
setting was set. An adapter that declares its own `defaultCapability` keeps it, so a
|
|
183
|
+
configured `InProcessWakeUpAdapter` still warns.
|
|
184
|
+
|
|
185
|
+
Automatic selection proves the PostgreSQL path before it chooses it. It listens
|
|
186
|
+
on a probe channel, sends one `NOTIFY` from a second connection, and waits up to
|
|
187
|
+
two seconds for it to arrive. A probe that does not deliver falls back to
|
|
188
|
+
polling and warns once, because `LISTEN` does not survive a transaction pooler
|
|
189
|
+
such as PgBouncer.
|
|
190
|
+
|
|
161
191
|
`database.wakeUp(options)` creates a dedicated notification adapter using the
|
|
162
192
|
same connection string. Its options are `channelPrefix = "solid_objects"`,
|
|
163
193
|
`applicationName = "solid-objects-wake-up"`, and `onListenerError`. Use a
|
package/docs/correctness.md
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
- Delivery is ordered per actor identity and at least once.
|
|
6
6
|
- Different identities may execute concurrently.
|
|
7
7
|
- Sequence allocation and durable enqueue are one transaction.
|
|
8
|
+
- An actor reads its own schedule. A read applies the intents staged so far in
|
|
9
|
+
the turn, so it agrees with what the commit will write rather than with what
|
|
10
|
+
the turn began with.
|
|
11
|
+
- A reminder can be cancelled. A cancellation commits with the state change that
|
|
12
|
+
decided it, and applies in the order the turn called it. A cancellation cannot
|
|
13
|
+
recall an occurrence the scheduler already turned into a message. It does
|
|
14
|
+
pre-empt one the scheduler claimed but has not yet enqueued, and the scheduler
|
|
15
|
+
treats that as ordinary work rather than a failure.
|
|
8
16
|
- Concurrent callers that create the same actor produce one instance row and
|
|
9
17
|
distinct sequences. The mailbox locks that row by its primary key, so MySQL
|
|
10
18
|
does not upgrade a shared lock and the enqueue does not deadlock.
|
package/docs/dashboard.md
CHANGED
|
@@ -141,6 +141,15 @@ Dead-letter retry calls `runtime.deadLetters.retry()`. It keeps the durable
|
|
|
141
141
|
idempotency and the actor-operation validation of that method. If the runtime
|
|
142
142
|
refuses a retry, the detail page shows it with status 422.
|
|
143
143
|
|
|
144
|
+
Dead effects and broadcasts have the same API, which the dashboard does not yet
|
|
145
|
+
surface. `runtime.deadLetters.effects` and `runtime.deadLetters.broadcasts` read
|
|
146
|
+
and retry their own kind, and `redrive` moves a whole scope as a durable task.
|
|
147
|
+
See [Operations](operations.md) for both.
|
|
148
|
+
|
|
149
|
+
Every retry and every redrive transition writes one row to
|
|
150
|
+
`solid_objects_administration_events`, holding the action, the kind, the
|
|
151
|
+
subject, and the identity that asked for it.
|
|
152
|
+
|
|
144
153
|
`HEAD /` performs only a schema reachability query and creates no CSRF session
|
|
145
154
|
state. Use it for liveness checks instead of polling the full dashboard.
|
|
146
155
|
|
package/docs/fit.md
CHANGED
|
@@ -43,7 +43,9 @@ history, and no actor-state migration contract.
|
|
|
43
43
|
limiter, see [Solid Objects Pro](https://solidobjects.pro/): grouped commits
|
|
44
44
|
coalesce concurrent writes into one insert, and ephemeral operations keep
|
|
45
45
|
loss-tolerant calls out of the durable journal. It ships for the Rails gem
|
|
46
|
-
today, and the Node build is in development.
|
|
46
|
+
today, and the Node build is in development. Distributed per-actor rate
|
|
47
|
+
limits, global admission control, and cache-capacity eviction are answered
|
|
48
|
+
there rather than in this runtime, and are not open roadmap items here.
|
|
47
49
|
- Durable workflow replay across named steps is more important than a mutable
|
|
48
50
|
object with ordered operations. The runtime redelivers an ordered message and
|
|
49
51
|
retries it. It does not replay a function from a step log.
|
package/docs/operations.md
CHANGED
|
@@ -14,16 +14,25 @@ work and wake-up notifications reset the role to the fast interval. Actor
|
|
|
14
14
|
workers clamp the ceiling to `leaseRenewalIntervalMilliseconds` while they may
|
|
15
15
|
hold cached activations.
|
|
16
16
|
|
|
17
|
-
The
|
|
17
|
+
The runtime selects a wake-up adapter on first use. It prefers
|
|
18
|
+
`SOLID_OBJECTS_REDIS_URL`, then PostgreSQL notifications, then polling.
|
|
19
|
+
The generation-based in-process adapter interrupts waits for new actor
|
|
18
20
|
messages, effects, reminders, and broadcasts in the same Node process. It does
|
|
19
|
-
not cross a process boundary. When live processes share the database
|
|
20
|
-
|
|
21
|
-
`solid_objects.polling_only_cross_process_wake_up` once.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
idle polling interval. The runtime isolates notification errors and logs them by
|
|
21
|
+
not cross a process boundary. When live processes share the database and the
|
|
22
|
+
installed adapter does not cross processes, the runtime logs
|
|
23
|
+
`solid_objects.polling_only_cross_process_wake_up` once. Without a
|
|
24
|
+
cross-process adapter, newly committed work can wait for the current idle
|
|
25
|
+
polling interval. The runtime isolates notification errors and logs them by
|
|
25
26
|
role and error class. The committed work does not fail.
|
|
26
27
|
|
|
28
|
+
`runtime.wakeUpCapability()` and the `wakeUp` doctor check report the adapter
|
|
29
|
+
that is installed, whether it crosses processes, its measured floor, and why it
|
|
30
|
+
was chosen. On PostgreSQL, selection first proves the path: it listens on a
|
|
31
|
+
probe channel, notifies it from a second connection, and waits for the
|
|
32
|
+
notification. A probe that does not arrive logs
|
|
33
|
+
`solid_objects.wake_up.pooled_session` once and falls back to polling, because
|
|
34
|
+
`LISTEN` does not survive a transaction pooler such as PgBouncer.
|
|
35
|
+
|
|
27
36
|
The warning excludes process rows with the current hostname and host process ID.
|
|
28
37
|
It can therefore appear during a rolling deployment or restart overlap when an
|
|
29
38
|
older and newer process briefly share the same database. A process that stopped
|
|
@@ -218,3 +227,91 @@ be resumed.
|
|
|
218
227
|
Scheduling an existing actor operation for a different time emits
|
|
219
228
|
`solid_objects.reminder.replaced` after the fenced actor commit. Scheduling it
|
|
220
229
|
for the same time emits nothing.
|
|
230
|
+
|
|
231
|
+
## Dead letters, retry, and redrive
|
|
232
|
+
|
|
233
|
+
A message that exhausts its attempts becomes a dead letter. An effect or a
|
|
234
|
+
broadcast that exhausts its attempts stays in its own table with
|
|
235
|
+
`status = 'dead'`. All three are read and retried through one receiver, which
|
|
236
|
+
carries the kind:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
await runtime.deadLetters.all({ authorizationContext })
|
|
240
|
+
await runtime.deadLetters.retry(deadLetterId, { authorizationContext })
|
|
241
|
+
|
|
242
|
+
await runtime.deadLetters.effects.all({ authorizationContext })
|
|
243
|
+
await runtime.deadLetters.effects.retry(effectId, { authorizationContext })
|
|
244
|
+
await runtime.deadLetters.broadcasts.retry(broadcastId, { authorizationContext })
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
An effect or broadcast retry returns the row to pending with a zero attempt
|
|
248
|
+
count, no claim, and immediate availability, and keeps its id, so a handler that
|
|
249
|
+
deduplicates on the effect id still sees the same key. An effect is
|
|
250
|
+
at-least-once by contract, so a retried effect can run twice.
|
|
251
|
+
|
|
252
|
+
Retry acts only on a dead row. A row that is pending, processing, or completed
|
|
253
|
+
comes back unchanged, so pressing a button twice cannot double-enqueue and
|
|
254
|
+
cannot take a row away from a worker that holds it.
|
|
255
|
+
|
|
256
|
+
An incident produces dead rows in the hundreds, so a scope also answers
|
|
257
|
+
`redrive`:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const task = await runtime.deadLetters.effects.redrive({
|
|
261
|
+
actorType: "payments",
|
|
262
|
+
failedAfter: new Date(Date.now() - 6 * 60 * 60 * 1000),
|
|
263
|
+
limit: 5_000,
|
|
264
|
+
authorizationContext,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
await task.cancel({ authorizationContext })
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`redrive` returns at once. The task is durable, and `runtime.run()` advances one
|
|
271
|
+
bounded batch per pass, so a redrive of thousands of rows never holds a
|
|
272
|
+
transaction longer than one batch. `redriveBatchSize` defaults to 100 and
|
|
273
|
+
`redriveBatchPauseMilliseconds` to 50.
|
|
274
|
+
|
|
275
|
+
A redrive moves the rows that were already dead when it started. A row that
|
|
276
|
+
fails again lands back in the same scope, and without that bound a task whose
|
|
277
|
+
handler is still broken would move it forever.
|
|
278
|
+
|
|
279
|
+
A redrive is idempotent over its scope and its filters. Starting the same one
|
|
280
|
+
while it runs returns the running task rather than a second one, which a
|
|
281
|
+
dashboard button an operator can press twice needs. A different scope or a
|
|
282
|
+
different filter starts its own task, and the same scope can be redriven again
|
|
283
|
+
once the first task finishes.
|
|
284
|
+
|
|
285
|
+
Read tasks back with `runtime.redrives`:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
await runtime.redrives.find(task.id, { authorizationContext })
|
|
289
|
+
await runtime.redrives.all({ status: "running", authorizationContext })
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
A running task reports what is left to move rather than a stored estimate,
|
|
293
|
+
because rows die and are retried while it runs.
|
|
294
|
+
|
|
295
|
+
Retry, redrive, and cancel each go through `authorizeAdministration` under their
|
|
296
|
+
own resource name: `dead_letters`, `effect_dead_letters`,
|
|
297
|
+
`broadcast_dead_letters`, and `redrives`. Every retry and every task transition
|
|
298
|
+
writes one row to `solid_objects_administration_events`, holding the action, the
|
|
299
|
+
kind, the subject, the identity, and when it happened. The identity comes from
|
|
300
|
+
`administrationIdentity`, which receives the authorization context the caller
|
|
301
|
+
passed and defaults to its `String` form.
|
|
302
|
+
|
|
303
|
+
An event records an authorized press, not a state transition. Pressing retry
|
|
304
|
+
twice writes two rows, because an operator did two things and a log that shows
|
|
305
|
+
one cannot answer who pressed what. The row the event names carries the outcome.
|
|
306
|
+
A refused caller writes nothing, and a retry that raises after the lookup writes
|
|
307
|
+
nothing, because the event shares the transaction with the work. The redrive
|
|
308
|
+
transitions are different: `redrive.start`, `redrive.finish`, and
|
|
309
|
+
`redrive.cancel` are written only when the task actually changes.
|
|
310
|
+
|
|
311
|
+
The Durable Objects engine keeps its own message and outbox tables inside each
|
|
312
|
+
object, so these scopes and redrive cover the SQL backends. `deadLetters` there
|
|
313
|
+
reports its own dead rows as it did before.
|
|
314
|
+
|
|
315
|
+
Automatic redrive on a schedule is deliberately absent. A dead row means a
|
|
316
|
+
person decided something, and these APIs give that person an alternative to an
|
|
317
|
+
`UPDATE` against a runtime table.
|