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

    A first program

    The first program sets how many minutes a timer lasts: "+" adds a minute, "−" takes one away, never below one. Its code is examples/timer/duration.ts.

    The state is everything the program knows, and a message is something that happened to it, here a click on either button:

    export type State = Readonly<{ minutes: number }>

    export type Message = 'More' | 'Less'

    The state is always an object, { minutes } rather than a number, so that any program can later become a child as it is (see anatomy of a program).

    update takes a message, then the state, and returns the next state, as { state }. When a message does not apply in the state, it returns reject instead: "−" at one minute.

    export const update: Update<State, Message> = (message) => ({ minutes }) => {
    if (message === 'More') return { state: { minutes: minutes + 1 } }
    if (minutes === 1) return reject
    return { state: { minutes: minutes - 1 } }
    }

    update is pure: it never runs an effect and never changes the state it is given. The run ignores a rejected message and keeps the state as it was, so a click that comes late, from a screen drawn before, is harmless (see rejections and defects).

    The view does not read the state. It draws a model that makeModel makes from it, with the messages its controls send. At one minute the model has no less: the view cannot offer what update rejects.

    export type Model = Readonly<{
    minutes: number
    more: Message
    less: Opt.Option<Message>
    }>

    export const duration: Program<{ state: State; model: Model; message: Message }> = {
    init: { state: { minutes: 25 } },
    update,
    makeModel: ({ minutes }) => ({
    minutes,
    more: 'More',
    less: minutes > 1 ? Opt.some('Less') : Opt.none(),
    }),
    }

    A program is a plain object, and its type names its types: Program<{ state; model; message }>. init is the state it starts from (see model).

    A view is (model, dispatch) => node. It draws the model and sends what the model holds through dispatch; it decides nothing on its own:

    // snabbdom's props module never removes a prop, so both cases of "−" set `disabled`.
    export const durationView: View<Duration.Model, Duration.Message, VNode> = (model, dispatch) =>
    h('main.timer', [
    Opt.match(model.less, {
    onNone: () => h('button', { props: { disabled: true } }, '−'),
    onSome: (less) => h('button', { props: { disabled: false }, on: onClick(less, dispatch) }, '−'),
    }),
    h('output', `${model.minutes} min`),
    h('button', { on: onClick(model.more, dispatch) }, '+'),
    ])

    run(duration, renderInto(root, durationView)) starts the program and hands the renderer a stream of frames, each a model and the dispatch to send its messages with. renderInto draws each frame into the element root. In a browser, the run is started with BrowserRuntime.runMain from @effect/platform-browser, as step 3 does, so that a run that dies is reported (see running).

    Next, the timer itself.