- Break on the first previously seen item in newest-first merges - Avoid cloning seen key sets unless new items are added - Add tape overhaul phase 1 notes
7.5 KiB
Server-Backed Persistent History
Summary
Make live mode server-authoritative across refreshes, sessions, and devices. The browser will not own data persistence. On load, the app will hydrate from ClickHouse-backed server history, then layer live WebSocket updates on top. Users will immediately see a substantial recent persisted window, with older records available through history pagination.
Chosen Defaults
- Source of truth: ClickHouse on the server.
- Browser persistence: UI preferences only, no market-data cache.
- Initial load: recent persisted window per active channel.
- Older data: fetched on demand using cursor pagination.
- Scope: every channel the server handles, including options, NBBO, equities, equity quotes, equity joins, flow packets, classifier hits, alerts, inferred dark events, candles, and chart overlays.
- Freshness: freshness affects status labels only; it must not hide persisted history from a refreshed browser.
Current State To Change
LiveStateManagerhydrates from Redis or ClickHouse, but freshness gates currently suppress stale options, NBBO, equities, and flow snapshots.- The unified
/ws/liveprotocol supports snapshots andnext_before, but the frontend does not retain/use per-channel history cursors for live-mode pagination. - Some channels have REST history endpoints, but
equity-quotesis not fully represented in the unified live protocol/history API. - Charts already query ClickHouse for candle and overlay ranges, but should be treated as part of the same server-history model.
Public Interfaces And Types
Update packages/types/src/live.ts:
- Add
"equity-quotes"to:LiveGenericChannelSchemaLiveChannelSchemaLiveSubscriptionSchemalivePayloadSchemas
- Preserve existing
FeedSnapshotshape:itemswatermarknext_before
Update API routes in services/api/src/index.ts:
- Add
GET /history/equity-quotes?before_ts=&before_seq=&limit=. - Include
equity-quotesin/ws/livesubscriptions and fanout. - Keep existing recent/replay endpoints compatible.
Update storage in packages/storage/src/clickhouse.ts:
- Add
fetchEquityQuotesBefore. - Reuse existing
(ts, seq)cursor ordering. - Keep limits clamped consistently with other history endpoints.
Server Implementation
In services/api/src/live.ts:
- Add generic config for
equity-quotes:- Redis key:
live:equity-quotes - cursor field:
equity-quotes - parser:
EquityQuoteSchema - cursor:
{ ts, seq } - fetchRecent:
fetchRecentEquityQuotes
- Redis key:
- Stop filtering historical snapshots by freshness:
- Remove
filterFreshGenericItemsfrom snapshot construction. - Keep
isLiveItemFreshavailable for UI status/fanout behavior if needed. - Do not reject persisted ClickHouse rows just because market timestamps are older than 15s/30s.
- Remove
- Stop rejecting stale ingests inside
LiveStateManager.ingest.- The manager should store valid events it receives.
- Event fanout can still choose how to label status, but should not silently lose durable cache state.
- Preserve Redis as a hot cache:
- Redis remains an optimization.
- ClickHouse remains the fallback and source of truth.
- API startup should hydrate from Redis if present, otherwise from ClickHouse.
In services/api/src/index.ts:
- Include
equity-quotesinconsumerBindings. - Pump
EquityQuoteSchemapayloads into:- legacy
/ws/equity-quotes - unified
/ws/live LiveStateManager
- legacy
- Add
/history/equity-quotes. - Keep durable consumer defaults unchanged unless a test proves old events are skipped in a live-running API scenario. ClickHouse hydration handles restart and refresh persistence.
Frontend Implementation
In apps/web/app/terminal.tsx:
- Extend
LiveSessionStatewith:- per-subscription
next_beforecursors - per-subscription loading/error state for older history
- equity quotes if exposed in UI state
- per-subscription
- When handling
snapshotmessages:- Replace the channel's current items with snapshot items when non-empty.
- Store
snapshot.next_before. - Do not discard stale-but-persisted rows.
- Continue deduping by
trace_id/seqorid.
- Add a generic live-history loader:
- Map subscription channel to history endpoint:
options->/history/optionsnbbo->/history/nbboequities->/history/equitiesequity-quotes->/history/equity-quotesequity-joins->/history/equity-joinsflow->/history/flowclassifier-hits->/history/classifier-hitsalerts->/history/alertsinferred-dark->/history/inferred-dark
- Carry option/flow filters into options history queries.
- Merge older results into existing channel state.
- Advance
next_beforefrom the response. - Stop when
next_beforeis null or the response is empty.
- Map subscription channel to history endpoint:
- UI behavior:
- Add a compact "Load older" control at the bottom of each applicable tape/list.
- Disable it while loading.
- Hide it when no more history exists.
- Keep existing pause/jump controls unchanged.
- Do not add browser market-data storage.
- Chart behavior:
- Keep candles loading from
/candles/equities. - Keep overlay loading from
/prints/equities/range. - Ensure refresh and device changes show the same server data for the same ticker/window.
- Keep candles loading from
Config And Deployment
Update .env.example:
- Add
LIVE_LIMIT_EQUITY_QUOTES=10000. - Document that
LIVE_LIMIT_*controls initial server snapshot/hot-cache depth, not total persisted history.
Update README if needed:
- Clarify persistence model:
- ClickHouse is durable history.
- Redis is hot cache.
- Browser is not a market-data database.
- All devices connected to the same API see the same server-seen data.
Docker volumes already persist ClickHouse/Redis/NATS data locally and in deployment compose, so no migration is needed for volume-backed persistence.
Tests
API tests in services/api/tests/live.test.ts:
- Snapshot hydration returns stale historical options/NBBO/equities/flow instead of filtering them out.
LiveStateManager.ingeststores older valid events.equity-quoteshydrates from Redis.equity-quoteshydrates from ClickHouse when Redis is empty.next_beforeis set from the oldest item in returned snapshot.- Redis hot cache persists hydrated ClickHouse data.
Storage tests:
- Add
fetchEquityQuotesBeforecoverage using the existing storage test style.
Frontend tests in apps/web/app/terminal.test.ts:
- Live snapshot with older persisted rows populates visible rows.
- Empty snapshot does not wipe existing visible rows only when preserving an already visible channel during reconnect.
- Older-history merge dedupes existing items.
- History cursor advances after loading older rows.
- "No more history" state is reached when
next_beforeis null. - Live status can be stale while items remain visible.
Acceptance Criteria
- Refreshing the app shows persisted data immediately, even when no new live events arrive after page load.
- Opening the app on another device connected to the same API shows the same server-backed recent history.
- Stale market timestamps do not cause persisted history to disappear.
- Users can load older data beyond the initial recent window.
- Live WebSocket updates still appear without requiring refresh.
- Redis loss does not lose history; API falls back to ClickHouse.
- Browser cache deletion does not lose market data.
bun test services/api/tests/live.test.ts apps/web/app/terminal.test.ts packages/storage/tests/*.test.tspasses, or any unavailable test target is documented.