--- url: /tesm/docs/examples/app-loading-flow.md description: App loading flow example --- # App loading flow example ## Machine ```ts import { machine, st, XMsg, XModel, XCmd, defineFlow } from "tesm" type InitialContext = { loadingStarted: number; } type WaitingInitialize = InitialContext & { localSave: LocalSave; } type LoadingConfigContext = WaitingInitialize & { initialize_data: Initialize } type LoadedContext = LoadingConfigContext & { config: Config; loadingFinished: number; } const m = machine( { initial: st(), waiting_initialize: st(), waiting_config: st(), loaded: st(), }, { initialized: (data: Initialize) => ({ data }), config_loaded: (config: Config, now: number) => ({ config, now }), local_storage_loaded: (localSave: LocalSave) => ({ localSave }), }, { loadLocalStorage: () => ({}), initialize: () => ({}), loadConfig: (configUrl: string) => ({ configUrl }), } ) const AppLoadingState = defineFlow( m, "AppLoadingState", () => [m.states.initial({ loadingStarted: Date.now() }), m.cmds.loadLocalStorage()], { initial: { local_storage_loaded: (msg, model) => { return [ m.states.waiting_initialize({ ...model, localSave: msg.localSave }), m.cmds.initialize() ]; } }, waiting_initialize: { initialized: (msg, model) => { return [ m.states.waiting_config({ ...model, initialize_data: msg.data }), m.cmds.loadConfig(msg.data.configUrl) ]; } }, waiting_config: { config_loaded: (msg, model) => { return [ m.states.loaded({ ...model, config: msg.config, loadingFinished: msg.now }), ]; } }, loaded: {} } ) export namespace AppLoading { export type Msgs = ReturnType; export type Cmd = XCmd export type Model = XModel export const machine = AppLoadingState } type Initialize = { configUrl: string; username: string; userId: string; // ... other properties } type LocalSave = { // ... properties for local save } type Config = { // ... properties for config } ``` ## React `context.tsx` ```tsx import { createContext, PropsWithChildren, useContext } from "react" import { AppLoading } from "../state" import { useTeaSimple } from "tesm/react" const GlobalStateContext = createContext< { model: AppLoading.Model; msgs: AppLoading.Msgs } | undefined >(undefined) export const GlobalStateProvider: React.FC = (props) => { const [state, msgs] = useTeaSimple(AppLoading.machine, { initialize: async (cmd, msgs) => { await sleep(1000) return msgs.initialized({ configUrl: "https://example.com/config.json", userId: "user123", username: "user", }) }, loadConfig: async (cmd, msgs) => { await sleep(1000) return msgs.config_loaded({}, Date.now()) }, loadLocalStorage: async (cmd, msgs) => { await sleep(1000) return msgs.local_storage_loaded({}) }, }) return ( {props.children} ) } export function useGlobalState() { let ctx = useContext(GlobalStateContext) if (!ctx) throw new Error("useGlobalState must be used within a GlobalStateProvider") return ctx } ``` `AppLoader.tsx` ```tsx import { SpecificState } from "tesm" import { AppLoading } from "../state" import { useGlobalState } from "./context" type LoadedModel = SpecificState const App = (props: LoadedModel) => { return (

