reflex-xy adapter: xy figures as Reflex components#55
Merged
Conversation
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.
Merging this PR will not alter performance
Comparing Footnotes
|
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/_xynamespace 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.figuredecorator 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 factoryreflex_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 directsetup(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): Exportsxy_client.jsfor 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
reflexcore/component packages (not fullhttps://claude.ai/code/session_01C4FLs5QcA5Eq4DEEVqEQEo