diff --git a/apps/web/app/terminal.test.ts b/apps/web/app/terminal.test.ts index 883b9cd..9eb51d0 100644 --- a/apps/web/app/terminal.test.ts +++ b/apps/web/app/terminal.test.ts @@ -10,6 +10,7 @@ import { getAlertWindowAnchorTs, getOptionTableSnapshot, getLiveFeedStatus, + getLiveManifest, normalizeAlertSeverity, nextFlowFilterPopoverState, projectPausableTapeState, @@ -38,6 +39,42 @@ const makeAlert = (overrides: Record = {}) => ...overrides }) as any; +describe("live manifest", () => { + it("includes options on every live route", () => { + const filters = buildDefaultFlowFilters(); + for (const pathname of ["/", "/tape", "/signals", "/charts", "/replay"]) { + expect( + getLiveManifest(pathname, "SPY", 60000, filters).some( + (subscription) => subscription.channel === "options" + ) + ).toBe(true); + } + }); + + it("dedupes tape options subscription", () => { + const tapeOptionsSubscriptions = getLiveManifest( + "/tape", + "SPY", + 60000, + buildDefaultFlowFilters() + ).filter((subscription) => subscription.channel === "options"); + expect(tapeOptionsSubscriptions).toHaveLength(1); + }); + + it("keeps option filters on baseline subscription", () => { + const filters = { + ...buildDefaultFlowFilters(), + minNotional: 125_000 + }; + + const optionsSubscription = getLiveManifest("/signals", "SPY", 60000, filters).find( + (subscription) => subscription.channel === "options" + ); + + expect(optionsSubscription?.filters).toBe(filters); + }); +}); + describe("live tape pausable helpers", () => { it("queues new items while paused and flushes them on resume", () => { let state = reducePausableTapeData( diff --git a/apps/web/app/terminal.tsx b/apps/web/app/terminal.tsx index 4a29481..9f56047 100644 --- a/apps/web/app/terminal.tsx +++ b/apps/web/app/terminal.tsx @@ -2185,47 +2185,72 @@ type LiveSessionState = { chartOverlay: EquityPrint[]; }; -const getLiveManifest = ( +const dedupeLiveSubscriptions = (subscriptions: LiveSubscription[]): LiveSubscription[] => { + const seen = new Set(); + return subscriptions.filter((subscription) => { + const key = getLiveSubscriptionKey(subscription); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +}; + +export const getLiveManifest = ( pathname: string, chartTicker: string, chartIntervalMs: number, flowFilters: OptionFlowFilters ): LiveSubscription[] => { + const baselineSubs: LiveSubscription[] = [{ channel: "options", filters: flowFilters }]; const chartSubs: LiveSubscription[] = [ { channel: "equity-candles", underlying_id: chartTicker, interval_ms: chartIntervalMs }, { channel: "equity-overlay", underlying_id: chartTicker } ]; if (pathname === "/tape") { - return [ + return dedupeLiveSubscriptions([ + ...baselineSubs, { channel: "options", filters: flowFilters }, { channel: "nbbo" }, { channel: "equities" }, { channel: "flow", filters: flowFilters }, { channel: "classifier-hits" } - ]; + ]); } if (pathname === "/signals") { - return [{ channel: "alerts" }, { channel: "classifier-hits" }, { channel: "inferred-dark" }]; + return dedupeLiveSubscriptions([ + ...baselineSubs, + { channel: "alerts" }, + { channel: "classifier-hits" }, + { channel: "inferred-dark" } + ]); } if (pathname === "/charts") { - return [...chartSubs, { channel: "classifier-hits" }, { channel: "inferred-dark" }]; + return dedupeLiveSubscriptions([ + ...baselineSubs, + ...chartSubs, + { channel: "classifier-hits" }, + { channel: "inferred-dark" } + ]); } if (pathname === "/replay") { - return []; + return baselineSubs; } - return [ + return dedupeLiveSubscriptions([ + ...baselineSubs, { channel: "equities" }, { channel: "flow" }, { channel: "alerts" }, { channel: "classifier-hits" }, { channel: "inferred-dark" }, ...chartSubs - ]; + ]); }; const useLiveSession = (