Skip to content

Client State and Cache

VerifiedOwner Frontend / productLast verified frontend@99f49dae6368414896bf2858cb848f5a7c7fcf98

The frontend uses no general-purpose state-management or data-fetching library. package.json's dependency list at this pin has no Redux, Zustand, Jotai, MobX, React Query, or SWR — only React 19, Next.js, CodeMirror (for the Strategy Studio editor), lightweight-charts (for the two older consoles), ajv (schema validation), and react-resizable-panels. State is instead handled by a small number of repeated, hand-built patterns, described below. This is a verified architectural observation, not a criticism or a recommendation — the patterns are deliberate and are each documented in the code they appear in.

Pattern 1: small React contexts for cross-widget selection

Where two or more dashboard widgets need to agree on a small, slow-changing fact, the project uses a plain React context wrapping useState — never a reducer library. DashboardSelectionContext (instrument/bar-type/resolution) and ConnectionSelectionContext (venue/account/environment) are the two examples on the dashboard (see dashboard-and-workspaces.md); both are described in their own source as deliberately dependency-free, so a widget can read or write shared selection without importing the gateway or an engine type. This is state that is genuinely UI-local — "what is selected right now" — and is never persisted beyond the page session.

Pattern 2: module-level singleton stores with useSyncExternalStore

For state that is shared across a whole dashboard but does not naturally belong to any one component — a single high-rate tick stream, or the roster of backtest jobs a browser has submitted — the project uses a plain module-level singleton object, read into React via useSyncExternalStore (React 19's supported mechanism for subscribing to state living outside React), rather than a context provider. src/components/market/tick-store.ts is a clear example: its own comment states the design choice directly — "This is a plain module-level singleton, not a React context ... useSyncExternalStore is the sanctioned way for components to subscribe to state that lives outside React, and a dashboard has exactly one of these regardless of how many components read it." The same module documents a specific performance discipline: an incoming SSE tick never calls a React setter directly; it mutates a private draft object and schedules one flush per animation frame (falling back to a 16ms setTimeout where requestAnimationFrame is unavailable, e.g. under jsdom in tests), so "a burst of quotes/trades within one frame produces at most one React render, never one per tick." This store replaced an earlier design where three separate widgets each opened their own unfiltered stream subscription across roughly 820 instruments; the singleton scopes one connection to whichever instrument is currently selected.

src/strategy/runs/store.ts follows the same shape for the Backtests workspace, and makes an explicit durable-versus-ephemeral split: only the lightweight BacktestJobSummary roster is persisted to localStorage (versioned, the same convention layout-store.ts and screener/persistence.ts use); the fuller BacktestJobDetail (which can carry a full result/reports/stats document) and streamed log lines are read fresh from the backend on demand and never written to storage, because — in the module's own words — "persisting that would turn a cache into a second copy of backend state this module would then have to keep honest." A corrupted or stale local roster is explicitly recoverable rather than fatal: it is repopulated from the backend's own GET /v2/backtests listing the moment that route answers.

Pattern 3: one-shot memoized reads for backend facts that rarely change

Facts that are expensive to fetch but effectively static for a page's lifetime — the published resolution contract (GET /v2/resolutions), a resolved instrument's full catalog entry — are cached as a single memoized promise or a Map, at module scope, invalidated only by an explicit test hook. src/datafeed/resolution-contract.ts's loadResolutionContract is the clearest instance: "One cached read per page load, retried on failure (a failed read is not memoized — the next caller re-asks rather than freezing the datafeed on a transient)." The datafeed's own entryCache (a Map<instrumentId, InstrumentIndexEntry>) follows the same idea for individual instrument lookups, seeded incrementally by every completed symbol search so a later selection resolves without a further catalog walk.

Pattern 4: schema validation as the cache-adjacent correctness layer

Every document read from the backend that carries a governed schema is checked at runtime against the exact vendored JSON Schema for its declared schema_id/schema_version, via ajv (src/contracts/validate.ts). Compiled validator functions are cached in a Map<schemaId, ValidateFunction> so each schema is compiled once per page load, not once per document. The validation order is itself part of the contract, not an implementation detail, and is stated as such in the code: is it an object at all; does it carry schema tags; is the expected schema one this build vendors; does the observed tag match what the caller asked for; is the wire version one this build understands; only then is the document's shape actually checked. A document that fails any step becomes a typed ContractViolation (MALFORMED, MISSING_SCHEMA_TAG, UNEXPECTED_SCHEMA_ID, UNKNOWN_SCHEMA_ID, UNKNOWN_SCHEMA_VERSION, SCHEMA_VIOLATION), never a value quietly coerced into the expected shape. This is not a performance cache in the traditional sense — it exists to make "this document is contractually what we asked for" a checked fact rather than an assumption, and its Map-based compile cache is a byproduct of that.

What is deliberately not cached

Nothing in src/state/** or the store modules above persists a live trading fact — an order, a fill, a position, a bar — to localStorage or any other durable browser storage. Persistence is reserved for browser-local preference (layout, view column order, the backtest-job roster's lightweight summary) and is always explicitly versioned and bounded in size, with a stated policy for what happens when a stored document fails to parse (see dashboard-and-workspaces.md's comparison of /dashboard's silent-fallback policy against /workspace-v2's visible-failure policy).

Status

The absence of a state-management dependency is verified directly from package.json. The four patterns above are verified against the specific modules cited and their accompanying unit tests (tick-store.test.ts, store.test.ts under src/strategy/runs/, resolution-map.test.ts, validate.test.ts) — resolution-contract.ts itself is exercised indirectly through the datafeed's own test suite rather than a dedicated unit test file at this pin. This page describes the patterns actually found, not an exhaustive inventory of every store in the codebase — see evidence/frontend/open-questions.md for what was not independently checked.

Evidence and source pins for this page

Verified. Current behaviour, confirmed in source at the pinned commit.

Verified on against the following immutable sources:

  • frontend@99f49dae:package.json
  • frontend@99f49dae:src/contracts/validate.ts
  • frontend@99f49dae:src/components/market/tick-store.ts
  • frontend@99f49dae:src/strategy/runs/store.ts
  • frontend@99f49dae:src/datafeed/resolution-contract.ts

Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.