Skip to content

Gate view-state broadcasts on the view-tree handshake#32

Merged
shmuelhizmi merged 2 commits into
masterfrom
fix-view-sync-handshake-race
Jul 13, 2026
Merged

Gate view-state broadcasts on the view-tree handshake#32
shmuelhizmi merged 2 commits into
masterfrom
fix-view-sync-handshake-race

Conversation

@0rierel

@0rierel 0rierel commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Problem

The wmux TUI crashes during dev-server startup with:

TypeError: undefined is not an object (evaluating 'props.onSelectCategory.mutate')
    at WmuxApp (wmux-client-terminal/src/components/WmuxApp.tsx:68:32)

Root cause is a connect-time race in the echoform view-sync protocol (singleInstance mode):

  1. The server broadcasts update_view prop diffs to every connected client (reconcileExistingSharedViewbroadcast), including clients that have not yet completed the request_views_tree handshake and therefore have no base state.
  2. The client's applyViewUpdate upserts an unknown view uid from the diff's create array alone. A diff carrying only a changed data prop (e.g. categories) materializes a view with none of its callback/event props.
  3. ViewsRenderer builds component props strictly from the view entry, so the component renders with its callback props missing and crashes on the first .mutate access.

The window is "client connected → snapshot response received". wmux hits it reliably at dev-server startup: the sidebar's categories prop embeds live per-process status, so the root WmuxApp view broadcasts diffs continuously while processes flip idle → running — exactly while the TUI is connecting and the server's event loop is busy spawning services.

Fix

Track handshake completion per client and withhold view-state events (update_view, delete_view) from clients that have not yet requested the view tree:

  • A client is marked synced when its request_views_tree is handled (snapshot sent in the same tick, so no interleaving).
  • broadcast skips unsynced clients for view-state events only; stream events and the per-client server.emit path (non-singleInstance mode, where mount-time full-prop creates deliver the initial state) are unchanged.

This is lossless: everything an unsynced client doesn't receive is superseded by its snapshot, and views created after the handshake still arrive as full-prop creates on the same ordered socket. The client-side unknown-uid upsert keeps working as the legitimate new-view delivery path.

Verification

  • New protocol-level test packages/echoform/tests/view-sync-handshake.test.ts (same harness style as stream-replay.test.ts):
    • a synced client receives prop diffs while a connected-but-unsynced client receives none — asserted deterministically via the synced client as a positive control, no timing sleeps;
    • after the late handshake the same client receives the snapshot and subsequent diffs;
    • the snapshot carries full props including type: "event" callback entries (the props whose absence crashed wmux).
  • Without the broadcast gate, the first test fails by receiving exactly the bug's payload: an update_view for an unknown uid whose create contains only the label data prop.
  • bun test tests/ (5 pass), bun run typecheck, bun run build, bun run lint (0 errors; the 2 touched files lint clean in isolation) all green locally.
  • All three demo Playwright suites pass locally on this branch (todo-app 6, file-editor 5, dev-server 4) — the dev-server demo exercises real client connects against a live wmux server with the gate in place.

Also: repair the CI browser install

Every CI run has failed since March at browserType.launch: Executable doesn't exist … chromium_headless_shell-1208. bunx playwright install resolves the registry's latest playwright (no root bin exists with isolated installs) and installs a browser revision that doesn't match the lockfile's @playwright/test. The install step now runs the workspace-pinned binary (demo/todo-app/node_modules/.bin/playwright), so this PR's CI actually runs the demo suites again. Happy to split this into its own PR if preferred.

0rierel added 2 commits July 12, 2026 19:53
A client that connected but had not yet requested the view tree could receive an incremental update_view prop diff before its snapshot. The client materializes unknown view uids from the diff alone, producing a view with only the changed props — no callback props — which crashes the client renderer on the first missing-prop access (seen as 'undefined is not an object (evaluating props.onSelectCategory.mutate)' in the wmux TUI during dev-server startup, where category status churn broadcasts diffs continuously while the TUI connects). Track per-client handshake completion and withhold update_view/delete_view from clients that have not requested the tree; the snapshot supersedes everything they missed, and views created afterwards still arrive as full-prop creates.
bunx playwright resolved the registry's latest playwright (no root bin exists with isolated installs), installing a chromium_headless_shell revision that does not match @playwright/test from the lockfile. Every demo suite has failed at browserType.launch since March because of this. Run the install through demo/todo-app's local binary instead; all demos pin the same @playwright/test version.
@shmuelhizmi shmuelhizmi merged commit 67e385d into master Jul 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants