Client API Context -- Hook based Graphistry embed#193
Draft
Client API Context -- Hook based Graphistry embed#193
Conversation
Empty React library skeleton (stubs only) and a Vite+React-18 smoke-test app. Implementation lands in follow-ups — see graphistry/ai_code_notes/architecture/external_bridge.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the package name with the existing client-api / client-api-react naming convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds createRpcClient(iframe) — a Promise-returning thin wrapper over the
Protocol v2 correlation-id envelope. Exposes three primitives matching
the iframe-side dispatcher: call(path, args), get(...paths), set(json).
Rejects with GraphistryRpcError carrying a typed kind ('invalid_op' |
'internal' | 'timeout' | 'disposed'), so consumers can distinguish
failure modes without parsing message strings.
Bumps CLIENT_SUBSCRIPTION_API_VERSION to 2 to declare support for the
new envelope on the handshake.
No rxjs in the RPC path — a junior dev wires up addFilter() without
learning Observables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…primitives Adds the protocol-level bits that both the React wrapper and non-React consumers need: - ExternalStore<T>: shallow-eq with explicit arrayKeys, refcount hooks on 0→1 and 1→0 so callers can drive subscribe/unsubscribe messages lazily. Bound subscribe/getSnapshot so React's useSyncExternalStore can adopt it without wrapping. - SubscriptionManager: registers (path → store + projection), refcounts listeners, emits graphistry-subscribe/unsubscribe, routes graphistry-sub-update + graphistry-sub-error to the right store. - snapshots.js: SelectionSnapshot / LabelsSnapshot / FiltersSnapshot shapes + projection fns that normalise falcor pseudo-arrays and swap local row indices for stable globalIndex. Projected once at the update boundary so consumers never see $refs. - GraphistryPermissionError: typed error for subscribe paths gated by server feature flags (esp. flag_unsafe_jsapi_export_row). - Path constants PATH_SELECTION_LABELS / PATH_LABELS / PATH_FILTERS so host and iframe agree on wire strings. No React dependency in any of these modules — the React bits live in @graphistry/client-api-context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin React layer over @graphistry/client-api primitives: - GraphistryProvider: owns SubscriptionManager + RpcClient + init handshake + controlled-domain set. Build scene URL from host/dataset props with a `src` escape hatch. - GraphistryScene: renders the iframe, registers it with the Provider on mount, tears down on unmount. useGraphistryScene() exposed for hand-rolled iframes. - useSelection / useLabels / useFilters: thin useSyncExternalStore wrappers that surface typed snapshots with ready/error flags. Users never see useSyncExternalStore, falcor, or rxjs. - useGraphistry(): imperative handle with addFilter / resetFilters / setSelectionExternal convenience methods; `rpc` escape hatch for raw call/get/set against the whitelisted model. - GraphistryControlledError: thrown when a hook mutator is called on a domain controlled by a Provider prop (filters, exclusions, …). Same one-way-at-a-time rule as the existing <Graphistry> component. Example app exercises all four hooks, catches the three error classes, and is parameterizable by ?host=…&dataset=… query string. Drops rxjs from the dep list — nothing in this package uses it directly anymore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client side of the Protocol v2 generic subscribe.
client-api:
- SubscriptionManager.register accepts optional pathSets; when set, the
graphistry-subscribe postMessage includes them and the iframe routes
to its generic model.get-based handler.
- FRAGMENT_FILTERS: canonical pathSet for the filters domain
(workbooks.open.views.current.filters[0..30].{id,query,enabled,name,
dataType,level} + length).
- projectFilters updated for the post-walk shape — iframe walks the
static prefix, so the projection receives the filters pseudo-array
directly, not the full jsonGraph root.
client-api-context:
- Provider registers the filters store with FRAGMENT_FILTERS. No
viz-side container needed — useFilters() just works once both
branches are deployed.
- Ambient types updated with FalcorPathSet + FRAGMENT_FILTERS.
v1 paths (selection.labels, labels) continue to register without
pathSets; iframe falls through to the hand-enriched handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrades the smoke-test app: - React 19, @vitejs/plugin-react 6, Vite 8. - Tailwind 4 via @tailwindcss/vite — CSS-first config (no config file, no PostCSS step). Dark theme defined in src/index.css via @theme. - vite-plugin-console-forward streams browser console.* and unhandled errors to the dev-server stdout, so server-side coding agents see runtime behavior without browser devtools. - App.tsx rewritten with Tailwind: iframe fills the left pane, 420px sidebar on the right with Selection + Filters cards, status dots, input + buttons styled with focus rings, error pills. Monospace for technical values. No code changes in the hooks or provider — this is pure example-app dressing + upstream bumps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GraphistryScene's ref callback depended on the full ctx object, which
gets a new reference every time Provider's value memo recomputes (i.e.,
every setRpc). In React 19 that triggered the ref-reattach cycle:
setRef regenerates → React detaches old ref → calls old(null) →
registerIframe(null) → setRpc(null, dispose) → re-render → loop until
"Maximum update depth exceeded". Pulling the stable pieces
(registerIframe, sceneSrc) out of ctx makes setRef's identity stable.
Also splits the module so Fast Refresh works when editing context.tsx:
- internal.ts: Ctx object + shared types (ControlledDomain,
GraphistryHandle, InternalContext).
- context.tsx: GraphistryProvider + GraphistryScene components only.
- hooks.ts: useGraphistry, useGraphistryScene, useSelection, useLabels,
useFilters — all in one hooks file.
React Fast Refresh bails on mixed component + non-component exports,
forcing full reloads on every tsx edit. With this split, tsx edits
hot-patch without tearing down the iframe + handshake state.
example/App.tsx: default HOST to `${location.hostname}:8491` so the
app works out-of-box against Alex's dev Graphistry instead of the
public hub (which runs pre-Protocol-v2 viz code). Override with
?host=… query param.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redesigns the smoke-test app so our React components float as glass-morphism cards on top of a full-bleed Graphistry iframe, instead of a side-by-side split. Graphistry's own chrome is disabled via URL params passed through Provider.params (menu=false, info=false — see apps/core/viz/src/containers/view.js), combined with play=5000 to auto-start layout and skip the splash. Tailwind 4 @theme tokens: dashboard.{dark,surface,header,border} for chrome + brand.{teal,purple,green} for accents. Selection point/edge counts colored purple/teal to match the palette. Default dataset bumped to the arrow-format one that's seeded on Alex's dev Graphistry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
play=5000 runs the force-layout for 5 seconds; it doesn't actually control the splash screen. splashAfter=false is the real kill-switch (apps/core/viz/server/splash.js:13 reads it from the query string). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operational notes for picking up the client-api-context prototype: getting started (vite dev + package aliases), gotchas that cost time (React 19 ref callback identity, Fast Refresh export splits, vite- plugin-console-forward peer deps, Tailscale HMR, chrome-disable URL params, hub vs dev graphistry), Protocol v2 quick reference, key file map, current state, next goals (connection debug + style polish). Architecture-level context stays in graphistry/ai_code_notes/ architecture/; this doc is agent-targeted operational layer only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubscriptionManager fixes that surfaced once the example ran against a fresh-loading iframe: - init-replay: subscribes posted before the iframe's LocalDataSink listener was constructed silently dropped. On first init from the iframe, replay all acquired paths once so cold-load subscribe works. - Per-path JSON dedupe in _onMessage: iframe's v1 push pipeline fires on every hover/animation tick with fresh object identities. Shallow eq treats these as updates and re-renders React each time. Dedupe at the boundary kills the selection-list flash. - register() accepts an options object, sent alongside subscribe messages. First use: withColumns:true for .selection.labels so v1 hand-enriched selections carry full column data for single-row inspector. - refresh(path) method: cycles unsubscribe/subscribe and clears the serialize cache so post-mutation re-primes bypass dedupe. - graphistry-iframe-log type relay: parent receives iframe-side diagnostic logs and forwards to host console, letting an agent owning the Vite terminal see inside-iframe behavior without devtools. rpc.js gains request/response tracing with bounded-length preview so call/get/set round-trips are visible in the same stream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose more of the Protocol v2 surface as declarative / imperative React
APIs so the example doesn't have to hand-roll falcor paths.
- <GraphistryProvider bg={hex}>: applied after the init handshake via
rpc.set against scene.renderer.background.color (the live path —
scene.bg.color is a $ref to the whole background subtree, writing
there does not propagate). Gated on subscriptionAPIVersion !== null
so we don't post before the iframe's listener exists.
- removeFilter(id) on the handle and useFilters().remove(id): calls the
new filters.remove route exposed in ClientAPIRoutes.
- refreshAfter(promise, path): all mutating RPCs (addFilter,
removeFilter, resetFilters, setSelectionExternal) now cycle the
relevant subscription so panels reflect the post-mutation state —
the iframe-side falcor-update emitter doesn't automatically observe
RPC-AppRouter mutations.
- Selection registers with { withColumns: true } option so single-row
inspector gets the full column map, not just title/globalIndex.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ollapsible filters
Second round of UX after end-to-end data started flowing:
- Drops top-left logo, top-right connection badge, host/dataset footer
per user feedback — cleaner overlay over the viz.
- Chrome flags: showInspector=false, showHistograms=false hide the
iframe's built-in right-side panels so only React overlays are
visible.
- Filters:
- AST-aware pill rendering. query arrives as {ast, error}; walk the
common node types (LimitExpression, BinaryPredicate, Literal,
Identifier, MemberExpression, NotExpression, FunctionCall) into
readable text. Unknown shapes fall back to JSON.
- Click "×" to remove — wired to useFilters().remove(id).
- Collapsible. Pinned to the bottom of the right rail as a strip
labeled "Filters · N active"; expands on click.
- Selection inspector:
- Hidden entirely when !ready or nothing selected.
- Single-selection shows a glyph (filled dot for point, arrow for
edge) + the row's title/label in the title bar; no static
"Selection" heading.
- Columns normalized from the pseudo-array {0:{key,value,dataType},…}
shape and rendered with smart inline/stack: short key+value
stays on one row right-aligned, long values drop to their own
line. No dataType badge per feedback.
- ScrollFade wrapper with mask-image gradient at top/bottom of long
scroll regions; Card exposes bodyPad={false} so scroll meets card
edges while padding lives inside the scroll content.
- Right rail outer wrapper is pointer-events-none; each card opts in
via [&>*]:pointer-events-auto so empty gaps pass clicks through to
the iframe.
- GraphistryProvider bg prop drives scene background to match the
dashboard dark palette.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vite-plugin-console-forward API changed options between the two versions we tried. Standardize on the current shape: - captureErrors / captureRejections explicit (was unhandledErrors) - prefix: 'browser' keeps forwarded lines grepable in vite stdout Dev dep pin adjusted to match what npm resolved locally, plus lock files in sibling projects brought up to date from npm install runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hand-off note for the deploy agent. Iframe's webpack-dev-server HMR client was trying wss://exrhizome.tailc68bf0.ts.net:3000/sockjs-node which isn't exposed via Tailscale — every viz-source edit required a hard refresh. Ask: allocate a new proxied port (proposed :8495), set WDS_SOCKET_HOST + WDS_SOCKET_PORT on the streamgl-viz service, and confirm the wss upgrade lands on reload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts reusable UI primitives out of App.tsx and moves the theme into CSS custom properties driven by Tailwind 4's @theme. The example's job becomes declarative composition: pick hooks, drop them into Panels, compose the layout — no more one-off Tailwind classes sprinkled through business logic. src/ui.tsx (new) — - Panel (with optional collapsible header, titleNode, bodyPad) - ScrollArea (mask-image fade top/bottom — no extra DOM) - Button (primary / ghost / iconGhost) - Input (Enter-submit) - KeyValueRow (smart inline ↔ stacked based on combined length, row striping via altIndex) - StatusDot, PointGlyph, EdgeGlyph, ErrorBox, Chip src/index.css — - `ink / surface / elevated / hover / border / border-strong` — desaturated midnight-purple surface stack. `ink` carries a touch more purple so the viz bg blends with the panels. - `accent / point / edge` — teal + purple at ~70% saturation. Semantic ok / warn / err pulled off their neon defaults. - `text / text-dim / text-faint` — three off-white steps with a warm purple cast. src/App.tsx — - Dropped from ~440 LOC to ~280. Every panel is now a Panel + rows from the primitives; no hand-written borders / shadows / backdrops / radii outside ui.tsx. - Radii: rounded-xl/2xl → rounded / rounded-sm across the board. - SCENE_BG_HEX tracks the `ink` token so the RPC-set viz bg matches the dashboard surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The deploy ask already landed (webpack-dev-server reachable via :8495 Tailscale). Docs fit better outside this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
node:16.13.0-slim pinned to Debian buster, which hit EOL in Jun 2024. http://deb.debian.org/debian buster Release now 404s, breaking the apt-get update in the base stage and failing the Storybook workflow on every build triggered by projects/client-api/** changes. Switch to node:16-bullseye-slim — still node 16 (matches the lock files across projects), but on Debian 11 bullseye which is supported until mid-2026. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Falcor Bridge hooks package (`@graphistry/client-api-context`):
- useColumns / usePalettes / useEncoding / columnsForEncoding, plus
ColumnMeta / Palette / EncodingKind types.
- readJsonGraphLeaf now follows `$ref` chains (depth-bounded), so every
view-scoped leaf resolves through `workbooks.open → workbooksById[X]`
and `views.current → viewsById[X]` without bailing at the first ref.
- `refreshAfter` for filters now primes the cache via rpc.get before
re-subscribing so a newly-added filter's expression data lands before
the push, instead of leaving an unresolved-ref ghost row.
- projectFilters drops phantom entries that lack id / query / name so
intermediate cache pushes can't wipe the filter list to empty.
Example app:
- App.tsx collapses to 23 lines — Provider + Scene + SettingsDrawer +
SelectionInspector + FilterBar, each self-positioning.
- Domain logic (polling, attribute bare-name, palette colors, atom
envelope) moved out of the example and into the context package.
- UI split into panels/ (Arrange, Layout, Encodings, Appearance,
SelectionInspector, FilterBar, SettingsDrawer) and components/
(PaletteSelect with inline swatch preview + PaletteSwatch +
ChevronGlyph). Config lifted to src/config.ts.
- Arrange button is full-width in the Drawer toolbar with an animated
conic-gradient border (@Property --arrange-angle) when running.
deploy.md: docs how the agent-driven debug loop uses Vite stdout +
ScheduleWakeup to iterate without user interaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Warning: Slop Quality level -- for demonstration purposes only
Simple
App.tsxThe entire host integration fits in a provider and two panels:
A panel is data + UI, nothing else:
No subscription wiring. No falcor paths to know. No rxjs.
useFilters()returns a typed snapshot and Promise-returning mutators; everything reactive is handled by the provider.SelectionInspectorfollows the same shape —useSelection()→{ points, edges, pointLabels, edgeLabels, ready }— and the single-node case surfaces the full column map with{ withColumns: true }opted in at the provider registry.What's under the covers
Host side (
projects/client-api/src/)rpc.js— correlation-id RPC client (call/get/set) with typed errors.subscriptions.js—SubscriptionManagerwith init-replay (cold-load subscribes land after iframe's listener exists), per-path JSON dedupe (kills hover-driven re-render flash),optionspass-through,refresh(path)for post-mutation resubscribe, and an iframe-log relay so iframe diagnostics surface in the host console.externalStore.js— shallow-eq + refcount store.snapshots.js— projections + canonical fragment pathSets.React side (
projects/client-api-context/src/)GraphistryProvider— handshake, rpc lifecycle, optionalbg/filters/exclusionsprops, controlled-vs-uncontrolled domain flags.GraphistryScene— the iframe element; just a stable ref-callback into the provider.useGraphistry,useGraphistryScene,useSelection,useLabels,useFilters. All typed.Pairs with