How it works?

Basic

---
config:
  look: handDrawn
---
graph LR
    UI -- "value update" --> State
    State -- "get value" --> PageFunc["Page Func"]
    UI -- "rerun" --> PageFunc
    PageFunc -- "notify update" --> UI

The key concept is that in the Page Function, the UI component interact immediately with the running logic.

For example:

if tgcomp.Button(p.Main, "Click me") {
    tgcomp.Text(p.Main, "Hi")
}

In the first call (For example: When the web page is entering.), The Click me button will render, but the Hi will not render. Since the button is not clicked in the first round.

When the user clicks the button, the Page Function will be called again. In this round, tgcomp.Button will return true and Hi will render.

How?

Since we declare a button whose id is Click me, and when the button is clicked, we store true with key Click me into p.State.

When entering tgcomp.Button, it will check if there is any data stored in p.State with key Click me.

Server-Client Architecture

Server-client architecture

Server-Client need to handle a more complex part: multiple state for multiple users. Hence we need a state_id for each state.

The executor owns the transport and the state pool — how long a state lives in it is on Session Cache. Everything from the event onwards is the Session's, and it is the same on the desktop.

packs is what travels back: a ready pack when a run starts, one notify pack per component change, and a result pack when the run ends.

File upload is the one thing off this path. The client POSTs to /api/files with its state_id and the percent-encoded component_id of the fileupload (a header can't carry a non-ASCII label), and the handler streams the body to a file of the state's own, so it touches neither the socket nor the Session. The page reads that file back through FileUpload, which means an upload only has to fit on disk, not in memory.

Session

The piece that turns an event into a page run is tgframe.Session. It holds a page name, a State, and one function to send packs to the client:

func NewSession(app *App, pageName string, state *State, send SendPackFunc) (*Session, error)

That is all it needs, so it does not know what the transport is. An executor only feeds it events:

session.HandleRawEvent(bs)

A Session is safe for concurrent use and serializes its runs: a new event interrupts the page func still running from the previous one, so the client never receives two runs interleaved. The interrupt travels as a cancelled Params.Context: a page func watching it returns on its own, and one that does not panics with ErrUpdateInterrupt at the next thing it draws, which the session recovers.

This is why the web executor and the desktop executor share the whole app logic. They differ only in how packs and events travel: a websocket and a state_id pool on the web, bound methods on a single window on the desktop.