@@ -69,13 +69,12 @@ pub enum MyEntityCommand {
6969### 3. Implement EventSourced
7070
7171``` rust
72- #[async_trait]
7372impl EventSourced for MyEntity {
7473 type Id = MyEntityId ; // strongly-typed, Display + FromStr
7574 type Event = MyEntityEvent ;
7675 type Command = MyEntityCommand ;
7776 type Error = Never ; // or a thiserror type
78- type Services = () ; // or Arc<dyn SomeService>
77+ type Jobs = Nil ; // or jobs![SendEmail, ChargeCard]
7978 type Materialized = Table ; // Table for projected, Nil for non-projected
8079
8180 const AGGREGATE_TYPE : & 'static str = " MyEntity" ;
@@ -87,10 +86,10 @@ impl EventSourced for MyEntity {
8786 fn evolve (entity : & Self , event : & Self :: Event )
8887 -> Result <Option <Self >, Self :: Error > { /* ... */ }
8988
90- // Command-side: process commands to produce events
91- async fn initialize (command : Self :: Command , services : & Self :: Services )
89+ // Command-side: sync, pure -- produce events, enqueue jobs
90+ fn initialize (command : Self :: Command , jobs : & mut JobQueue < Self :: Jobs > )
9291 -> Result <Vec <Self :: Event >, Self :: Error > { /* ... */ }
93- async fn transition (& self , command : Self :: Command , services : & Self :: Services )
92+ fn transition (& self , command : Self :: Command , jobs : & mut JobQueue < Self :: Jobs > )
9493 -> Result <Vec <Self :: Event >, Self :: Error > { /* ... */ }
9594}
9695```
@@ -243,32 +242,73 @@ receives `(error, id, event)` and can reprocess the event from the errored state
243242
244243Wire reactors via ` Unwired ` + ` StoreBuilder::wire() ` .
245244
246- ## Services Pattern
245+ ## Jobs Pattern
246+
247+ Handlers are sync and pure: they must not call external systems. Side-effecting
248+ work is enqueued as a durable job and runs in a separate worker. The enqueue
249+ commits in the same SQLite transaction as the events that triggered it, so a job
250+ exists iff its events do.
251+
252+ The worker retries ` perform ` on failure and re-runs it after a crash, so jobs
253+ are delivered ** at-least-once** -- ` perform ` must be idempotent. Carry any dedup
254+ key in the job payload or ` Input ` .
255+
256+ Declare a job and the entity's job list:
257+
258+ ``` rust
259+ #[derive(Serialize , Deserialize )]
260+ struct PlaceOrder { order_id : OrderId , quantity : u32 }
261+
262+ impl Job for PlaceOrder {
263+ type Input = Arc <dyn OrderPlacer >; // dependency bundle, injected by the worker
264+ type Output = ();
265+ type Error = PlaceOrderError ;
247266
248- Inject external dependencies into command handlers:
267+ const WORKER_NAME : & 'static str = " place-order" ;
268+ const KIND : & 'static str = " PlaceOrder" ;
269+
270+ fn label (& self ) -> Label { Label (format! (" place-order:{}" , self . order_id)) }
271+
272+ async fn perform (& self , placer : & Self :: Input ) -> Result <(), Self :: Error > {
273+ placer . place_order (self . order_id, self . quantity). await
274+ }
275+ }
276+ ```
249277
250278``` rust
251- type Services = Arc <dyn OrderPlacer >;
279+ type Jobs = jobs! [PlaceOrder ]; // Nil if the entity dispatches none
280+ ```
252281
253- async fn transition (
282+ Enqueue from a handler through the ` jobs ` handle. ` push ` is synchronous and
283+ buffers; the framework flushes the buffer at commit. ` push::<J> ` only compiles
284+ when ` J ` is in the entity's ` Jobs ` list (` HasJob<J> ` ):
285+
286+ ``` rust
287+ fn transition (
254288 & self ,
255289 command : Self :: Command ,
256- services : & Self :: Services ,
290+ jobs : & mut JobQueue < Self :: Jobs > ,
257291) -> Result <Vec <Self :: Event >, Self :: Error > {
258- let result = services . place_order ( /* ... */ ) . await ? ;
259- Ok (vec! [MyEvent :: OrderPlaced { /* ... */ }])
292+ jobs . push ( PlaceOrder { order_id : self . id, quantity : command . quantity }) ;
293+ Ok (vec! [MyEvent :: OrderRequested { /* ... */ }])
260294}
261295```
262296
263- Pass services when building the ` Store ` :
297+ A queued row is never observable mid-handler -- the push is buffered, not a live
298+ ` INSERT ` . Reads a handler once did via ` Services ` (clock, config, idempotency
299+ keys) move into the ` Command ` ; the caller supplies them at dispatch time.
300+
301+ Run the jobs in a worker, wired with the shared retry/backoff/circuit-breaker
302+ policy. ` Input ` is the only context handed in:
264303
265304``` rust
266- let store = StoreBuilder :: <MyEntity >:: new (pool )
267- . build (services )
268- . await ? ;
269- ```
305+ let placer : Arc <dyn OrderPlacer > = Arc :: new (/* ... */ );
306+ let backend = JobBackend :: <PlaceOrder >:: new (& pool );
270307
271- For entities that don't need services, use ` type Services = () ` .
308+ Monitor :: new (). register (build_supervised_worker! (
309+ :: <PlaceOrder >, 0 , backend . into_storage (), placer , fail_stop , failure_notify ,
310+ ));
311+ ```
272312
273313## Schema Versioning
274314
@@ -640,38 +680,31 @@ the same result.
640680** Call replay at startup** to ensure views are up-to-date with any schema
641681changes.
642682
643- ## Services Pattern
683+ ## Jobs and Crash Safety
644684
645- Domain types can depend on external services (APIs, blockchain, etc.) via the
646- ` Services ` associated type on ` EventSourced ` :
685+ The reason handlers enqueue jobs instead of calling out directly: a job's
686+ enqueue and its triggering events commit in one SQLite transaction. There is no
687+ window where a side effect fires but no event records it, and none where an
688+ event commits but the follow-up work is lost.
647689
648690``` rust
649- #[async_trait]
650- impl EventSourced for MyEntity {
651- type Services = Arc <dyn MyService >; // or () if none needed
652-
653- async fn transition (
654- & self ,
655- command : Self :: Command ,
656- services : & Self :: Services ,
657- ) -> Result <Vec <Self :: Event >, Self :: Error > {
658- let result = services . do_something (). await ? ;
659- Ok (vec! [MyEvent :: SomethingDone { result }])
660- }
661- // ...
691+ fn transition (
692+ & self ,
693+ command : Self :: Command ,
694+ jobs : & mut JobQueue <Self :: Jobs >,
695+ ) -> Result <Vec <Self :: Event >, Self :: Error > {
696+ jobs . push (SettlePayment { id : self . id }); // buffered, not yet persisted
697+ Ok (vec! [MyEvent :: PaymentRequested { /* ... */ }])
662698}
663699```
664700
665- Pass services when building the ` Store ` :
666-
667- ``` rust
668- let services : Arc <dyn MyService > = Arc :: new (MyServiceImpl :: new ());
669- let store = StoreBuilder :: <MyEntity >:: new (pool )
670- . build (services )
671- . await ? ;
672- ```
701+ ` push ` only buffers; the framework drains the buffer with a transaction-bound
702+ ` INSERT ` into the apalis ` Jobs ` table alongside the event write. On rollback,
703+ neither is visible. A worker (` build_supervised_worker! ` ) then polls the table
704+ and runs ` Job::perform ` with retries and a circuit breaker. See
705+ [ Jobs Pattern] ( #jobs-pattern ) for the full ` Job ` impl and worker wiring.
673706
674- For entities that don't need services , use ` type Services = () ` .
707+ For entities that dispatch no jobs , use ` type Jobs = Nil ` .
675708
676709## Testing Aggregates
677710
0 commit comments