Skip to content

reflex-xy adapter: xy figures as Reflex components#55

Merged
Alek99 merged 5 commits into
mainfrom
claude/reflex-xy-integration-ur81be
Jul 16, 2026
Merged

reflex-xy adapter: xy figures as Reflex components#55
Alek99 merged 5 commits into
mainfrom
claude/reflex-xy-integration-ur81be

Conversation

@masenf

@masenf masenf commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Implements the complete Reflex integration design (docs/design/reflex-integration.md), delivering xy figures as first-class Reflex components with the same performance contract as the notebook path: screen-bounded binary wire, kernel-side canonical data, and stale-while-revalidate interaction.

Summary

The adapter (python/reflex-xy/) multiplexes chart data onto the app's existing websocket as a second socket.io namespace (/_xy), eliminating the need for sidecar HTTP endpoints. Data columns ride as native binary attachments; the only chart state in Reflex is a token string. Figures live in a per-process registry and are rebuilt on reconnect/worker restart from state, making the registry a rebuildable cache (same rule as GPU buffers in §27).

Key changes

  • Frontend assets (XYChart.jsx, xy_client.js): React wrapper that multiplexes the /_xy namespace onto the app's engine.io connection and drives the ChartView client. Handles both live (token-based) and static (pre-compiled) chart sources.

  • Data plane (namespace.py): socket.io namespace implementing the wire protocol—metadata as JSON, data columns as binary attachments. Handles subscription, unsubscription, and message routing with session-affine tokens.

  • Figure registration (registry.py, tokens.py): Per-process registry keyed by deterministic state tokens (xyv1|...) or builder tokens. Registry misses are recovered by re-running the figure builder against Reflex state (no distributed store needed).

  • Computed var integration (vars.py): @reflex_xy.figure decorator for state methods that build charts. Evaluating the var registers the figure and returns a token; Reflex's dependency tracking drives rebuilds and republishes.

  • Component (component.py): Single factory reflex_xy.chart(...) accepting three sources: state var, token string, or Chart object. Compiles to the appropriate tier (live or static).

  • App wiring (app.py): Two entry points—plugin (zero app-code changes) or direct setup(app) call. Registers the namespace and sets up broadcast scheduling.

  • State bridge (state_bridge.py): Rebuilds figures from Reflex state for multi-worker and reconnect scenarios.

  • Static payload tier (payload_asset.py): Compiles Chart objects to first-paint payloads written as asset files (zero-backend charts).

  • Demo app (examples/demo_app/): Full integration showcase—1M-point drillable scatter, hover row readout, box-select cross-filtering, live streaming line.

  • Tests (tests/reflex_adapter/): End-to-end socket data plane tests, registry tests, figure var tests, component compile smoke, asset parity, and async figure builder tests.

  • Smoke test (scripts/reflex_ws_smoke.py): Headless Chromium probe verifying one physical websocket carries both app and data planes, all charts paint from binary payloads, deep zoom drills correctly, and streaming works.

  • Build integration (js/build.mjs): Exports xy_client.js for the adapter's frontend bundle.

