@fgaudo/effect-tea - v0.7.2
    Preparing search index...

    Commands and subscriptions

    The duration becomes a timer: once its minutes are set, it starts, may pause and resume, and finishes. It ticks every second while it runs, and rings a bell when the time is up. Its code is examples/timer/timer.ts, and the rules of step 1 carry over: "+" and "−" now apply only while the timer is being set.

    Where the timer is, is one value with a case for each phase, holding only what exists in it. Flags such as isRunning and isPaused would allow combinations that cannot be; a union cannot hold them (see state and messages).

    export type Phase = Data.TaggedEnum<{
    Setting: object
    Running: { secondsLeft: number }
    Paused: { secondsLeft: number }
    Finished: object
    }>

    export const Phase = Data.taggedEnum<Phase>()

    /** `minutes` is kept while the timer runs, so that a reset sets it again as it was. */
    export type State = Readonly<{ minutes: number; phase: Phase }>

    export type Message = Data.TaggedEnum<{
    MoreClicked: object
    LessClicked: object
    StartClicked: object
    PauseClicked: object
    ResumeClicked: object
    ResetClicked: object
    Ticked: object
    }>

    export const Message = Data.taggedEnum<Message>()

    update never runs an effect. When the timer finishes, it returns a command, data that says what to do, and the program's runCommand turns it into an Effect. Ringing needs a bell, a service the program declares and whoever runs it provides. Ringing gives update nothing to learn, so its execution answers None; a command that has an answer, such as a save, answers with one message (see commands).

    export type Command = Data.TaggedEnum<{ Ring: object }>

    export const Command = Data.taggedEnum<Command>()

    /** The bell the timer rings. The program knows only this interface: `main.ts` and the tests provide it. */
    export class Bell extends Context.Service<Bell, { readonly ring: Effect.Effect<void> }>()('examples/timer/Bell') {}

    // Ringing only acts on the world: there is no answer for `update`, so the execution answers `None`.
    export const runCommand = (command: Command): Execution<Message, Bell> => {
    switch (command._tag) {
    case 'Ring':
    return Effect.flatMap(Effect.service(Bell), (bell) => bell.ring).pipe(Effect.as(Opt.none()))
    }
    }

    An execution cannot fail: its type says so. A command that can fail turns its failure into a message, as examples/form does with a save.

    Each message applies in some phases and is rejected in the others. A tick is different: the timer listens to the clock only while it runs (below), so a tick in any other phase cannot arrive, and its arrival is a bug. update throws defect(reason) on it, and the run dies instead of hiding it (see rejections and defects).

    export const update: Update<State, Message, Command> = (message) => (state) => {
    const { minutes, phase } = state
    switch (message._tag) {
    case 'MoreClicked':
    return phase._tag === 'Setting' ? { state: { ...state, minutes: minutes + 1 } } : reject
    case 'LessClicked':
    return phase._tag === 'Setting' && minutes > 1 ? { state: { ...state, minutes: minutes - 1 } } : reject
    case 'StartClicked':
    return phase._tag === 'Setting'
    ? { state: { ...state, phase: Phase.Running({ secondsLeft: minutes * 60 }) } }
    : reject
    case 'PauseClicked':
    return phase._tag === 'Running'
    ? { state: { ...state, phase: Phase.Paused({ secondsLeft: phase.secondsLeft }) } }
    : reject
    case 'ResumeClicked':
    return phase._tag === 'Paused'
    ? { state: { ...state, phase: Phase.Running({ secondsLeft: phase.secondsLeft }) } }
    : reject
    case 'ResetClicked':
    return phase._tag === 'Setting' ? reject : { state: { ...state, phase: Phase.Setting() } }
    // Ticks come only while running: pausing stops the subscription, and the run drops what it sent. A tick in
    // any other phase is a bug, and the run dies with it.
    case 'Ticked':
    if (phase._tag !== 'Running') return defect('A tick while the timer was not running')
    return phase.secondsLeft > 1
    ? { state: { ...state, phase: Phase.Running({ secondsLeft: phase.secondsLeft - 1 }) } }
    : { state: { ...state, phase: Phase.Finished() }, commands: [Command.Ring()] }
    }
    }

    The last tick moves the timer to Finished and returns the command with the new state, in one step.

    A subscription is what the program listens to. subscriptions(state) returns them in a HashMap, by key, and after every change of state the run compares the keys: a new key starts its subscription, a kept key leaves it running, and a key that went away stops it, and drops what it sent that update has not seen (see subscriptions).

    The timer has the key 'ticks' only while it runs. Pausing removes it, so the clock stops; resuming brings it back, and a new clock starts. The clock is an Effect Channel that sends Ticked every second and never ends:

    const ticks: Subscription<Message, never> = Channel.forever(
    Channel.fromEffectDrain(Effect.sleep('1 second')).pipe(
    Channel.concatWith(() => Channel.succeed(Message.Ticked())),
    ),
    )

    The model has a case for each phase the screen tells apart, each with the messages of its controls:

    export type Model = Data.TaggedEnum<{
    Setting: { minutes: number; more: Message; less: Opt.Option<Message>; start: Message }
    Running: { secondsLeft: number; pause: Message; reset: Message }
    Paused: { secondsLeft: number; resume: Message; reset: Message }
    Finished: { reset: Message }
    }>

    const makeModel = ({ minutes, phase }: State): Model => {
    const reset = Message.ResetClicked()
    switch (phase._tag) {
    case 'Setting':
    return {
    _tag: 'Setting',
    minutes,
    more: Message.MoreClicked(),
    less: minutes > 1 ? Opt.some(Message.LessClicked()) : Opt.none(),
    start: Message.StartClicked(),
    }
    case 'Running':
    return { _tag: 'Running', secondsLeft: phase.secondsLeft, pause: Message.PauseClicked(), reset }
    case 'Paused':
    return { _tag: 'Paused', secondsLeft: phase.secondsLeft, resume: Message.ResumeClicked(), reset }
    case 'Finished':
    return { _tag: 'Finished', reset }
    }
    }

    The program now declares its commands and its services, and has runCommand and subscriptions:

    export const timer: Program<{
    state: State
    model: Model
    message: Message
    command: Command
    services: Bell
    }> = {
    init: { state: { minutes: 25, phase: Phase.Setting() } },
    update,
    runCommand,
    subscriptions: (state) => state.phase._tag === 'Running' ? HashMap.make(['ticks', ticks]) : HashMap.empty(),
    makeModel,
    }

    Its view, timerView in examples/timer/view.ts, draws each case as the duration's view did, and formats the seconds left as m:ss.

    Next, many timers.