Fix live tape scroll hold and lazy history
This commit is contained in:
parent
eaddf4b7a0
commit
39fb5ce9f1
6 changed files with 332 additions and 75 deletions
|
|
@ -164,6 +164,7 @@ describe("live manifest", () => {
|
|||
|
||||
expect(optionsSubscription?.underlying_ids).toEqual(["AAPL"]);
|
||||
expect(optionsSubscription?.option_contract_id).toBe("AAPL-2025-01-17-200-C");
|
||||
expect(optionsSubscription?.snapshot_limit).toBe(100);
|
||||
expect(equitiesSubscription?.underlying_ids).toEqual(["AAPL"]);
|
||||
});
|
||||
|
||||
|
|
@ -635,23 +636,23 @@ describe("live tape history helpers", () => {
|
|||
expect(next.map((item) => item.trace_id)).toEqual(["existing", "older-1"]);
|
||||
});
|
||||
|
||||
it("keeps scoped option and equity history on the normal retention cap", () => {
|
||||
it("keeps option and equity history effectively unbounded while scrolling", () => {
|
||||
expect(
|
||||
getLiveHistoryRetentionCap({
|
||||
channel: "options",
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
} as any)
|
||||
).toBeGreaterThan(0);
|
||||
).toBe(0);
|
||||
expect(
|
||||
getLiveHistoryRetentionCap({
|
||||
channel: "equities",
|
||||
underlying_ids: ["AAPL"]
|
||||
} as any)
|
||||
).toBeGreaterThan(0);
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps auto-hydrating scoped live history while next_before exists", () => {
|
||||
it("does not auto-hydrate scoped live history before the scroll gate is reached", () => {
|
||||
const manifest = getLiveManifest(
|
||||
"/tape",
|
||||
"AAPL",
|
||||
|
|
@ -669,18 +670,12 @@ describe("live tape history helpers", () => {
|
|||
|
||||
expect(
|
||||
getScopedLiveAutoHydrationChannels(true, "/tape", manifest, historyCursors, {})
|
||||
).toEqual(["options", "equities"]);
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getScopedLiveAutoHydrationChannels(true, "/tape", manifest, historyCursors, {
|
||||
[getLiveSubscriptionKey(manifest.find((subscription) => subscription.channel === "options")!)]: true
|
||||
})
|
||||
).toEqual(["equities"]);
|
||||
expect(
|
||||
getScopedLiveAutoHydrationChannels(true, "/tape", manifest, {
|
||||
...historyCursors,
|
||||
[getLiveSubscriptionKey(manifest.find((subscription) => subscription.channel === "equities")!)]: null
|
||||
}, {})
|
||||
).toEqual(["options"]);
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("restores the same anchor key after live insertions at the top", () => {
|
||||
|
|
@ -864,9 +859,15 @@ describe("signals helpers", () => {
|
|||
expect(getAlertWindowAnchorTs([], 42)).toBe(42);
|
||||
});
|
||||
|
||||
it("returns connected/stale live status labels without live wording", () => {
|
||||
it("returns connected/held/stale live status labels without live wording", () => {
|
||||
expect(statusLabel("connected", false, "live")).toBe("Connected");
|
||||
expect(statusLabel("connected", true, "live")).toBe("Held");
|
||||
expect(statusLabel("stale", false, "live")).toBe("Feed behind");
|
||||
expect(statusLabel("stale", true, "live")).toBe("Feed behind");
|
||||
});
|
||||
|
||||
it("keeps replay pause wording on replay tapes", () => {
|
||||
expect(statusLabel("connected", true, "replay")).toBe("Paused");
|
||||
});
|
||||
|
||||
it("treats healthy scoped channels as connected even when no matching rows are visible", () => {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ const LIVE_HOT_WINDOW_OPTIONS = parseBoundedInt(
|
|||
1,
|
||||
100000
|
||||
);
|
||||
const LIVE_OPTIONS_HEAD_LIMIT = 100;
|
||||
const LIVE_HISTORY_SOFT_CAP = parseBoundedInt(
|
||||
process.env.NEXT_PUBLIC_LIVE_HISTORY_SOFT_CAP,
|
||||
5000,
|
||||
|
|
@ -846,7 +847,7 @@ export const getLiveHistoryRetentionCap = (subscription: LiveSubscription): numb
|
|||
switch (subscription.channel) {
|
||||
case "options":
|
||||
case "equities":
|
||||
return LIVE_HISTORY_SOFT_CAP;
|
||||
return 0;
|
||||
default:
|
||||
return LIVE_HISTORY_SOFT_CAP;
|
||||
}
|
||||
|
|
@ -859,27 +860,12 @@ export const getScopedLiveAutoHydrationChannels = (
|
|||
historyCursors: Partial<Record<string, Cursor | null>>,
|
||||
historyLoading: Partial<Record<string, boolean>>
|
||||
): Array<Extract<LiveSubscription["channel"], "options" | "equities">> => {
|
||||
if (!enabled || pathname !== "/tape") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const channels: Array<Extract<LiveSubscription["channel"], "options" | "equities">> = [];
|
||||
for (const subscription of manifest) {
|
||||
const scoped =
|
||||
(subscription.channel === "options" &&
|
||||
(subscription.underlying_ids?.length || subscription.option_contract_id)) ||
|
||||
(subscription.channel === "equities" && subscription.underlying_ids?.length);
|
||||
if (!scoped) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = getLiveSubscriptionKey(subscription);
|
||||
if (historyCursors[key] && !historyLoading[key]) {
|
||||
channels.push(subscription.channel);
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
void enabled;
|
||||
void pathname;
|
||||
void manifest;
|
||||
void historyCursors;
|
||||
void historyLoading;
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getLiveFeedStatus = (
|
||||
|
|
@ -2027,7 +2013,10 @@ export const prunePinnedEntries = <T,>(
|
|||
|
||||
export const statusLabel = (status: WsStatus, paused: boolean, mode: TapeMode): string => {
|
||||
if (paused) {
|
||||
return "Paused";
|
||||
if (mode === "replay") {
|
||||
return "Paused";
|
||||
}
|
||||
return status === "connected" ? "Held" : statusLabel(status, false, mode);
|
||||
}
|
||||
|
||||
if (mode === "replay") {
|
||||
|
|
@ -2512,22 +2501,20 @@ type PausableTapeViewConfig<T extends SortableItem & { seq: number }> = {
|
|||
const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||
config: PausableTapeViewConfig<T>
|
||||
): TapeState<T> => {
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [data, setData] = useState<PausableTapeData<T>>(EMPTY_PAUSABLE_TAPE);
|
||||
const holdForScroll = config.enabled ? (config.shouldHold ? config.shouldHold() : false) : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.enabled) {
|
||||
setPaused(false);
|
||||
setData(EMPTY_PAUSABLE_TAPE);
|
||||
return;
|
||||
}
|
||||
|
||||
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||
setData((current) => {
|
||||
const next = reducePausableTapeData(
|
||||
current,
|
||||
config.sourceItems,
|
||||
paused || holdForScroll,
|
||||
holdForScroll,
|
||||
config.retentionLimit ?? LIVE_HOT_WINDOW
|
||||
);
|
||||
if (next === current) {
|
||||
|
|
@ -2535,7 +2522,7 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
}
|
||||
|
||||
const unseenCount = next.seenKeys.size - current.seenKeys.size;
|
||||
if (!paused && unseenCount > 0) {
|
||||
if (unseenCount > 0) {
|
||||
config.onNewItems?.(unseenCount);
|
||||
config.captureScroll?.();
|
||||
}
|
||||
|
|
@ -2548,17 +2535,11 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
config.onNewItems,
|
||||
config.captureScroll,
|
||||
config.retentionLimit,
|
||||
config.shouldHold,
|
||||
paused
|
||||
holdForScroll
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.enabled || paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||
if (holdForScroll) {
|
||||
if (!config.enabled || holdForScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2581,14 +2562,9 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
config.onNewItems,
|
||||
config.retentionLimit,
|
||||
config.resumeSignal,
|
||||
config.shouldHold,
|
||||
paused
|
||||
holdForScroll
|
||||
]);
|
||||
|
||||
const togglePause = useCallback(() => {
|
||||
setPaused((current) => !current);
|
||||
}, []);
|
||||
|
||||
const status = config.enabled ? config.sourceStatus : "disconnected";
|
||||
const projected = projectPausableTapeState(data.visible, status, config.lastUpdate);
|
||||
const historyItems = config.historyTail ?? [];
|
||||
|
|
@ -2602,9 +2578,9 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
lastUpdate: projected.lastUpdate,
|
||||
replayTime: null,
|
||||
replayComplete: false,
|
||||
paused,
|
||||
paused: holdForScroll,
|
||||
dropped: data.dropped,
|
||||
togglePause
|
||||
togglePause: () => {}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -3052,7 +3028,7 @@ export const getLiveManifest = (
|
|||
? undefined
|
||||
: optionPrintFilters ?? flowFilters,
|
||||
...optionScope,
|
||||
snapshot_limit: LIVE_HOT_WINDOW_OPTIONS
|
||||
snapshot_limit: LIVE_OPTIONS_HEAD_LIMIT
|
||||
});
|
||||
}
|
||||
if (features.nbbo) {
|
||||
|
|
@ -3337,7 +3313,7 @@ const useLiveSession = (
|
|||
|
||||
switch (subscription.channel) {
|
||||
case "options":
|
||||
mergeItems(setOptions, optionsRef, items as OptionPrint[], LIVE_HOT_WINDOW_OPTIONS, {
|
||||
mergeItems(setOptions, optionsRef, items as OptionPrint[], LIVE_OPTIONS_HEAD_LIMIT, {
|
||||
setter: setOptionsHistory,
|
||||
ref: optionsHistoryRef,
|
||||
cap: getLiveHistoryRetentionCap(subscription)
|
||||
|
|
@ -3794,6 +3770,7 @@ const TapeStatus = ({
|
|||
};
|
||||
|
||||
type TapeControlsProps = {
|
||||
mode: TapeMode;
|
||||
paused: boolean;
|
||||
onTogglePause: () => void;
|
||||
isAtTop: boolean;
|
||||
|
|
@ -3801,13 +3778,15 @@ type TapeControlsProps = {
|
|||
onJump: () => void;
|
||||
};
|
||||
|
||||
const TapeControls = ({ paused, onTogglePause, isAtTop, missed, onJump }: TapeControlsProps) => {
|
||||
const TapeControls = ({ mode, paused, onTogglePause, isAtTop, missed, onJump }: TapeControlsProps) => {
|
||||
const active = !isAtTop && missed > 0;
|
||||
return (
|
||||
<div className={`tape-controls${active ? " tape-controls-active" : ""}`}>
|
||||
<button className="pause-button" type="button" onClick={onTogglePause}>
|
||||
{paused ? "Resume" : "Pause"}
|
||||
</button>
|
||||
{mode === "replay" ? (
|
||||
<button className="pause-button" type="button" onClick={onTogglePause}>
|
||||
{paused ? "Resume" : "Pause"}
|
||||
</button>
|
||||
) : null}
|
||||
<button className="jump-button" type="button" onClick={onJump} disabled={isAtTop}>
|
||||
Jump to top
|
||||
</button>
|
||||
|
|
@ -5373,7 +5352,7 @@ const useTerminalState = () => {
|
|||
sourceItems: liveSession.options,
|
||||
historyTail: liveSession.optionsHistory,
|
||||
lastUpdate: liveSession.lastUpdate,
|
||||
retentionLimit: LIVE_HOT_WINDOW_OPTIONS,
|
||||
retentionLimit: LIVE_OPTIONS_HEAD_LIMIT,
|
||||
captureScroll: optionsAnchor.capture,
|
||||
onNewItems: optionsScroll.onNewItems,
|
||||
shouldHold: () => !optionsScroll.isAtTopRef.current,
|
||||
|
|
@ -7141,6 +7120,7 @@ const OptionsPane = memo(({ state, limit }: OptionsPaneProps) => {
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.options.paused}
|
||||
onTogglePause={state.options.togglePause}
|
||||
isAtTop={state.optionsScroll.isAtTop}
|
||||
|
|
@ -7329,6 +7309,7 @@ const EquitiesPane = memo(({ state, limit }: EquitiesPaneProps) => {
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.equities.paused}
|
||||
onTogglePause={state.equities.togglePause}
|
||||
isAtTop={state.equitiesScroll.isAtTop}
|
||||
|
|
@ -7432,6 +7413,7 @@ const FlowPane = memo(({ state, limit, title = "Flow" }: FlowPaneProps) => {
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.flow.paused}
|
||||
onTogglePause={state.flow.togglePause}
|
||||
isAtTop={state.flowScroll.isAtTop}
|
||||
|
|
@ -7581,6 +7563,7 @@ const AlertsPane = memo(({ state, limit, withStrip = false, className }: AlertsP
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.alerts.paused}
|
||||
onTogglePause={state.alerts.togglePause}
|
||||
isAtTop={state.alertsScroll.isAtTop}
|
||||
|
|
@ -7695,6 +7678,7 @@ const ClassifierPane = memo(({ state, limit, className }: ClassifierPaneProps) =
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.smartMoney.paused}
|
||||
onTogglePause={state.smartMoney.togglePause}
|
||||
isAtTop={state.classifierScroll.isAtTop}
|
||||
|
|
@ -7818,6 +7802,7 @@ const DarkPane = memo(({ state, limit, className }: DarkPaneProps) => {
|
|||
}
|
||||
actions={
|
||||
<TapeControls
|
||||
mode={state.mode}
|
||||
paused={state.inferredDark.paused}
|
||||
onTogglePause={state.inferredDark.togglePause}
|
||||
isAtTop={state.darkScroll.isAtTop}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue