effect-tea-persistent
    Preparing search index...

    A booking

    A booking of seats for an event, held in the event's inventory and confirmed before a deadline. It shows two entities that talk: the booking's operation sends a request to the inventory, and the booking acts on the reply. A booking cancelled or expired once its seats are held releases them. The scenarios rehearse each entity alone, answering for the other, and then both together in one world. The runtime test runs both in one database.

    /**
    * A booking of seats for an event. It holds them in the event's inventory at once, then waits for the customer
    * to confirm before a deadline. A booking cancelled or expired once its seats are held releases them.
    */
    import * as DateTime from 'effect/DateTime'
    import * as Effect from 'effect/Effect'
    import * as Match from 'effect/Match'
    import * as Schema from 'effect/Schema'
    import {
    defect,
    noWork,
    type Origin,
    operation,
    program,
    reference,
    reject,
    type Rejected,
    send,
    timer,
    } from '@fgaudo/effect-tea-persistent/program'
    import type { inventory } from './inventory.ts'

    // The inventory imports the booking's name too, so each imports only the other's types (decision 0183).
    const Inventory = reference<typeof inventory>('Inventory')

    // Checked with the flags, before the booking exists: a booking holds some seats (decision 0049).
    const Seats = Schema.Int.check(Schema.isGreaterThan(0))

    const Booking = Schema.Struct({
    customer: Schema.String,
    // The inventory's id.
    event: Schema.String,
    seats: Seats,
    deadline: Schema.DateTimeUtcFromMillis,
    })
    type Booking = typeof Booking.Type

    const State = Schema.Union([
    // Only the inventory's reply leaves it: a cancellation meanwhile is remembered.
    Schema.TaggedStruct('Holding', { booking: Booking, cancelled: Schema.Boolean }),
    Schema.TaggedStruct('Held', { booking: Booking }),
    Schema.TaggedStruct('Confirming', { booking: Booking }),
    Schema.TaggedStruct('Releasing', { booking: Booking, reason: Schema.Literals(['ByCustomer', 'Expired']) }),
    Schema.TaggedStruct('Confirmed', { booking: Booking }),
    Schema.TaggedStruct('Cancelled', {
    booking: Booking,
    reason: Schema.Literals(['ByCustomer', 'Expired', 'SoldOut', 'SalesClosed', 'NoSuchEvent']),
    }),
    ])
    export type State = typeof State.Type

    const Flags = Schema.Struct({ customer: Schema.String, event: Schema.String, seats: Seats })
    type Flags = typeof Flags.Type

    const Inbound = Schema.Union([
    Schema.TaggedStruct('ConfirmRequested', {}),
    Schema.TaggedStruct('CancelRequested', {}),
    ])
    type Inbound = typeof Inbound.Type

    const Internal = Schema.Union([
    Schema.TaggedStruct('SeatsHeld', {}),
    Schema.TaggedStruct('SeatsUnavailable', { reason: Schema.Literals(['SoldOut', 'SalesClosed', 'NoSuchEvent']) }),
    Schema.TaggedStruct('SeatsConfirmed', {}),
    Schema.TaggedStruct('SeatsReleased', {}),
    Schema.TaggedStruct('DeadlinePassed', {}),
    ])
    type Internal = typeof Internal.Type

    const Sender = Schema.TaggedStruct('Customer', { id: Schema.String })
    export type Sender = typeof Sender.Type

    // What callers read: it leaves the process, so it has a schema too, and it is JSON: the deadline is its
    // milliseconds (decision 0139).
    const Projection = Schema.Struct({
    status: Schema.Literals(['Holding', 'Held', 'Confirming', 'Releasing', 'Confirmed', 'Cancelled']),
    seats: Schema.Int,
    deadline: Schema.Number,
    canConfirm: Schema.Boolean,
    canCancel: Schema.Boolean,
    })
    export type Projection = typeof Projection.Type

    const holdWindow = { minutes: 15 }

    const isCustomer = (from: Origin<Sender>, booking: Booking) =>
    from._tag === 'Caller' && from.sender.id === booking.customer

    // What the booking asks of the event's inventory, as data: a test sees it without running it.
    const Operation = Schema.TaggedUnion({
    Hold: { event: Schema.String, seats: Seats },
    Confirm: { event: Schema.String },
    Release: { event: Schema.String },
    })
    type Operation = typeof Operation.Type

    // A request is the run of the operation and the name it gives: a hold run again after a crash sends the same
    // request, is answered from the stored reply, and holds nothing twice. So it may run at least once.
    const hold = Effect.fn('Hold')(
    function*({ event, seats }: Extract<Operation, { _tag: 'Hold' }>) {
    const reply = yield* send(Inventory, event, { _tag: 'HoldRequested', seats }, 'hold')
    // A rejection with no reason means the two entities disagree: a booking holds once, and holds some seats.
    return reply._tag === 'Accepted'
    ? { _tag: 'SeatsHeld' } satisfies Internal
    : reply._tag === 'NotFound'
    ? { _tag: 'SeatsUnavailable', reason: 'NoSuchEvent' } satisfies Internal
    : reply.reason !== null
    ? { _tag: 'SeatsUnavailable', reason: reply.reason } satisfies Internal
    : yield* Effect.die('the inventory rejected a hold for no reason')
    },
    )

    // The inventory confirms and releases any hold it made, and existed when it made it. Any other reply means the
    // two entities disagree, which no message of this program can repair: it is a defect, and suspends the booking.
    const confirm = Effect.fn('Confirm')(
    function*({ event }: Extract<Operation, { _tag: 'Confirm' }>) {
    const reply = yield* send(Inventory, event, { _tag: 'ConfirmRequested' }, 'confirm')
    return reply._tag === 'Accepted'
    ? { _tag: 'SeatsConfirmed' } satisfies Internal
    : yield* Effect.die('the inventory did not accept the confirmation of its own hold')
    },
    )

    const release = Effect.fn('Release')(
    function*({ event }: Extract<Operation, { _tag: 'Release' }>) {
    const reply = yield* send(Inventory, event, { _tag: 'ReleaseRequested' }, 'release')
    return reply._tag === 'Accepted'
    ? { _tag: 'SeatsReleased' } satisfies Internal
    : yield* Effect.die('the inventory did not accept the release of its own hold')
    },
    )

    export const booking = program({
    state: State,
    flags: Flags,
    inbound: Inbound,
    internal: Internal,
    operation: Operation,
    sender: Sender,
    projection: Projection,
    })({
    name: 'Booking',

    keys: ['hold', 'deadline', 'confirm', 'release'],

    // The deadline counts from the booking, not from the hold: a slow inventory shortens the customer's wait.
    init: ({ customer, event, seats }, at) => ({
    _tag: 'Holding',
    booking: { customer, event, seats, deadline: DateTime.add(at, holdWindow) },
    cancelled: false,
    }),

    update: (message, { from }) => (state) =>
    Match.value(message).pipe(
    Match.withReturnType<State | Rejected>(),
    Match.tagsExhaustive({
    ConfirmRequested: () =>
    state._tag === 'Held' && isCustomer(from, state.booking)
    ? { _tag: 'Confirming', booking: state.booking }
    : reject,

    CancelRequested: () =>
    !isCustomer(from, state.booking)
    ? reject
    : state._tag === 'Holding' && !state.cancelled
    ? { ...state, cancelled: true }
    : state._tag === 'Held'
    ? { _tag: 'Releasing', booking: state.booking, reason: 'ByCustomer' }
    : reject,

    // The deadline may have passed meanwhile: its timer comes with `Held`, and fires at once.
    SeatsHeld: () =>
    state._tag !== 'Holding'
    ? defect('seats were held while nothing was holding them')
    : state.cancelled
    ? { _tag: 'Releasing', booking: state.booking, reason: 'ByCustomer' }
    : { _tag: 'Held', booking: state.booking },

    SeatsUnavailable: ({ reason }) =>
    state._tag === 'Holding'
    ? { _tag: 'Cancelled', booking: state.booking, reason: state.cancelled ? 'ByCustomer' : reason }
    : defect('seats were unavailable while nothing was holding them'),

    SeatsConfirmed: () =>
    state._tag === 'Confirming'
    ? { _tag: 'Confirmed', booking: state.booking }
    : defect('seats were confirmed while nothing was confirming them'),

    SeatsReleased: () =>
    state._tag === 'Releasing'
    ? { _tag: 'Cancelled', booking: state.booking, reason: state.reason }
    : defect('seats were released while nothing was releasing them'),

    DeadlinePassed: () =>
    state._tag === 'Held'
    ? { _tag: 'Releasing', booking: state.booking, reason: 'Expired' }
    : defect('the deadline passed while no seats were held'),
    }),
    ),

    work: (state) =>
    Match.valueTags(state, {
    Holding: ({ booking }) => ({ hold: Operation.cases.Hold.make({ event: booking.event, seats: booking.seats }) }),
    Held: ({ booking }) => ({ deadline: timer(booking.deadline, { _tag: 'DeadlinePassed' }) }),
    Confirming: ({ booking }) => ({ confirm: Operation.cases.Confirm.make({ event: booking.event }) }),
    Releasing: ({ booking }) => ({ release: Operation.cases.Release.make({ event: booking.event }) }),
    Confirmed: () => noWork,
    Cancelled: () => noWork,
    }),

    operations: {
    Hold: operation.atLeastOnce(hold),
    Confirm: operation.atLeastOnce(confirm),
    Release: operation.atLeastOnce(release),
    },

    project: (state) => ({
    status: state._tag,
    seats: state.booking.seats,
    deadline: DateTime.toEpochMillis(state.booking.deadline),
    canConfirm: state._tag === 'Held',
    canCancel: (state._tag === 'Holding' && !state.cancelled) || state._tag === 'Held',
    }),
    })
    /**
    * The seats of an event. A booking holds seats, then confirms or releases them. The organizer closes the sales:
    * the holds already made may still be confirmed, since each one was a promise.
    */
    import * as Match from 'effect/Match'
    import * as Schema from 'effect/Schema'
    import { type Origin, program, reference, reject, type Rejected, rejectWith } from '@fgaudo/effect-tea-persistent/program'
    import type { booking } from './booking.ts'

    // The booking sends to the inventory, so each imports only the other's types (decision 0183).
    const Booking = reference<typeof booking>('Booking')

    const State = Schema.Struct({
    open: Schema.Boolean,
    capacity: Schema.Int,
    // Seats by booking id.
    held: Schema.Record(Schema.String, Schema.Int),
    sold: Schema.Record(Schema.String, Schema.Int),
    })
    type State = typeof State.Type

    const Flags = Schema.Struct({ capacity: Schema.Int })
    type Flags = typeof Flags.Type

    // A booking sends them as itself, so they name no booking.
    const Inbound = Schema.Union([
    Schema.TaggedStruct('HoldRequested', { seats: Schema.Int }),
    Schema.TaggedStruct('ConfirmRequested', {}),
    Schema.TaggedStruct('ReleaseRequested', {}),
    Schema.TaggedStruct('CloseRequested', {}),
    ])
    type Inbound = typeof Inbound.Type

    // What a booking can tell its customer. The other rejections of a hold are the booking's own mistakes.
    const Rejection = Schema.Literals(['SoldOut', 'SalesClosed'])
    type Rejection = typeof Rejection.Type

    /** Bookings are not senders: they arrive as entities, from their operations. */
    const Sender = Schema.TaggedStruct('Organizer', {})
    export type Sender = typeof Sender.Type

    const Projection = Schema.Struct({ open: Schema.Boolean, available: Schema.Int, canClose: Schema.Boolean })
    export type Projection = typeof Projection.Type

    const total = (seats: Readonly<Record<string, number>>) => Object.values(seats).reduce((a, b) => a + b, 0)

    const available = (state: State) => state.capacity - total(state.held) - total(state.sold)

    // The return type is written. The booking's types are inferred from the inventory's, so a type inferred here from
    // `Booking` would make the inventory's type depend on itself.
    const bookingOf = (from: Origin<Sender>): string | null =>
    from._tag === 'Entity' && from.entity.name === Booking.name ? from.entity.id : null

    const isOrganizer = (from: Origin<Sender>) => from._tag === 'Caller' && from.sender._tag === 'Organizer'

    // The inventory holds no work of its own: it declares no internal message, operation or key.
    export const inventory = program({
    state: State,
    flags: Flags,
    inbound: Inbound,
    rejection: Rejection,
    sender: Sender,
    projection: Projection,
    })({
    name: 'Inventory',

    init: ({ capacity }) => ({ open: true, capacity, held: {}, sold: {} }),

    update: (message, { from }) => (state) => {
    const booking = bookingOf(from)
    return Match.value(message).pipe(
    Match.withReturnType<State | Rejected<Rejection>>(),
    Match.tagsExhaustive({
    // A booking holds once: a second hold would be a second booking under the same id.
    HoldRequested: ({ seats }) =>
    booking === null || booking in state.held || booking in state.sold || seats <= 0
    ? reject
    : !state.open
    ? rejectWith('SalesClosed')
    : seats > available(state)
    ? rejectWith('SoldOut')
    : { ...state, held: { ...state.held, [booking]: seats } },

    ConfirmRequested: () => {
    if (booking === null) return reject
    const { [booking]: seats, ...held } = state.held
    return seats === undefined ? reject : { ...state, held, sold: { ...state.sold, [booking]: seats } }
    },

    ReleaseRequested: () => {
    if (booking === null) return reject
    const { [booking]: seats, ...held } = state.held
    return seats === undefined ? reject : { ...state, held }
    },

    CloseRequested: () => state.open && isOrganizer(from) ? { ...state, open: false } : reject,
    }),
    )
    },

    project: (state) => ({
    open: state.open,
    available: available(state),
    canClose: state.open,
    }),
    })
    /**
    * The booking's properties, for generated states.
    */
    import { describe, expect, test } from 'bun:test'
    import * as DateTime from 'effect/DateTime'
    import fc from 'fast-check'
    import { projectionViolations } from '@fgaudo/effect-tea-persistent/testing'
    import { booking, type Sender, type State } from './booking.ts'

    const at = DateTime.makeUnsafe('2026-10-30T10:00:00Z')

    const time = fc.date({ min: new Date('2026-01-01'), max: new Date('2027-01-01'), noInvalidDate: true })
    .map(DateTime.fromDateUnsafe)

    const aBooking = fc.record({
    customer: fc.constantFrom('ada', 'bob'),
    event: fc.constantFrom('concert-1', 'concert-2'),
    seats: fc.integer({ min: 1, max: 10 }),
    deadline: time,
    })

    // Every case of the booking's states, with any data.
    const state: fc.Arbitrary<State> = fc.oneof(
    fc.record({ _tag: fc.constant('Holding'), booking: aBooking, cancelled: fc.boolean() }),
    fc.record({ _tag: fc.constant('Held'), booking: aBooking }),
    fc.record({ _tag: fc.constant('Confirming'), booking: aBooking }),
    fc.record({ _tag: fc.constant('Releasing'), booking: aBooking, reason: fc.constantFrom('ByCustomer', 'Expired') }),
    fc.record({ _tag: fc.constant('Confirmed'), booking: aBooking }),
    fc.record({
    _tag: fc.constant('Cancelled'),
    booking: aBooking,
    reason: fc.constantFrom('ByCustomer', 'Expired', 'SoldOut', 'SalesClosed', 'NoSuchEvent'),
    }),
    )

    describe('booking', () => {
    test('the projection allows what update accepts, all of it', () => {
    const check = projectionViolations(booking, {
    ConfirmRequested: (projection) => projection.canConfirm,
    CancelRequested: (projection) => projection.canCancel,
    })
    fc.assert(fc.property(state, (state) => {
    const customer: Sender = { _tag: 'Customer', id: state.booking.customer }
    expect(check(state, [
    { message: { _tag: 'ConfirmRequested' }, sender: customer },
    { message: { _tag: 'CancelRequested' }, sender: customer },
    ], at)).toEqual([])
    }))
    })
    })
    /**
    * The booking and its inventory on the runtime, in one database: two entities that talk through operations
    * (decision 0009), where the scenarios of `scenarios.test.ts` rehearse the booking alone and answer for the
    * inventory. It is also how a program is run: a client, a runtime that creates its tables, and a handle per program.
    * What only a probe or values frozen through show is the library's, under `two entities that talk` in
    * `src/runtime/callers.test.ts`.
    */
    import { SqliteClient } from '@effect/sql-sqlite-bun'
    import { expect, test } from 'bun:test'
    import * as Effect from 'effect/Effect'
    import type { SqlClient } from 'effect/sql'
    import { create, launch, operation, send } from '@fgaudo/effect-tea-persistent'
    import { booking } from './booking.ts'
    import { inventory } from './inventory.ts'

    /** A limit no reading of this test reaches: a reading says how many rows it takes (decision 0141). */
    const all = 100

    const organizer = { _tag: 'Organizer' } as const
    const ann = { _tag: 'Customer', id: 'ann' } as const

    const database = <A, E>(test: Effect.Effect<A, E, SqlClient.SqlClient>) =>
    Effect.runPromise(
    Effect.scoped(test).pipe(
    Effect.provide(SqliteClient.layer({ filename: ':memory:' })),
    Effect.orDie,
    ),
    )

    test('a booking holds its seats in the inventory, and confirms them', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    // Neither program needs a service of its own: the booking talks to the inventory, which the runtime routes.
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(booking)

    expect(yield* inventories.create('gig', { flags: { capacity: 10 }, requestId: 'c-gig', sender: organizer }))
    .toEqual({ _tag: 'Created' })
    expect(
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann }),
    ).toEqual({ _tag: 'Created' })

    // The booking's hold is an operation: it sends to the inventory and waits for its reply.
    yield* runtime.settled
    expect(yield* bookings.read('b1')).toMatchObject({ status: 'Held', canConfirm: true })
    expect(yield* inventories.read('gig')).toMatchObject({ open: true, available: 7 })

    expect(yield* bookings.send('b1', { _tag: 'ConfirmRequested' }, { requestId: 'r1', sender: ann, keep: 'forever' }))
    .toEqual({ _tag: 'Accepted' })
    yield* runtime.settled
    expect(yield* bookings.read('b1')).toMatchObject({ status: 'Confirmed', canConfirm: false })
    // The seats are sold, not held: the booking's confirmation moved them.
    expect(yield* inventories.read('gig')).toMatchObject({ available: 7 })

    // What other systems learn: the booking's whole story, step by step (decision 0038).
    expect(
    (yield* bookings.published('projection', [], all)).map(({ kind, message, value }) =>
    [kind, message?._tag ?? null, value.status]
    ),
    ).toEqual([
    ['Creation', null, 'Holding'],
    ['Internal', 'SeatsHeld', 'Held'],
    ['Inbound', 'ConfirmRequested', 'Confirming'],
    ['Internal', 'SeatsConfirmed', 'Confirmed'],
    ])
    // Of the inventory, only the hold. Confirming the seats moves them from held to sold, which leaves
    // `available` where it was: a fact no caller can read is not published, and an inventory whose sales must
    // be is one that shows them.
    expect(
    (yield* inventories.published('projection', [], all)).map(({ kind, message, value }) =>
    [kind, message?._tag ?? null, value.available]
    ),
    ).toEqual([['Creation', null, 10], ['Inbound', 'HoldRequested', 7]])
    }))))

    test('a booking of more seats than are left is cancelled with the inventory’s reason', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(booking)

    yield* inventories.create('small', { flags: { capacity: 2 }, requestId: 'c-small', sender: organizer })
    yield* bookings.create('b2', { flags: { customer: 'ann', event: 'small', seats: 5 }, requestId: 'c-b2', sender: ann })
    yield* runtime.settled

    expect(yield* bookings.read('b2')).toMatchObject({ status: 'Cancelled', canConfirm: false })
    expect(yield* inventories.read('small')).toMatchObject({ available: 2 })
    }))))

    test('a booking for an event that does not exist is cancelled', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(booking)

    yield* bookings.create('b3', { flags: { customer: 'ann', event: 'nowhere', seats: 1 }, requestId: 'c-b3', sender: ann })
    yield* runtime.settled

    expect(yield* bookings.read('b3')).toMatchObject({ status: 'Cancelled' })
    }))))

    test('a booking whose hold sends a message the inventory cannot encode is suspended, not sent again', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    // Half a seat is no `Int`: the message is the booking's defect, and reaches no inventory (decision 0040).
    const halving = {
    ...booking,
    operations: {
    ...booking.operations,
    Hold: operation.atLeastOnce((op: { event: string }) =>
    Effect.flatMap(
    send(inventory, op.event, { _tag: 'HoldRequested', seats: 0.5 }, 'hold'),
    () => Effect.succeed({ _tag: 'SeatsHeld' } as const),
    )
    ),
    },
    } as typeof booking
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(halving)
    yield* inventories.create('gig', { flags: { capacity: 10 }, requestId: 'c-gig', sender: organizer })
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann })
    yield* runtime.settled
    expect(yield* bookings.send('b1', { _tag: 'ConfirmRequested' }, { requestId: 'r1', sender: ann, keep: 'forever' }))
    .toEqual({ _tag: 'Suspended' })
    expect(yield* inventories.read('gig')).toMatchObject({ available: 10 })
    }))))

    test('an operation that sends two requests to one inventory has both applied, each under its name', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    // A hold taken back at once: two messages to the same entity from one run, which their names keep apart
    // (decision 0047).
    const undoing = {
    ...booking,
    operations: {
    ...booking.operations,
    Hold: operation.atLeastOnce((op: { event: string; seats: number }) =>
    Effect.flatMap(
    send(inventory, op.event, { _tag: 'HoldRequested', seats: op.seats }, 'hold'),
    () =>
    Effect.as(
    send(inventory, op.event, { _tag: 'ReleaseRequested' }, 'release'),
    { _tag: 'SeatsHeld' } as const,
    ),
    )
    ),
    },
    } as typeof booking
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(undoing)
    yield* inventories.create('gig', { flags: { capacity: 10 }, requestId: 'c-gig', sender: organizer })
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann })
    yield* runtime.settled
    expect(yield* inventories.read('gig')).toMatchObject({ available: 10 })
    }))))

    test('an operation that sends twice under one name to one inventory is suspended', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    // The second message would be answered the first one's reply, and never applied: it is the booking's defect
    // (decision 0047).
    const twice = {
    ...booking,
    operations: {
    ...booking.operations,
    Hold: operation.atLeastOnce((op: { event: string; seats: number }) =>
    Effect.flatMap(
    send(inventory, op.event, { _tag: 'HoldRequested', seats: op.seats }, 'hold'),
    () =>
    Effect.as(
    send(inventory, op.event, { _tag: 'ReleaseRequested' }, 'hold'),
    { _tag: 'SeatsHeld' } as const,
    ),
    )
    ),
    },
    } as typeof booking
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(twice)
    yield* inventories.create('gig', { flags: { capacity: 10 }, requestId: 'c-gig', sender: organizer })
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann })
    yield* runtime.settled
    expect(yield* bookings.send('b1', { _tag: 'ConfirmRequested' }, { requestId: 'r1', sender: ann, keep: 'forever' }))
    .toEqual({ _tag: 'Suspended' })
    // The release reached no inventory.
    expect(yield* inventories.read('gig')).toMatchObject({ available: 7 })
    }))))

    // A booking that opens its event's inventory itself: an operation creates an entity under a name, and a creation
    // and a message are requests apart, so the same name serves both (decisions 0050, 0060).
    const opening = (creations: number) => ({
    ...booking,
    operations: {
    ...booking.operations,
    Hold: operation.atLeastOnce((op: { event: string; seats: number }) =>
    Effect.gen(function*() {
    for (let made = 0; made < creations; made++) yield* create(inventory, op.event, 'hold', { capacity: 10 })
    yield* send(inventory, op.event, { _tag: 'HoldRequested', seats: op.seats }, 'hold')
    return { _tag: 'SeatsHeld' } as const
    })
    ),
    },
    } as typeof booking)

    test('an operation creates an entity, and sends it a message under the same name', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(opening(1))
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann })
    yield* runtime.settled
    expect(yield* inventories.read('gig')).toMatchObject({ available: 7 })
    }))))

    test('an operation that creates twice under one name is suspended', () =>
    database(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    const inventories = yield* runtime.deploy(inventory)
    const bookings = yield* runtime.deploy(opening(2))
    yield* bookings.create('b1', { flags: { customer: 'ann', event: 'gig', seats: 3 }, requestId: 'c-b1', sender: ann })
    yield* runtime.settled
    expect(yield* bookings.send('b1', { _tag: 'ConfirmRequested' }, { requestId: 'r1', sender: ann, keep: 'forever' }))
    .toEqual({ _tag: 'Suspended' })
    // The first creation reached the inventory, and the hold did not.
    expect(yield* inventories.read('gig')).toMatchObject({ available: 10 })
    }))))
    /**
    * The booking's and the inventory's scenarios, rehearsed.
    *
    * `rehearse` runs one entity. In the booking's scenarios the test answers for the inventory, and in the
    * inventory's it sends as the bookings would. `rehearse.together` runs both, and the booking's runners with them.
    */
    import { describe, expect, test } from 'bun:test'
    import * as Cause from 'effect/Cause'
    import * as DateTime from 'effect/DateTime'
    import * as Effect from 'effect/Effect'
    import * as Exit from 'effect/Exit'
    import type { Address } from '@fgaudo/effect-tea-persistent'
    import { accepted, rehearse, rejected } from '@fgaudo/effect-tea-persistent/testing'
    import { booking, type Sender } from './booking.ts'
    import { inventory } from './inventory.ts'

    const bookedAt = DateTime.makeUnsafe('2026-10-30T10:00:00Z')

    const start = { flags: { customer: 'ada', event: 'concert-1', seats: 2 }, at: bookedAt }

    const ada: Sender = { _tag: 'Customer', id: 'ada' }

    describe('booking', () => {
    test('a booking of no seats is never created', async () => {
    const exit = await Effect.runPromiseExit(
    rehearse(booking, { ...start, flags: { ...start.flags, seats: 0 } }, () => Effect.void),
    )
    expect(exit._tag).toBe('Failure')
    })

    test('held seats are confirmed', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.answer('hold', { _tag: 'SeatsHeld' })
    expect(yield* rehearsal.send({ _tag: 'ConfirmRequested' }, { sender: ada })).toEqual(accepted)
    yield* rehearsal.answer('confirm', { _tag: 'SeatsConfirmed' })
    expect((yield* rehearsal.state)._tag).toBe('Confirmed')
    }),
    )))

    test('a booking the inventory rejects is cancelled', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.answer('hold', { _tag: 'SeatsUnavailable', reason: 'SoldOut' })
    expect(yield* rehearsal.projection).toMatchObject({ status: 'Cancelled', canConfirm: false })
    expect(yield* rehearsal.state).toMatchObject({ reason: 'SoldOut' })
    }),
    )))

    test('a cancellation during the hold releases the seats once held', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    expect(yield* rehearsal.send({ _tag: 'CancelRequested' }, { sender: ada })).toEqual(accepted)
    expect((yield* rehearsal.projection).canCancel).toBe(false)
    yield* rehearsal.answer('hold', { _tag: 'SeatsHeld' })
    yield* rehearsal.answer('release', { _tag: 'SeatsReleased' })
    expect(yield* rehearsal.state).toMatchObject({ _tag: 'Cancelled', reason: 'ByCustomer' })
    }),
    )))

    test('seats held after the deadline are released', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.answer('hold', { _tag: 'SeatsHeld' })
    // With no clock, firing the timer at once stands for a deadline already past when the seats were held.
    yield* rehearsal.fire('deadline')
    expect(yield* rehearsal.send({ _tag: 'ConfirmRequested' }, { sender: ada })).toMatchObject(rejected)
    yield* rehearsal.answer('release', { _tag: 'SeatsReleased' })
    expect(yield* rehearsal.state).toMatchObject({ _tag: 'Cancelled', reason: 'Expired' })
    }),
    )))

    test('a hold under way when the process dies sends the same request again', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    const before = yield* rehearsal.pending('hold')
    yield* rehearsal.restart
    // The idempotency key is the request id: the inventory answers from its stored reply.
    expect((yield* rehearsal.pending('hold')).idempotencyKey).toBe(before.idempotencyKey)
    }),
    )))

    test('only the customer confirms', () =>
    Effect.runPromise(rehearse(
    booking,
    start,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.answer('hold', { _tag: 'SeatsHeld' })
    expect(yield* rehearsal.send({ _tag: 'ConfirmRequested' }, { sender: { _tag: 'Customer', id: 'eve' } }))
    .toMatchObject(rejected)
    }),
    )))
    })

    const event = { flags: { capacity: 4 }, at: bookedAt }

    const first: Address = { name: 'Booking', id: 'booking-1' }

    const second: Address = { name: 'Booking', id: 'booking-2' }

    describe('inventory', () => {
    test('holds never exceed the capacity', () =>
    Effect.runPromise(rehearse(
    inventory,
    event,
    Effect.fnUntraced(function*(rehearsal) {
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 3 }, first)).toEqual(accepted)
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 2 }, second))
    .toEqual({ _tag: 'Rejected', reason: 'SoldOut' })
    expect(yield* rehearsal.sendFrom({ _tag: 'ReleaseRequested' }, first)).toEqual(accepted)
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 2 }, second)).toEqual(accepted)
    expect((yield* rehearsal.projection).available).toBe(2)
    }),
    )))

    test('a closed sale takes no hold, and confirms those it made', () =>
    Effect.runPromise(rehearse(
    inventory,
    event,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 2 }, first)
    expect(yield* rehearsal.send({ _tag: 'CloseRequested' }, { sender: { _tag: 'Organizer' } })).toEqual(accepted)
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 1 }, second))
    .toEqual({ _tag: 'Rejected', reason: 'SalesClosed' })
    expect(yield* rehearsal.sendFrom({ _tag: 'ConfirmRequested' }, first)).toEqual(accepted)
    }),
    )))

    test('a booking acts only on its own hold', () =>
    Effect.runPromise(rehearse(
    inventory,
    event,
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 2 }, first)
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 1 }, first)).toMatchObject(rejected)
    expect(yield* rehearsal.sendFrom({ _tag: 'ReleaseRequested' }, second)).toMatchObject(rejected)
    expect(yield* rehearsal.sendFrom({ _tag: 'HoldRequested', seats: 1 }, { name: 'Order', id: 'booking-2' }))
    .toMatchObject(rejected)
    }),
    )))
    })

    const reasonOf = (exit: Exit.Exit<unknown>) => (Exit.isFailure(exit) ? Cause.squash(exit.cause) : null)

    describe('booking and inventory together', () => {
    test('held seats are confirmed, and sold', () =>
    Effect.runPromise(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    const seats = yield* world.create(inventory, 'concert-1', { capacity: 4 })
    const ada1 = yield* world.create(booking, 'booking-1', start.flags)
    expect(yield* ada1.run('hold')).toBe('Delivered')
    expect((yield* seats.projection).available).toBe(2)
    expect(yield* ada1.send({ _tag: 'ConfirmRequested' }, { sender: ada })).toEqual(accepted)
    yield* ada1.run('confirm')
    expect((yield* ada1.state)._tag).toBe('Confirmed')
    expect((yield* seats.state).sold).toEqual({ 'booking-1': 2 })
    }),
    )))

    test('a booking the inventory cannot seat is cancelled, and holds nothing', () =>
    Effect.runPromise(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    const seats = yield* world.create(inventory, 'concert-1', { capacity: 1 })
    const ada1 = yield* world.create(booking, 'booking-1', start.flags)
    yield* ada1.run('hold')
    expect(yield* ada1.state).toMatchObject({ _tag: 'Cancelled', reason: 'SoldOut' })
    expect((yield* seats.projection).available).toBe(1)
    }),
    )))

    test('a booking of an event with no inventory is cancelled', () =>
    Effect.runPromise(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    const ada1 = yield* world.create(booking, 'booking-1', start.flags)
    yield* ada1.run('hold')
    expect(yield* ada1.state).toMatchObject({ _tag: 'Cancelled', reason: 'NoSuchEvent' })
    }),
    )))

    test('a cancellation during the hold gives the seats back to the inventory', () =>
    Effect.runPromise(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    const seats = yield* world.create(inventory, 'concert-1', { capacity: 4 })
    const ada1 = yield* world.create(booking, 'booking-1', start.flags)
    yield* ada1.send({ _tag: 'CancelRequested' }, { sender: ada })
    yield* ada1.run('hold')
    expect((yield* seats.projection).available).toBe(2)
    yield* ada1.run('release')
    expect(yield* ada1.state).toMatchObject({ _tag: 'Cancelled', reason: 'ByCustomer' })
    expect((yield* seats.projection).available).toBe(4)
    }),
    )))

    // The inventory takes one hold per booking: a hold sent again were refused, had it no stored reply.
    test('a hold the process died on is sent again, and given its stored reply', () =>
    Effect.runPromise(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    const seats = yield* world.create(inventory, 'concert-1', { capacity: 4 })
    const ada1 = yield* world.create(booking, 'booking-1', start.flags)
    yield* ada1.run('hold', { crash: true })
    expect((yield* ada1.state)._tag).toBe('Holding')
    expect(yield* ada1.run('hold')).toBe('Delivered')
    expect((yield* ada1.state)._tag).toBe('Held')
    expect((yield* seats.projection).available).toBe(2)
    }),
    )))

    // A test that answers for the inventory answers `SeatsConfirmed`, and passes: only the inventory rejects it.
    test('a booking that confirms seats it could not hold fails, where the inventory rejects the confirmation', async () => {
    const hopeful: typeof booking = {
    ...booking,
    update: (message, meta) => (state) =>
    message._tag === 'SeatsUnavailable' && state._tag === 'Holding'
    ? { _tag: 'Confirming', booking: state.booking }
    : booking.update(message, meta)(state),
    }
    const exit = await Effect.runPromiseExit(rehearse.together(
    bookedAt,
    Effect.fnUntraced(function*(world) {
    yield* world.create(inventory, 'concert-1', { capacity: 1 })
    const ada1 = yield* world.create(hopeful, 'booking-1', start.flags)
    yield* ada1.run('hold')
    yield* ada1.run('confirm')
    }),
    ))
    expect(reasonOf(exit)).toBe('the inventory did not accept the confirmation of its own hold')
    })
    })