Welcome {props.initialize_data.username}

) } const LoadingScreen = (props: { stage: AppLoading.Model["state"] }) => { return
loading stage: {props.stage}
} export const AppLoader = () => { const { model } = useGlobalState() switch (model.state) { case "initial": case "waiting_initialize": case "waiting_config": return case "loaded": return } } ``` --- --- url: /tesm/docs/other/error-handling.md --- # Error Handling ## Type Safety TESM provides strict type checking at compile time. When creating a state machine using `defineFlow()`, TypeScript verifies that: 1. All states from `Model` have corresponding handlers in `flow` 2. All handlers in `flow` correspond to existing states 3. All messages in handlers correspond to defined `Msg` ## Runtime Errors When a message cannot be handled in the current state, TESM will throw an exception. This behavior can be modified using `onInvalidState`. ```ts import { invalidStateMsg } from "tesm" const machine = defineFlow(m, "MachineName", initial, flow, extras, { onInvalidState: (machine, msg, model) => { console.error(`Invalid state transition in ${machine}: ${model.state}.${msg.type}`); }, // or use built-in error message generator onInvalidState: (machine, msg, model) => { console.error(invalidStateMsg(machine, msg, model)); } }); ``` The `onInvalidState` handler receives: * `machine`: State machine name (second argument of defineFlow) * `msg`: Message that cannot be handled * `model`: Current state model ## Universal Message Handlers The `extras` parameter of the `defineFlow()` function allows you to define message handlers that will be called if the current state doesn't have its own handler for that message. ```ts const machine = defineFlow(m, "MachineName", initial, flow, { some_message: (msg, model) => [ m.states.some_state({}), m.cmds.some_cmd() ], }); ``` ## Ignoring Transition In machine there is a helper `ignore` that returns current model, so transition is simply ignored ```ts const machine = defineFlow(m, "MachineName", initial, flow, { other_message: m.ignore, }); ``` --- --- url: /tesm/docs/getting-started.md description: Quick guide to TESM --- # Getting Started The Elm State Machine is a state management library based on [The Elm Architecture](https://guide.elm-lang.org/architecture/). This library makes use of concepts familiar to Elm programmers (such as Model, Msg, Cmd) to manage application state in a totally **pure** way. TESM is written in TypeScript. While vanilla JavaScript is supported, it is highly recommended to use TypeScript for the benefit of strict type checking. ## Installation ::: code-group ```sh [npm] npm install tesm ``` ```sh [yarn] yarn add tesm ``` ::: ## Example In this example we will develop a small state that handles a subset of the application logic. 1. Import the required components: ```ts import { machine, st, XMsg, XModel, XCmd, defineFlow } from "tesm" ``` *** This example state will handle logic of some loading process. We will have **3 states**: `initial`, `loading` and `loaded`. Every **state** has a **context** where all the data is stored. Context is basically a plain JS object that stores fields with data. 2. Let's create **context types** for when the loading process has just started and when the loading process has completed. ```ts type InitialContext = {} type LoadingContext = { loadingStarted: number } type LoadedContext = LoadingContext & { loadingFinished: number } ``` `loadingStarted` is the time when loading has started. `loadingFinished` is the time when loading has finished. *** Now we can create state machine 3. Let's create our states. ```ts const m = machine({ initial: (m: T) => m, loading: (m: T) => m, loaded: (m: T) => m, }) ``` to less boilerplate use an imported **`st()`** function ```ts const m = machine( { initial: st(), loading: st(), loaded: st(), }, ``` We pass an object as `machine` first argument where the field names are state names and values are functions that serve as a strictly typed boilerplate. Next, we need a way to change the underlying state. Let's define *incoming and outgoing messages* that are related to our state. *** Incoming messages are called **`Msg`** in Elm world.\ The only requirement for `Msg` in TESM is to have a `type` field. `Msg` can have extra fields with data. You can think of `Msg` as events that happen in outside world and are passed to TESM state with related data. 4. Let's create a couple of `Msg`. ```ts const m = machine( { initial: st(), loading: st(), loaded: st(), }, { started_loading: (now: number) => ({ now }), finished_loading: (now: number) => ({ now }), }, ``` msg structure is similar to state: we pass as `machine` second argument an object with `Msg` names and generator functions. The convention for `Msg` names is `snake_case` and past tense verbs. These are the events that already happened and our state is being notified of them. `APPLICATION_LOADED`, `http_error_encountered` and `user.loading.failed` are all good names for `Msg`. *** 5. Let's create a couple of `Cmd`. ```ts const m = machine( { initial: st(), loading: st(), loaded: st(), }, { started_loading: (now: number) => ({ now }), finished_loading: (now: number) => ({ now }), }, { startLoadingAnimation: () => ({}), displayPopup: (text: string) => ({ text }), } ``` `Cmd` are almost identical to `Msg`, the only difference being the naming convention: `Cmd` names are `camelCase` and use present tense verbs. You should name `Cmd` the same way you name methods in your code.\ `loadUserInfo(uid: string)`, `todo.create(name: string)` and `cancelLoading` are all good names for `Cmd`. *** 6. Let's extract types from our machine for future use. ```ts export type Msg = XMsg export type Cmd = XCmd export type Model = XModel ``` *** It's time for us to create the core of our logic: the **`update()`** function.\ This function will take care of all incoming messages and will update the state accordingly. The signature of the `update()` function is as follows: ```ts export function update(msg: Msg, model: Model): [Model, ...Cmd[]] ``` If you're familiar with Elm, this function looks almost the same as Elm update function: ```elm update : Msg -> Model -> (Model, Cmd Msg) ``` The differences are that TESM `update()` can return multiple `Cmd` if needed, and `Cmd` are not tied to `Msg` Since we're using a state machine, we need to define state transition logic rather than one update function. transition logic is an object with following syntax: ```ts { : { : (msg, model) => [ , <...cmds> ] } } ``` Let's create our state transition logic by passing the object to the **`defineFlow()`** function as its 4th argument. Try use autocomplete between curly braces and it will suggest the initial state and messages to you. ```ts export const LoadingState = defineFlow( m, // our machine "LoadingState", // machine name for debug () => [m.states.initial({})], // initial state and commands { initial: { started_loading: (msg, model) => [ m.states.loading({ loadingStarted: msg.now }), m.cmds.startLoadingAnimation(), ], }, loading: { finished_loading: (msg, model) => [ m.states.loaded({ loadingStarted: model.loadingStarted, loadingFinished: msg.now, }), m.cmds.displayPopup( `Loading finished in ${msg.now - model.loadingStarted} milliseconds!` ), ], }, loaded: {} }) ``` We're using [pattern matching](https://stackoverflow.com/questions/2502354/what-is-pattern-matching-in-functional-languages) to process incoming messages based on their types and current state type. The `defineFlow` function will throw an error if current state cannot handle the message. *** Let's focus on the return value in this part of the code: ```ts started_loading: (msg, model) => [ m.states.loading({ loadingStarted: msg.now }), m.cmds.startLoadingAnimation(), ], ``` First element of the array is the updated state. States should always be immutable and it's up to you to make sure that none of the fields of the states are ever changed. Use [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) where applicable.\ Based on the state definitions from step 3 of this tutorial. TypeScript checks that all of the required parameters were passed to the state context and ensures type safety Second element of the array (and also third and fourth and so on) is the *side effect* that is produced by this combination of state and `Msg`. In the provided example we instruct the outer world to start loading animation. It is important to note that the state itself *never starts* any of the side-effects: no timers, no HTTP requests, no DOM operations etc.\ Our state only instructs the outer world to run these side effects. The exact way to perform side effects is decided by the outer world.\ In the real application we can handle `startLoadingAnimation` by displaying an animation on the webpage.\ In the tests, however, we can just skip this side effect and proceed with other messages. Let's take a look at the full code of the example before proceeding to the *outer world* implementation of our state. ## Complete code of the example ```ts import { machine, st, XMsg, XModel, XCmd, defineFlow } from "tesm" type InitialContext = {} type LoadingContext = { loadingStarted: number } type LoadedContext = LoadingContext & { loadingFinished: number } const m = machine( { initial: st(), loading: st(), loaded: st(), }, { started_loading: (now: number) => ({ now }), finished_loading: (now: number) => ({ now }), }, { startLoadingAnimation: () => ({}), displayPopup: (text: string) => ({ text }), } ) export type Msg = XMsg export type Cmd = XCmd export type Model = XModel export const LoadingState = defineFlow( m, // our machine "LoadingState", // machine name for debug () => [m.states.initial({})], // initial state and commands { initial: { started_loading: (msg, model) => [ m.states.loading({ loadingStarted: msg.now }), m.cmds.startLoadingAnimation(), ], }, loading: { finished_loading: (msg, model) => [ m.states.loaded({ loadingStarted: model.loadingStarted, loadingFinished: msg.now, }), m.cmds.displayPopup( `Loading finished in ${msg.now - model.loadingStarted} milliseconds!` ), ], }, loaded: {} }) ``` --- --- url: /tesm/docs/side-effects.md description: Quick guide to TESM --- # Side effects So far we've been writing our state in full isolation: we have **states**, **Msg**, **Cmd** and the transition logic inside **defineFlow()**, but it's just a bunch of pure functions and raw data.\ In this section of the tutorial we get to the interesting part: integrating this pure state to the real world application full of side effects and asynchronicity. ## React For react, TESM comes with **`useTeaSimple`** hook 1. Let's import our state and hook ```ts import { LoadingState } from "./state" import { useTeaSimple } from "tesm/react" ``` 2. The hook accepts two arguments: * The state machine definition * An object with side-effect handlers ```tsx function App() { const [model, msgs] = useTeaSimple(LoadingState, { displayPopup: ({ text }, msgs) => { alert(text) }, startLoadingAnimation: (cmd, msgs) => {}, }) return (
) } export default App ``` ## Separate Cmd Handler You can create command handlers separately from the hook using `createHandler()` function. ```ts import { createHandler } from "tesm" import { LoadingState } from "./state" // Create handler separately const cmdHandler = createHandler(LoadingState, { displayPopup: ({ text }, msgs) => { alert(text) }, startLoadingAnimation: (cmd, msgs) => { // Start loading animation logic }, }) function App() { // Pass the pre-created handler to the hook const [model, msgs] = useTeaSimple(LoadingState, cmdHandler) // ... rest of component } ``` Even more flexibility can be achieved with the `createHandlerF()` function. It accepts a function with parameters that can be passed through in the component. ```ts import { createHandlerF } from "tesm" import { LoadingState } from "./state" // Create handler with external "alert" type HandlerParams = { alert: (s: string) => void } const cmdHandlerF = createHandlerF(LoadingState, (params: HandlerParams) => ({ displayPopup: ({ text }, msgs) => { params.alert(text) }, startLoadingAnimation: (cmd, msgs) => { }, })) function App() { // Pass the pre-created handler to the hook const [model, msgs] = useTeaSimple(LoadingState, cmdHandlerF({ alert: window.alert })) // ... rest of component } ``` This approach is useful when: * You want to keep your component code cleaner by extracting side-effect logic * You need to test command handlers separately * You need to compose state machines in a hierarchy, where one machine's command handler can receive another machine's command handler ## Node.js For convenience we can use a **hook** (not to be confused with React Hooks): an object that maintains current state and updates it when new `Msg` arrive.\ TESM comes with a hook called `createHook()`. 1. Import `createHook()` and our state. ```typescript import { createHook } from "tesm" // destructured from `LoadingState` import { msgs, state, update, initial } from "./state" ``` 2. Create an instance of hook by providing it with an `update()` function of our state and with a function that returns initial state. ```typescript let hook = createHook(update)(initial) ``` 3. Add a side effect handler to the current state. ```typescript hook.addHandler((cmd) => { switch (cmd.type) { case "startLoadingAnimation": return console.log(`loading animation started`) case "displayPopup": return console.log(`displaying popup with text "${cmd.text}"`) } }) ``` For the purpose of this example we will just log our side effects into console. In the real world scenario we could update DOM, send HTTP requests, perform other async actions etc. Sending new messages (`Msg`) to the state is simple: ```typescript hook.send(msgs.started_loading(Date.now())) ``` You can construct an object manually if you want to, but it's always easier to use `Msg` constructors. Manual mode: ```typescript hook.send({ type: "started_loading", now: Date.now() }) ``` Let's take a look at the full example: ```typescript import { createHook } from "tesm" import { msgs, state, update, initial } from "./state" let hook = createHook(update)(initial) hook.addHandler((cmd) => { switch (cmd.type) { case "startLoadingAnimation": return console.log(`loading animation started`) case "displayPopup": return console.log(`displaying popup with text "${cmd.text}"`) } }) console.log(hook.getState()) // { state: 'initial' } hook.send(msgs.started_loading(Date.now())) // loading animation started console.log(hook.getState()) // { state: 'loading', loadingStarted: 1582582297994 } hook.send(msgs.finished_loading(Date.now())) // displaying popup with text "Loading finished in 2 milliseconds!" console.log(hook.getState()) // { state: 'loaded', loadingStarted: 1582582297994, loadingFinished: 1582582297996 } ``` --- --- url: /tesm/docs/other/llm.md --- * [llms.txt](/tesm/llms.txt) * [llms-full.txt](/tesm/llms-full.txt)