- 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
170 lines
7.5 KiB
Markdown
170 lines
7.5 KiB
Markdown
# 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
|
|
|
|
- `LiveStateManager` hydrates from Redis or ClickHouse, but freshness gates currently suppress stale options, NBBO, equities, and flow snapshots.
|
|
- The unified `/ws/live` protocol supports snapshots and `next_before`, but the frontend does not retain/use per-channel history cursors for live-mode pagination.
|
|
- Some channels have REST history endpoints, but `equity-quotes` is 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:
|
|
- `LiveGenericChannelSchema`
|
|
- `LiveChannelSchema`
|
|
- `LiveSubscriptionSchema`
|
|
- `livePayloadSchemas`
|
|
- Preserve existing `FeedSnapshot` shape:
|
|
- `items`
|
|
- `watermark`
|
|
- `next_before`
|
|
|
|
Update API routes in `services/api/src/index.ts`:
|
|
|
|
- Add `GET /history/equity-quotes?before_ts=&before_seq=&limit=`.
|
|
- Include `equity-quotes` in `/ws/live` subscriptions 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`:
|
|
|
|
1. Add generic config for `equity-quotes`:
|
|
- Redis key: `live:equity-quotes`
|
|
- cursor field: `equity-quotes`
|
|
- parser: `EquityQuoteSchema`
|
|
- cursor: `{ ts, seq }`
|
|
- fetchRecent: `fetchRecentEquityQuotes`
|
|
2. Stop filtering historical snapshots by freshness:
|
|
- Remove `filterFreshGenericItems` from snapshot construction.
|
|
- Keep `isLiveItemFresh` available for UI status/fanout behavior if needed.
|
|
- Do not reject persisted ClickHouse rows just because market timestamps are older than 15s/30s.
|
|
3. 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.
|
|
4. 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`:
|
|
|
|
1. Include `equity-quotes` in `consumerBindings`.
|
|
2. Pump `EquityQuoteSchema` payloads into:
|
|
- legacy `/ws/equity-quotes`
|
|
- unified `/ws/live`
|
|
- `LiveStateManager`
|
|
3. Add `/history/equity-quotes`.
|
|
4. 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`:
|
|
|
|
1. Extend `LiveSessionState` with:
|
|
- per-subscription `next_before` cursors
|
|
- per-subscription loading/error state for older history
|
|
- equity quotes if exposed in UI state
|
|
2. When handling `snapshot` messages:
|
|
- 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/seq` or `id`.
|
|
3. Add a generic live-history loader:
|
|
- Map subscription channel to history endpoint:
|
|
- `options` -> `/history/options`
|
|
- `nbbo` -> `/history/nbbo`
|
|
- `equities` -> `/history/equities`
|
|
- `equity-quotes` -> `/history/equity-quotes`
|
|
- `equity-joins` -> `/history/equity-joins`
|
|
- `flow` -> `/history/flow`
|
|
- `classifier-hits` -> `/history/classifier-hits`
|
|
- `alerts` -> `/history/alerts`
|
|
- `inferred-dark` -> `/history/inferred-dark`
|
|
- Carry option/flow filters into options history queries.
|
|
- Merge older results into existing channel state.
|
|
- Advance `next_before` from the response.
|
|
- Stop when `next_before` is null or the response is empty.
|
|
4. 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.
|
|
5. 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.
|
|
|
|
## 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.ingest` stores older valid events.
|
|
- `equity-quotes` hydrates from Redis.
|
|
- `equity-quotes` hydrates from ClickHouse when Redis is empty.
|
|
- `next_before` is set from the oldest item in returned snapshot.
|
|
- Redis hot cache persists hydrated ClickHouse data.
|
|
|
|
Storage tests:
|
|
|
|
- Add `fetchEquityQuotesBefore` coverage 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_before` is 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.ts` passes, or any unavailable test target is documented.
|