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.
hold.ts: the program.hold.test.ts: its rehearsal, and its launch on SQLite.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:' })))))