// `orders.fulfill` — EXECUTOR (server-only). // // Durable order-fulfillment pipeline. The default export is a factory // `(ctx) => execute`; the CLI calls `workflow.toLayer(execute)` at boot // so the executor closes over the live `AppContext` (store, events, …). // // The flow, every step durable (replayed from its journaled result on a // crash/resume rather than re-run): // // 1. step 'reserve-stock' — flip the order to `reserved`. // 2. sleep — a (short) durable delay; the wake time is // journaled in the engine. Bump to hours/days // for a real "hold before charging" window. // 3. awaitSignal 'approval' — PARK until a human sends the approval // signal (via the `orders.approve` mutation // or the dashboard "Send signal" button). // 4. step 'ship' / mark rejected — branch on the decision. // // `withCompensation` wraps the reserve step with saga-style rollback: // the finalizer runs only if the WHOLE workflow later fails, releasing // the reservation so a downstream failure doesn't strand held stock. import { step, sleep, awaitSignal, withCompensation } from '@voltro/workflow' import { Effect, Schema } from 'effect' import type { AppContext } from '@voltro/runtime' const Decision = Schema.Struct({ approved: Schema.Boolean }) const buildExecute = (ctx: AppContext) => (payload: { orderId: string; tenantId: string }, _executionId: string) => Effect.gen(function* () { const { orderId } = payload // 1. Reserve stock. `withCompensation` registers an undo that runs // only if the workflow as a whole fails after this point. yield* withCompensation( step({ name: 'reserve-stock', input: { orderId }, success: Schema.Struct({ orderId: Schema.String }), execute: Effect.tryPromise({ try: async () => { await ctx.store.update('orders', orderId, { status: 'reserved' }) return { orderId } }, catch: (error) => error as never, }), }), // Saga rollback: release the reservation on whole-workflow failure. () => Effect.promise(() => ctx.store .update('orders', orderId, { status: 'placed' }) .then(() => undefined), ), ) // 2. Durable delay. The wake is journaled — survives a restart. yield* sleep({ name: 'settle-window', duration: '5 seconds' }) // 3. Human-approval gate. The run parks here until an external // caller sends a matching `approval` signal; the parsed payload // is journaled, so a post-signal replay returns it instantly. const decision = yield* awaitSignal(ctx, { name: 'approval', schema: Decision, // pollIntervalMs defaults to 200 (backoff → 5s); timeoutMs 24h. }) if (!decision.approved) { yield* step({ name: 'mark-rejected', input: { orderId }, success: Schema.Struct({ orderId: Schema.String }), execute: Effect.tryPromise({ try: async () => { await ctx.store.update('orders', orderId, { status: 'rejected' }) return { orderId } }, catch: (error) => error as never, }), }) return { orderId, outcome: 'rejected' as const } } // 4. Approved → ship. yield* step({ name: 'ship', input: { orderId }, success: Schema.Struct({ orderId: Schema.String }), execute: Effect.tryPromise({ try: async () => { await ctx.store.update('orders', orderId, { status: 'shipped' }) return { orderId } }, catch: (error) => error as never, }), }) return { orderId, outcome: 'shipped' as const } }) export default buildExecute