effect-tea-persistent
    Preparing search index...

    Quick start

    A seat held for a quarter of an hour, confirmed in time or released when its time runs out. It is the smallest program that does something: a state, an inbound message that update rejects where it does not apply, a timer that work derives from the state, and a projection. Its first test rehearses the seat without a database or a clock, and fires the timer instead of waiting for it. Its second test launches the program on SQLite, and sends a request twice under one request id, which is applied once. The README shows both files whole.

    import * as DateTime from 'effect/DateTime'
    import * as Schema from 'effect/Schema'
    import { noWork, program, reject, timer } from '@fgaudo/effect-tea-persistent/program'

    // A seat held for a quarter of an hour: confirmed in time, or released when the time runs out.
    const State = Schema.Union([
    Schema.TaggedStruct('Held', { until: Schema.DateTimeUtcFromMillis }),
    Schema.TaggedStruct('Confirmed', {}),
    Schema.TaggedStruct('Released', {}),
    ])

    export const hold = program({
    state: State,
    flags: Schema.Null,
    inbound: Schema.TaggedStruct('Confirm', {}),
    internal: Schema.TaggedStruct('Expired', {}),
    projection: Schema.Struct({ status: Schema.Literals(['Held', 'Confirmed', 'Released']) }),
    })({
    name: 'Hold',
    keys: ['expiry'],
    init: (_, at) => ({ _tag: 'Held', until: DateTime.add(at, { minutes: 15 }) }),
    // `Expired` arrives only while the state holds its timer, so only while the seat is held.
    update: (message) => (state) =>
    message._tag === 'Expired' ? { _tag: 'Released' } : state._tag === 'Held' ? { _tag: 'Confirmed' } : reject,
    // The timer is data, derived from the state: a confirmed seat holds none, and nothing has to cancel it.
    work: (state) => (state._tag === 'Held' ? { expiry: timer(state.until, { _tag: 'Expired' }) } : noWork),
    project: (state) => ({ status: state._tag }),
    })
    import { SqliteClient } from '@effect/sql-sqlite-bun'
    import { expect, test } from 'bun:test'
    import * as DateTime from 'effect/DateTime'
    import * as Effect from 'effect/Effect'
    import { launch } from '@fgaudo/effect-tea-persistent'
    import { rehearse, rejected } from '@fgaudo/effect-tea-persistent/testing'
    import { hold } from './hold.ts'

    // Rehearsed, without a database or a clock: the timer fires because the test fires it.
    test('a seat nobody confirms in time is released', () =>
    Effect.runPromise(rehearse(
    hold,
    { at: DateTime.makeUnsafe('2026-11-08T10:00:00Z') },
    Effect.fnUntraced(function*(rehearsal) {
    yield* rehearsal.fire('expiry')
    expect(yield* rehearsal.send({ _tag: 'Confirm' })).toMatchObject(rejected)
    expect(yield* rehearsal.projection).toEqual({ status: 'Released' })
    }),
    )))

    // Launched on SQLite: the state is stored, and a retry of a request is answered as the first time.
    test('a seat confirmed in time is confirmed once', () =>
    Effect.runPromise(Effect.scoped(Effect.gen(function*() {
    const runtime = yield* launch()
    const holds = yield* runtime.deploy(hold)
    yield* holds.create('seat-12', { requestId: 'hold-1' })
    // The reply is kept a day, so a retry within it is answered as the first time.
    const confirm = { requestId: 'confirm-1', keep: { millis: 86_400_000 } }
    expect(yield* holds.send('seat-12', { _tag: 'Confirm' }, confirm)).toEqual({ _tag: 'Accepted' })
    expect(yield* holds.send('seat-12', { _tag: 'Confirm' }, confirm)).toEqual({ _tag: 'Accepted' })
    expect(yield* holds.read('seat-12')).toEqual({ status: 'Confirmed' })
    })).pipe(Effect.provide(SqliteClient.layer({ filename: ':memory:' })))))