This site is driven by two keys, like the machine it came from. Reading: j and k scroll, d and u move half a page, gg goes to the top, G to the bottom, H and L switch windows, ? opens the help. Space is the Neovim leader and handles content: Space then h home, r research, p projects, g gear, a about, / tags, or Space then a digit to jump to that window. Home is the tmux prefix and handles windows: Home then c opens a terminal, Home then & closes a window, Home then space goes to the next one. While focus is on the keyboard, h j k l move between keys and Enter opens one.

ResearchR AboutA Gear — G for gear — keyboard, terminal, editor, homelab.G
    ProjectsP Tags — / as in vim: search./
    ×
    Menu

    fate-studio: Seeing and Driving Statecharts in the Browser

    A viewer and live simulator for fate statecharts — send events, fire timers, resolve invocations, and watch the active state change over Server-Sent Events. One static binary, no outbound requests at all.

    If fate is the engine, fate-studio is where you watch it run. Open a machine and its diagram appears; then step through it — send events, fire delayed transitions, resolve or reject invocations — and the active state updates live on screen.

    Why it’s a separate repo

    This is the question it gets most, and the answer is one sentence: the engine has no dependencies, and the studio needs a web server.

    In one repo, go get github.com/arisros/fate would drag net/http and the whole studio into the program of someone who only wanted a state machine. Splitting them means people pay only for what they use.

    It embeds into your own program

    The studio is an ordinary http.Handler, so it isn’t only a demo server — it can be mounted inside a running application:

    srv := studio.NewServer("my app")
    srv.Register(studio.Entry{
        Name:    "checkout",
        Summary: "the checkout flow",
        Build:   checkoutMachine().Describe,
        BuildLive: func() studio.LiveInstance {
            return studio.NewLiveActor(checkoutMachine(), dispatch, checkoutMachine().Describe)
        },
    })
    http.Handle("/studio/", http.StripPrefix("/studio", srv.Handler()))

    The key is the LiveInstance interface. fate is generic — Actor[Ctx, Evt] — while the simulator has to drive any machine without knowing its concrete types. LiveInstance erases them, and the dispatch function you supply maps an event name from the UI onto one of your typed events. A machine registered with only Build still shows its diagram, just without the simulator.

    Canvas and transport

    The diagram is drawn with React Flow (@xyflow/react) and laid out by elkjs, so node positions never have to be authored by hand. State changes travel from server to browser over Server-Sent Events rather than WebSockets — the data really does flow one way, and user events go back as ordinary requests. SSE reconnects on its own and needs no special handling in a proxy.

    Snapshots can be inspected, diffed, imported and exported, and a timeline records every step — which turns out to be the practical way to retell the exact sequence that produced a strange state.

    One binary, no outbound calls

    The built UI (Vite output) is committed into assets/ and embedded in the Go binary. What gets deployed is a single file: no Node, no CDN, no external font request. That matters because this thing is most likely to be mounted inside an internal network with no egress at all.

    go run ./cmd/fate-studio    # then open http://localhost:8090

    The address is set via FATE_STUDIO_ADDR. The demo server ships a handful of example machines — a traffic light, a parallel media player, a build pipeline, a deep-history editor, a live-context counter, and timeout / fetch machines for trying the timer and invocation controls.

    id en
    rss gh in