Architecture

Overview

pgwidgets follows a client-server architecture. Python is the server; the browser is the client. Widget constructors and method calls in Python are translated to JSON messages and sent over WebSocket to the browser, where the pgwidgets JavaScript library executes them. User interactions (clicks, input, etc.) travel back as callback messages.

Python (server)                     Browser (client)
+-----------------+                 +------------------+
| Application     |   WebSocket    | pgwidgets JS     |
|   Session  <----|----JSON------->|   widget tree     |
|     widgets     |                |   DOM rendering   |
+-----------------+                +------------------+
       |
       | HTTP (static files)
       v
JS/CSS assets served to browser

Servers

The Application class starts two servers:

HTTP server (default port 9501)

Serves the pgwidgets JavaScript/CSS assets and a connector HTML page. When a browser hits /, it gets remote.html with the WebSocket URL injected. Set http_server=False if you serve the static files from your own web server (Flask, FastAPI, nginx, etc.).

WebSocket server (default port 9500)

Carries the JSON command protocol. Each browser tab opens one WebSocket connection, which becomes one Session.

JSON Protocol

All messages are JSON objects with a type field.

Python -> Browser:

  • {"type": "init", "id": 0} – handshake initiation.

  • {"type": "session-info", "session_id": 1, "token": "..."} – session credentials for reconnection.

  • {"type": "create", "wid": 1, "class": "Button", "args": ["Click"]} – create a widget.

  • {"type": "call", "wid": 1, "method": "set_text", "args": ["New"]} – call a method on a widget.

  • {"type": "call", ..., "silent": true} – call a method without triggering callbacks (used for cross-browser sync).

  • {"type": "listen", "wid": 1, "action": "activated"} – subscribe to a callback.

  • {"type": "unlisten", "wid": 1, "action": "activated"} – unsubscribe.

  • {"type": "reconstruct-start", "next_wid": N} – begin UI reconstruction.

  • {"type": "reconstruct-end"} – end UI reconstruction.

Browser -> Python:

  • {"type": "ack", "session_id": 1, "token": "..."} – handshake acknowledgment (includes session credentials when reconnecting).

  • {"type": "result", "id": 1, "value": ...} – method return value.

  • {"type": "error", "id": 1, "error": "..."} – method error.

  • {"type": "callback", "wid": 1, "action": "activated", "args": [...]} – user interaction.

  • {"type": "file-chunk", ...} – chunked file data (see Callback System).

Session Model

Each browser connection gets a Session object. Sessions persist independently of browser connections – they survive page refreshes, network drops, and tab closes. Python is the source of truth for all widget state.

A session owns:

  • A widget tree with full state tracking (text, colors, sizes, children, etc.)

  • A callback registry ("wid:action" -> handler function)

  • A list of active browser connections

  • A security token for reconnection

  • A message-ID counter for request/response matching

Multiple sessions can be active concurrently (controlled by max_sessions).

Lifecycle:

  1. Browser opens the URL and loads remote.html.

  2. JavaScript connects to the WebSocket server.

  3. Python sends init; browser acknowledges with session info (if reconnecting).

  4. For a new connection: on_connect fires with a new Session.

  5. For a reconnection: the existing session’s UI is automatically reconstructed in the browser.

  6. User code creates widgets, registers callbacks.

  7. When a browser disconnects, on_disconnect fires. The session remains alive for reconnection.

  8. Sessions are only destroyed when session.destroy() is called explicitly.

Reconnection and Reconstruction

When a browser reconnects to an existing session (e.g. after a page refresh), the framework walks the widget tree and replays every widget’s creation, state, children, and callbacks. The browser receives the full UI as if it were being built for the first time.

The reconstruction process:

  1. Clean up stale auto-wrapped widget references from the previous connection.

  2. Send reconstruct-start so the browser suppresses callback echo.

  3. For each widget (parents before children):

    1. Create the widget with its original constructor arguments.

    2. Replay item lists (e.g. ComboBox items).

    3. Replay state changes (text, colors, size, position, etc.).

    4. Attach to parent via the same child method used originally.

    5. Replay factory calls (menu actions, toolbar actions, separators).

    6. Re-register all callbacks.

    7. Re-register auto-sync listeners.

  4. Replay deferred state (splitter sizes, tab/stack index, tree collapse state).

  5. Replay show/hide state.

  6. Send reconstruct-end.

Multi-Browser Synchronization

Multiple browsers can connect to the same session simultaneously. When one browser triggers a state change (slider move, tab switch, tree expand, etc.), the change is pushed to all other connected browsers in real time.

Browser A                Python (session)              Browser B
+---------+              +---------------+              +---------+
| slider  |---callback-->| update state  |---push------>| slider  |
| moved   |              | in _state     |  (silent)    | updated |
+---------+              +---------------+              +---------+

The push uses a silent flag so the receiving browser suppresses callback echo, preventing infinite feedback loops.

State changes that are synchronized include:

  • Widget state: slider values, checkbox state, text content, tab index, etc.

  • Layout: move and resize of windows and MDI subwindows.

  • Tree/table: expand, collapse, and sort operations.

  • Child management: closing MDI subwindows or tab pages.

Headless Sessions

Sessions can be created without a browser using app.create_session(). The widget tree can be built up before any browser connects. When a browser navigates to the session URL, the pre-built UI is reconstructed automatically.

session = app.create_session()
Widgets = session.get_widgets()
top = Widgets.TopLevel(title="Pre-built UI")
top.show()
# ... build the full UI ...
# When a browser connects with this session's ID and token,
# the UI appears immediately.

Widget References

Widgets are identified by integer IDs (wid). When a Python widget is passed as an argument to another widget’s method (e.g., vbox.add_widget(btn, 0)), the framework automatically converts the Python Widget object to a {"__wid__": N} reference on the wire, and converts it back on return values.