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

    Children

    The application shows a list of timers: "Add a timer" adds one, and "Remove" takes one away. Each timer is the program of step 2, unchanged: any program can be a child as it is. Its code is examples/timer/timers.ts.

    The parent keeps each timer's state with its id, and the next id to give. Ids are never reused, so that a removed timer's id never comes back to another one (see children by id). Adding and removing are the parent's own messages:

    /** Ids are numbers, never reused, so they compare by value and a removed timer's id never comes back. */
    export type Entry = Readonly<{ id: number; timer: Timer.State }>

    export type State = Readonly<{ nextId: number; entries: ReadonlyArray<Entry> }>

    type AddClicked = Readonly<{ _tag: 'AddClicked' }>

    type RemoveClicked = Readonly<{ _tag: 'RemoveClicked'; id: number }>

    parent<{ state }>() gives the functions that declare children. each declares children by id: where each one lives in the parent's state (get and set, by id), and which of them may listen (subscribed). The library does the routing (see composition):

    const { compose, each } = parent<{ state: State }>()

    export const timers = compose({
    init: (children) => {
    const first = children.Timer.init(1)
    return { state: { nextId: 2, entries: [{ id: 1, timer: first.state }] }, commands: first.commands }
    },
    children: {
    Timer: each(Timer.timer, {
    // A removed timer is `None`: a click on a frame drawn before its removal is rejected.
    get: (state, id: number) =>
    Opt.map(Arr.findFirst(state.entries, (entry) => entry.id === id), (entry) => entry.timer),
    set: (state, id, timer) => ({
    ...state,
    entries: Arr.map(state.entries, (entry) => entry.id === id ? { id, timer } : entry),
    }),
    // Every timer may listen: each one's own `subscriptions` say when it ticks.
    subscribed: (state) => Arr.map(state.entries, ({ id, timer }) => [id, timer] as const),
    }),
    },
    update: (message: AddClicked | RemoveClicked, children) => (state) => {
    switch (message._tag) {
    case 'AddClicked': {
    const timer = children.Timer.init(state.nextId)
    return {
    state: { nextId: state.nextId + 1, entries: [...state.entries, { id: state.nextId, timer: timer.state }] },
    commands: timer.commands,
    }
    }
    // A second click on "Remove", from the same frame, finds the timer gone.
    case 'RemoveClicked':
    return Arr.some(state.entries, (entry) => entry.id === message.id)
    ? { state: { ...state, entries: Arr.filter(state.entries, (entry) => entry.id !== message.id) } }
    : reject
    }
    },
    makeModel: (state, children) => ({
    add: { _tag: 'AddClicked' } as AddClicked,
    timers: Arr.map(state.entries, ({ id }) => ({
    id,
    model: children.Timer(state, id),
    remove: { _tag: 'RemoveClicked', id } as RemoveClicked,
    })),
    }),
    })

    export type Model = ModelOf<typeof timers>

    /** The list's messages, made by the library: its own, and `{ _tag: 'Timer', id, message }`. */
    export type Message = MessageOf<typeof timers>

    What the parent gets from the declaration:

    • Starting a child: children.Timer.init(id) is a timer's first step, its state and its commands; the parent's own init is a function of children for this.
    • Its messages: its own, plus { _tag: 'Timer', id, message } for each timer's. MessageOf names them.
    • Its commands: each timer's, as { _tag: 'Timer', id, command }, run by the timer's runCommand.
    • Its subscriptions: each running timer's 'ticks', under ['Timer', id, 'ticks'].
    • Its children's models: children.Timer(state, id) in makeModel, the same object while the timer's state is.

    A timer that is not there is None in get, so a click on a removed timer, from a frame drawn before, is rejected. A parent never reads a child's state to decide: the parent here only adds and removes (see splitting a program).

    Each timer is drawn by timerView with a dispatch of its own, from dispatchTo, which wraps the timer's messages as the parent receives them. It is the same function in every frame, so snabbdom's thunk draws again only the timer whose model changed (see children's views):

    // A timer's model is the same object while its state is, and `dispatchTo` gives it the same dispatch in every
    // frame: `thunk` draws again only the timer that changed.
    export const view: View<Timers.Model, Timers.Message, VNode> = (model, dispatch) =>
    h('main', [
    h('button', { on: onClick(model.add, dispatch) }, 'Add a timer'),
    h(
    'ul',
    model.timers.map(({ id, model, remove }) =>
    h('li', { key: id }, [
    thunk('div.timer', String(id), timerView, [model, dispatchTo(dispatch, 'Timer', id)]),
    h('button.secondary', { on: onClick(remove, dispatch) }, 'Remove'),
    ])
    ),
    ),
    ])

    examples/timer/main.ts runs the list in the page, with a bell that beeps. The services of every command are provided around run:

    // A short beep, made with the Web Audio API.
    const bell = Bell.of({
    ring: Effect.sync(() => {
    const audio = new AudioContext()
    const tone = audio.createOscillator()
    tone.connect(audio.destination)
    tone.start()
    tone.stop(audio.currentTime + 0.3)
    tone.onended = () => void audio.close()
    }),
    })

    BrowserRuntime.runMain(run(timers, renderInto(root, view)).pipe(Effect.provideService(Bell, bell)))

    Next, testing it.