Notable implementation details

  • No new HTTP endpoints: All data flows over the app's existing websocket via socket.io multiplexing. Proxy config, auth, and TLS are inherited from the app plane.

  • Binary wire protocol: Data columns are native socket.io attachments (ArrayBuffers in the browser), not JSON numbers or base64—honoring §29.

  • Deterministic tokens: State tokens are computed from the full state path and var name, making them stable across rebuilds and enabling cache invalidation.

  • Thread-safe registry: Guarded by a threading lock; async broadcast fan-out is scheduled onto the event loop captured at setup time.

  • Rebuild-from-state recovery: Registry misses (worker restart, reconnect on another node) are handled by re-running the figure builder, not by shipping canonical columns through Redis.

  • Strict dependency budget: The adapter depends only on reflex core/component packages (not full

https://claude.ai/code/session_01C4FLs5QcA5Eq4DEEVqEQEo

masenf added 4 commits July 16, 2026 00:43
Implements the revised docs/design/reflex-integration.md: the xy
data plane rides the Reflex app's existing engine.io connection as a second
socket.io namespace (/_xy) instead of new HTTP endpoints — binary columns as
native attachments (no JSON numbers, no base64, §29), kernel dispatch via
xy.channel, lifecycle/auth/proxying inherited from the app socket.

python/reflex-xy (new package):
- registry.py: per-process figure registry (tokens in state, figures as
  rebuildable caches, §27), versioning, coalesced fan-out, TTL sweep, append
- vars.py: @reflex_xy.figure computed var — evaluation registers the figure,
  deps auto-track the builder body, token stays stable across rebuilds
- state_bridge.py + tokens.py: deterministic xyv1 tokens; registry misses
  rebuild from Reflex state via state_manager (multi-worker/reconnect story
  without a figure server or chart data in redis)
- namespace.py: sub/unsub/msg protocol, session-affine tokens, fail-closed
  handlers, mount-addressed replies, room broadcasts
- component.py + assets/XYChart.jsx: rx.Component whose React wrapper
  multiplexes onto the shared socket manager and drives the stock ESM render
  client (assets/xy_client.js now emitted by js/build.mjs, drift-tested)
- app.py: setup(app) + XYPlugin (post_compile) one-line wiring
- examples/demo_app: 1M-point drillable scatter, hover rows, box-select
  cross-filter, streaming line

Verification: tests/reflex_adapter (54 tests incl. a real-websocket
integration suite over uvicorn) and scripts/reflex_ws_smoke.py, a headless-
Chromium E2E that asserts one shared backend websocket, painted pixels on
all charts, density-to-points drilldown, the hover pick -> reflex event ->
DOM loop, and append streaming — all passing against the demo app.
…ne() pinned tokens

reflex_xy.chart() now dispatches on its source (design §3.4/§5):

- token (state var / string): live kernel chart on the shared websocket,
  unchanged.
- xy Chart/Figure passed directly: compiled at page build into a
  content-addressed XYBF payload asset (assets/xy/<digest>.xyf, new
  payload_asset.py), served as a static file, and rendered kernel-less via
  the client's renderStandalone path (src prop) — client-side hover,
  pan/zoom, worker density re-bin; no registry, no socket, works under
  reflex export. Page bodies run before the compiler's assets->public copy,
  so the file ships with every compile; writes are idempotent and skipped
  under REFLEX_BACKEND_ONLY (prod workers re-evaluate stateful pages but
  produce no frontend files). The URL is digest-stable across workers and
  recompiles.

reflex_xy.inline(chart) covers fixed data that still wants the kernel:
module-scope registration under a content-addressed xyin- token, so every
worker derives the same token with no state or rebuild hook. Such entries
are pinned: the TTL sweep now spares figures that have no rebuild recipe.

Demo gains a direct-Chart sparkline; the browser E2E now asserts four
mounted charts, ink in the static one, and exactly three sub frames — the
static chart provably never subscribes — still over one backend websocket.
65 adapter tests passing.
Every example, docstring, test, and the demo app now do a plain
'import xy' — no 'import xy as fc'. Mechanical rename within
this branch's files only; the pre-existing fastcharts-era conventions
elsewhere in the repo are untouched.
@reflex_xy.figure now accepts 'async def' builders for charts whose data
comes from a database, HTTP endpoint, or dataframe store. Mirrors reflex's
ComputedVar/AsyncComputedVar breakdown with the same iscoroutinefunction
dispatch rx.var uses: async builders become AsyncFigureVar
(an AsyncComputedVar), evaluated and cached by reflex's normal async-var
machinery — builder runs on first access and on dependency-dirty, never on
cache hits. Dependency tracking still targets the builder body, and the
registry-miss rebuild path awaits the same builder on whatever worker
recovers the figure.

The demo's cross-filter histogram is now an async builder; the browser E2E
passes against it from a cold start (reflex's hydrate machinery awaiting
the var end to end). Also resyncs the adapter's xy_client.js copy with the
post-rebase render client, caught by the drift check. 72 adapter tests.
@masenf masenf changed the title Land reflex-xy adapter: xy figures as Reflex components reflex-xy adapter: xy figures as Reflex components Jul 16, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 94 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing claude/reflex-xy-integration-ur81be (78c5611) with main (4d5a56f)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

Two packaging-hygiene fixes for PR #55:

CI ('Source distribution' job): the root sdist include swept all of
python/, dragging the separately-distributed reflex-xy adapter — and its
reflex.lock frontend pins, which scripts/verify_sdist.py rightly forbids —
into the xy tarball. Narrow the include to python/xy and exclude the
adapter tree + its tests explicitly (hatchling auto-admits readme-named
files, so the exclude is load-bearing). Verified locally with the exact CI
sequence: uv build --sdist + verify_sdist now pass with zero adapter
entries.

Drift elimination: reflex_xy no longer carries a copy of xy_client.js at
all. register() links the render client out of the installed xy
package (xy/static/index.js) into the app's assets tree, repairing
stale links when the install moves — the JS that renders a payload is now
always the build that shipped with the Python that produced it, replacing
drift *detection* (build-emitted copy + parity test) with structural
impossibility. Drops the js/build.mjs third output and the wheel
force-include; the adapter wheel ships only XYChart.jsx.

111 tests (adapter + sdist guards) and the cold-start browser E2E pass.
@Alek99
Alek99 merged commit 59214be into main Jul 16, 2026
30 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