gloomberb 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +3 -2
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +8 -1
- package/src/cli/native-smoke.ts +5 -0
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- package/src/components/chart/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/bitmap.ts +16 -9
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/list/view.tsx +1 -1
- package/src/components/command-bar/theme-picker.tsx +1 -1
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -5
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +45 -4
- package/src/components/layout/pane/index.tsx +6 -4
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +204 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +45 -3
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +10 -4
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +10 -6
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +12 -14
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/ui/data-table/opentui/index.tsx +29 -22
- package/src/components/ui/data-table/types.ts +10 -4
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/list-view.tsx +1 -1
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +27 -12
- package/src/components/ui/ui.test.tsx +87 -9
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/table.ts +5 -5
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
- package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/box.tsx +6 -1
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +18 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/native-smoke.ts +1 -0
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -84,6 +84,7 @@ export function WebTextField({
|
|
|
84
84
|
inputRef,
|
|
85
85
|
onChange,
|
|
86
86
|
onSubmit,
|
|
87
|
+
onBlur,
|
|
87
88
|
hint,
|
|
88
89
|
type = "text",
|
|
89
90
|
variant = "default",
|
|
@@ -155,7 +156,12 @@ export function WebTextField({
|
|
|
155
156
|
}}
|
|
156
157
|
onInput={onChange}
|
|
157
158
|
onChange={onChange}
|
|
158
|
-
onSubmit={() => onSubmit?.(
|
|
159
|
+
onSubmit={(nextValue?: string) => onSubmit?.(
|
|
160
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
161
|
+
)}
|
|
162
|
+
onBlur={(nextValue?: string) => onBlur?.(
|
|
163
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
164
|
+
)}
|
|
159
165
|
/>
|
|
160
166
|
</Box>
|
|
161
167
|
{hint && (
|
|
@@ -347,14 +353,14 @@ export function WebPageStackView({
|
|
|
347
353
|
|
|
348
354
|
if (!detailOpen) {
|
|
349
355
|
return (
|
|
350
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
356
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
351
357
|
{rootContent}
|
|
352
358
|
</Box>
|
|
353
359
|
);
|
|
354
360
|
}
|
|
355
361
|
|
|
356
362
|
return (
|
|
357
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
363
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
358
364
|
<Box
|
|
359
365
|
flexDirection="row"
|
|
360
366
|
alignItems="flex-start"
|
|
@@ -370,7 +376,7 @@ export function WebPageStackView({
|
|
|
370
376
|
flexDirection="row"
|
|
371
377
|
alignItems="center"
|
|
372
378
|
backgroundColor={panelFill()}
|
|
373
|
-
onMouseDown={(event) => {
|
|
379
|
+
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
374
380
|
event.preventDefault?.();
|
|
375
381
|
event.stopPropagation?.();
|
|
376
382
|
onBack();
|
|
@@ -413,7 +419,7 @@ export function WebPageStackView({
|
|
|
413
419
|
</Text>
|
|
414
420
|
) : null}
|
|
415
421
|
</Box>
|
|
416
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
422
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
417
423
|
{detailContent}
|
|
418
424
|
</Box>
|
|
419
425
|
</Box>
|
|
@@ -33,6 +33,9 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
33
33
|
const pendingDragRef = useRef<CellMouseEvent | null>(null);
|
|
34
34
|
const propsRef = useRef(props);
|
|
35
35
|
propsRef.current = props;
|
|
36
|
+
const hoverBackgroundColor = typeof props.hoverBackgroundColor === "string"
|
|
37
|
+
? props.hoverBackgroundColor
|
|
38
|
+
: undefined;
|
|
36
39
|
|
|
37
40
|
useImperativeHandle(ref, () => cellBoundsForElement(() => elementRef.current, () => propsRef.current) as unknown as HTMLDivElement, []);
|
|
38
41
|
|
|
@@ -140,6 +143,7 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
140
143
|
return (
|
|
141
144
|
<div
|
|
142
145
|
{...cleanDomProps(props)}
|
|
146
|
+
data-gloom-hover-bg={hoverBackgroundColor ? "true" : undefined}
|
|
143
147
|
data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
|
|
144
148
|
ref={elementRef}
|
|
145
149
|
onMouseDown={handleMouseDown}
|
|
@@ -149,9 +153,10 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
149
153
|
onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
|
|
150
154
|
style={{
|
|
151
155
|
...commonStyle(props),
|
|
156
|
+
"--gloom-box-hover-bg": hoverBackgroundColor,
|
|
152
157
|
...(props.style as CSSProperties | undefined),
|
|
153
158
|
...(props.visible === false ? { display: "none" } : undefined),
|
|
154
|
-
}}
|
|
159
|
+
} as CSSProperties}
|
|
155
160
|
>
|
|
156
161
|
{children as ReactNode}
|
|
157
162
|
</div>
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
/** @jsxImportSource react */
|
|
3
3
|
import {
|
|
4
4
|
forwardRef,
|
|
5
|
+
useCallback,
|
|
5
6
|
useEffect,
|
|
6
7
|
useImperativeHandle,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
type CSSProperties,
|
|
10
11
|
type KeyboardEvent,
|
|
12
|
+
type RefObject,
|
|
11
13
|
} from "react";
|
|
12
14
|
import type { InputRenderable, TextareaRenderable } from "../../../../ui/host";
|
|
13
15
|
import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../input-host";
|
|
@@ -79,6 +81,65 @@ function useEditableValue(props: Record<string, unknown>) {
|
|
|
79
81
|
return { value, valueRef, setValue };
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
function useLatestRef<T>(value: T): RefObject<T> {
|
|
85
|
+
const ref = useRef(value);
|
|
86
|
+
ref.current = value;
|
|
87
|
+
return ref;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function useSyncedEditableElement<T extends HTMLInputElement | HTMLTextAreaElement>({
|
|
91
|
+
elementRef,
|
|
92
|
+
propsRef,
|
|
93
|
+
valueRef,
|
|
94
|
+
handleValueChange,
|
|
95
|
+
active,
|
|
96
|
+
}: {
|
|
97
|
+
elementRef: RefObject<T | null>;
|
|
98
|
+
propsRef: RefObject<Record<string, unknown>>;
|
|
99
|
+
valueRef: RefObject<string>;
|
|
100
|
+
handleValueChange: (nextValue: string) => void;
|
|
101
|
+
active: boolean;
|
|
102
|
+
}): () => string {
|
|
103
|
+
const syncElementValue = useCallback(() => {
|
|
104
|
+
const nextValue = elementRef.current?.value;
|
|
105
|
+
if (typeof nextValue !== "string") return valueRef.current;
|
|
106
|
+
if (nextValue !== valueRef.current) {
|
|
107
|
+
handleValueChange(nextValue);
|
|
108
|
+
}
|
|
109
|
+
return nextValue;
|
|
110
|
+
}, [elementRef, handleValueChange, valueRef]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!active) return;
|
|
114
|
+
let animationFrame: number | null = null;
|
|
115
|
+
const syncDomValue = () => {
|
|
116
|
+
syncElementValue();
|
|
117
|
+
animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
|
|
118
|
+
};
|
|
119
|
+
animationFrame = globalThis.requestAnimationFrame?.(syncDomValue) ?? null;
|
|
120
|
+
return () => {
|
|
121
|
+
if (animationFrame !== null) globalThis.cancelAnimationFrame?.(animationFrame);
|
|
122
|
+
};
|
|
123
|
+
}, [active, syncElementValue]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!active) return;
|
|
127
|
+
const commitBeforeOutsideMouseDown = (event: globalThis.MouseEvent) => {
|
|
128
|
+
const element = elementRef.current;
|
|
129
|
+
const target = event.target;
|
|
130
|
+
if (!element || (target instanceof Node && element.contains(target))) return;
|
|
131
|
+
const nextValue = syncElementValue();
|
|
132
|
+
callTextHandler(propsRef.current.onBlur, nextValue);
|
|
133
|
+
};
|
|
134
|
+
document.addEventListener("mousedown", commitBeforeOutsideMouseDown, true);
|
|
135
|
+
return () => {
|
|
136
|
+
document.removeEventListener("mousedown", commitBeforeOutsideMouseDown, true);
|
|
137
|
+
};
|
|
138
|
+
}, [active, elementRef, propsRef, syncElementValue]);
|
|
139
|
+
|
|
140
|
+
return syncElementValue;
|
|
141
|
+
}
|
|
142
|
+
|
|
82
143
|
type WebVisualCursor = TextareaRenderable["visualCursor"];
|
|
83
144
|
|
|
84
145
|
function textareaColumnCount(props: Record<string, unknown>, element: HTMLTextAreaElement | null): number {
|
|
@@ -149,8 +210,10 @@ function textareaMetrics(
|
|
|
149
210
|
|
|
150
211
|
export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(function WebInput(props, ref) {
|
|
151
212
|
const elementRef = useRef<HTMLInputElement | null>(null);
|
|
213
|
+
const propsRef = useLatestRef(props);
|
|
152
214
|
const { value, valueRef, setValue } = useEditableValue(props);
|
|
153
215
|
const [cursorOffset, setCursorOffset] = useState(value.length);
|
|
216
|
+
const [domFocused, setDomFocused] = useState(false);
|
|
154
217
|
const applyCursorOffset = (offset: number) => {
|
|
155
218
|
setCursorOffset(offset);
|
|
156
219
|
queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
|
|
@@ -165,7 +228,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
|
|
|
165
228
|
|
|
166
229
|
useImperativeHandle(ref, () => ({
|
|
167
230
|
editBuffer: {
|
|
168
|
-
getText: () => valueRef.current,
|
|
231
|
+
getText: () => elementRef.current?.value ?? valueRef.current,
|
|
169
232
|
setText: (nextText: string) => setValue(nextText),
|
|
170
233
|
},
|
|
171
234
|
get cursorOffset() {
|
|
@@ -175,18 +238,25 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
|
|
|
175
238
|
focus: () => elementRef.current?.focus(),
|
|
176
239
|
}), [applyCursorOffset, cursorOffset, setValue, valueRef]);
|
|
177
240
|
|
|
178
|
-
const handleValueChange = (nextValue: string) => {
|
|
241
|
+
const handleValueChange = useCallback((nextValue: string) => {
|
|
179
242
|
setValue(nextValue);
|
|
180
243
|
setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
|
|
181
|
-
callTextHandler(
|
|
182
|
-
callTextHandler(
|
|
183
|
-
callTextHandler(
|
|
184
|
-
};
|
|
244
|
+
callTextHandler(propsRef.current.onInput, nextValue);
|
|
245
|
+
callTextHandler(propsRef.current.onChange, nextValue);
|
|
246
|
+
callTextHandler(propsRef.current.onCursorChange, nextValue);
|
|
247
|
+
}, [propsRef, setValue]);
|
|
248
|
+
const syncElementValue = useSyncedEditableElement({
|
|
249
|
+
elementRef,
|
|
250
|
+
propsRef,
|
|
251
|
+
valueRef,
|
|
252
|
+
handleValueChange,
|
|
253
|
+
active: props.focused === true || domFocused,
|
|
254
|
+
});
|
|
185
255
|
|
|
186
256
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
187
|
-
if (event.key === "Enter" && typeof
|
|
257
|
+
if (event.key === "Enter" && typeof propsRef.current.onSubmit === "function") {
|
|
188
258
|
event.preventDefault();
|
|
189
|
-
(
|
|
259
|
+
(propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
|
|
190
260
|
}
|
|
191
261
|
};
|
|
192
262
|
|
|
@@ -200,7 +270,16 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
|
|
|
200
270
|
autoComplete="off"
|
|
201
271
|
spellCheck={false}
|
|
202
272
|
placeholder={getStringProp(props, "placeholder")}
|
|
273
|
+
onInput={(event) => handleValueChange(event.currentTarget.value)}
|
|
203
274
|
onChange={(event) => handleValueChange(event.currentTarget.value)}
|
|
275
|
+
onFocus={() => {
|
|
276
|
+
setDomFocused(true);
|
|
277
|
+
callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
|
|
278
|
+
}}
|
|
279
|
+
onBlur={() => {
|
|
280
|
+
setDomFocused(false);
|
|
281
|
+
callTextHandler(propsRef.current.onBlur, syncElementValue());
|
|
282
|
+
}}
|
|
204
283
|
onKeyDown={handleKeyDown}
|
|
205
284
|
onContextMenu={(event) => {
|
|
206
285
|
if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
|
|
@@ -211,7 +290,7 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
|
|
|
211
290
|
}}
|
|
212
291
|
onSelect={() => {
|
|
213
292
|
setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
|
|
214
|
-
callTextHandler(
|
|
293
|
+
callTextHandler(propsRef.current.onCursorChange, valueRef.current);
|
|
215
294
|
}}
|
|
216
295
|
style={textInputStyle(props, false)}
|
|
217
296
|
/>
|
|
@@ -220,8 +299,10 @@ export const WebInput = forwardRef<InputRenderable, Record<string, unknown>>(fun
|
|
|
220
299
|
|
|
221
300
|
export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown>>(function WebTextarea(props, ref) {
|
|
222
301
|
const elementRef = useRef<HTMLTextAreaElement | null>(null);
|
|
302
|
+
const propsRef = useLatestRef(props);
|
|
223
303
|
const { value, valueRef, setValue } = useEditableValue(props);
|
|
224
304
|
const [cursorOffset, setCursorOffset] = useState(value.length);
|
|
305
|
+
const [domFocused, setDomFocused] = useState(false);
|
|
225
306
|
const applyCursorOffset = (offset: number) => {
|
|
226
307
|
setCursorOffset(offset);
|
|
227
308
|
queueMicrotask(() => applyDomCursorOffset(elementRef.current, offset));
|
|
@@ -236,7 +317,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
|
|
|
236
317
|
|
|
237
318
|
useImperativeHandle(ref, () => ({
|
|
238
319
|
editBuffer: {
|
|
239
|
-
getText: () => valueRef.current,
|
|
320
|
+
getText: () => elementRef.current?.value ?? valueRef.current,
|
|
240
321
|
setText: (nextText: string) => setValue(nextText),
|
|
241
322
|
},
|
|
242
323
|
get cursorOffset() {
|
|
@@ -271,18 +352,25 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
|
|
|
271
352
|
clearLineHighlights: () => {},
|
|
272
353
|
}), [applyCursorOffset, cursorOffset, props, setValue, valueRef]);
|
|
273
354
|
|
|
274
|
-
const handleValueChange = (nextValue: string) => {
|
|
355
|
+
const handleValueChange = useCallback((nextValue: string) => {
|
|
275
356
|
setValue(nextValue);
|
|
276
357
|
setCursorOffset(elementRef.current?.selectionStart ?? nextValue.length);
|
|
277
|
-
callTextHandler(
|
|
278
|
-
callTextHandler(
|
|
279
|
-
callTextHandler(
|
|
280
|
-
};
|
|
358
|
+
callTextHandler(propsRef.current.onInput, nextValue);
|
|
359
|
+
callTextHandler(propsRef.current.onChange, nextValue);
|
|
360
|
+
callTextHandler(propsRef.current.onCursorChange, nextValue);
|
|
361
|
+
}, [propsRef, setValue]);
|
|
362
|
+
const syncElementValue = useSyncedEditableElement({
|
|
363
|
+
elementRef,
|
|
364
|
+
propsRef,
|
|
365
|
+
valueRef,
|
|
366
|
+
handleValueChange,
|
|
367
|
+
active: props.focused === true || domFocused,
|
|
368
|
+
});
|
|
281
369
|
|
|
282
370
|
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
283
|
-
if (event.key === "Enter" && !event.shiftKey && typeof
|
|
371
|
+
if (event.key === "Enter" && !event.shiftKey && typeof propsRef.current.onSubmit === "function") {
|
|
284
372
|
event.preventDefault();
|
|
285
|
-
(
|
|
373
|
+
(propsRef.current.onSubmit as (value: string) => void)(syncElementValue());
|
|
286
374
|
}
|
|
287
375
|
};
|
|
288
376
|
|
|
@@ -296,7 +384,16 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
|
|
|
296
384
|
autoComplete="off"
|
|
297
385
|
spellCheck={false}
|
|
298
386
|
placeholder={getStringProp(props, "placeholder")}
|
|
387
|
+
onInput={(event) => handleValueChange(event.currentTarget.value)}
|
|
299
388
|
onChange={(event) => handleValueChange(event.currentTarget.value)}
|
|
389
|
+
onFocus={() => {
|
|
390
|
+
setDomFocused(true);
|
|
391
|
+
callTextHandler(propsRef.current.onFocus, elementRef.current?.value ?? valueRef.current);
|
|
392
|
+
}}
|
|
393
|
+
onBlur={() => {
|
|
394
|
+
setDomFocused(false);
|
|
395
|
+
callTextHandler(propsRef.current.onBlur, syncElementValue());
|
|
396
|
+
}}
|
|
300
397
|
onKeyDown={handleKeyDown}
|
|
301
398
|
onContextMenu={(event) => {
|
|
302
399
|
if (!NATIVE_CONTEXT_MENU_SUPPORTED) return;
|
|
@@ -307,7 +404,7 @@ export const WebTextarea = forwardRef<TextareaRenderable, Record<string, unknown
|
|
|
307
404
|
}}
|
|
308
405
|
onSelect={() => {
|
|
309
406
|
setCursorOffset(elementRef.current?.selectionStart ?? valueRef.current.length);
|
|
310
|
-
callTextHandler(
|
|
407
|
+
callTextHandler(propsRef.current.onCursorChange, valueRef.current);
|
|
311
408
|
}}
|
|
312
409
|
style={textInputStyle(props, true)}
|
|
313
410
|
/>
|
|
@@ -104,6 +104,7 @@ export function cleanDomProps(props: Record<string, unknown>): Record<string, un
|
|
|
104
104
|
"cursorColor", "selectionBg", "selectionFg", "showCursor", "keyBindings", "wrapText", "wrapMode",
|
|
105
105
|
"initialValue", "value", "onInput", "onChange", "onSubmit", "onCursorChange", "onMouse",
|
|
106
106
|
"scrollX", "scrollY", "focusable", "bitmap", "bitmaps", "crosshair", "text", "font",
|
|
107
|
+
"hoverBackgroundColor",
|
|
107
108
|
]) {
|
|
108
109
|
delete next[key];
|
|
109
110
|
}
|
|
@@ -21,6 +21,7 @@ export function WebTabs({
|
|
|
21
21
|
closeMode = "always",
|
|
22
22
|
addLabel = "+",
|
|
23
23
|
onAdd,
|
|
24
|
+
focused = false,
|
|
24
25
|
palette,
|
|
25
26
|
}: HostTabsProps) {
|
|
26
27
|
const activeTabRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -112,16 +113,20 @@ export function WebTabs({
|
|
|
112
113
|
paddingBlock: 0,
|
|
113
114
|
paddingBottom: showUnderline ? 2 : 0,
|
|
114
115
|
margin: 0,
|
|
115
|
-
border:
|
|
116
|
+
border: "1px solid transparent",
|
|
116
117
|
borderRadius: variant === "underline" ? 5 : 6,
|
|
117
118
|
background: resolveTabBackground(active, hovered),
|
|
119
|
+
boxSizing: "border-box",
|
|
118
120
|
font: "inherit",
|
|
119
121
|
fontSize: tabFontSize,
|
|
120
122
|
fontWeight: active ? 700 : 500,
|
|
121
123
|
lineHeight: 1,
|
|
122
124
|
textAlign: "center",
|
|
123
125
|
whiteSpace: "nowrap",
|
|
124
|
-
|
|
126
|
+
borderColor: active && focused && variant !== "pill"
|
|
127
|
+
? `color-mix(in srgb, ${palette.activeUnderline} 35%, transparent)`
|
|
128
|
+
: "transparent",
|
|
129
|
+
transition: "background-color 110ms ease, border-color 110ms ease, color 110ms ease",
|
|
125
130
|
cursor: disabled ? "default" : "pointer",
|
|
126
131
|
} satisfies CssVars;
|
|
127
132
|
|
|
@@ -20,6 +20,13 @@ body {
|
|
|
20
20
|
input, textarea, button { font: inherit; }
|
|
21
21
|
input, textarea { cursor: text; user-select: text; }
|
|
22
22
|
button, [data-gloom-interactive="true"] { cursor: pointer; }
|
|
23
|
+
[data-gloom-hover-bg="true"]:hover {
|
|
24
|
+
background-color: var(--gloom-box-hover-bg) !important;
|
|
25
|
+
}
|
|
26
|
+
[data-gloom-role="data-table-row"]:not([data-selected="true"]):hover,
|
|
27
|
+
[data-gloom-role="data-table-row"]:not([data-selected="true"]):hover > [data-gloom-role="data-table-cell"] {
|
|
28
|
+
background-color: var(--gloom-hover-bg) !important;
|
|
29
|
+
}
|
|
23
30
|
[data-gloom-role="pane-window"] {
|
|
24
31
|
background-clip: padding-box;
|
|
25
32
|
isolation: isolate;
|
|
@@ -170,6 +177,17 @@ body.gloom-dragging {
|
|
|
170
177
|
[data-gloom-role="status-bar"] span {
|
|
171
178
|
line-height: var(--cell-h);
|
|
172
179
|
}
|
|
180
|
+
[data-gloom-role="pane-body"] {
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
}
|
|
183
|
+
[data-gloom-role="pane-content"] {
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
}
|
|
186
|
+
[data-gloom-role="pane-content"] > * {
|
|
187
|
+
min-width: 0;
|
|
188
|
+
min-height: 0;
|
|
189
|
+
max-height: 100%;
|
|
190
|
+
}
|
|
173
191
|
[data-gloom-role="pane-footer"] {
|
|
174
192
|
flex: 0 0 var(--cell-h);
|
|
175
193
|
height: var(--cell-h);
|
|
@@ -138,8 +138,14 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
138
138
|
get isDestroyed() {
|
|
139
139
|
return renderer.isDestroyed;
|
|
140
140
|
},
|
|
141
|
+
get currentFocusedRenderable() {
|
|
142
|
+
return renderer.currentFocusedRenderable ?? null;
|
|
143
|
+
},
|
|
144
|
+
get currentFocusedEditor() {
|
|
145
|
+
return renderer.currentFocusedEditor ?? null;
|
|
146
|
+
},
|
|
141
147
|
get keyInput() {
|
|
142
|
-
return renderer.keyInput;
|
|
148
|
+
return renderer.keyInput as unknown as NativeRendererHost["keyInput"];
|
|
143
149
|
},
|
|
144
150
|
on: (event, handler) => renderer.on(event, handler),
|
|
145
151
|
off: (event, handler) => renderer.off(event, handler),
|
|
@@ -147,7 +153,17 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
147
153
|
registerLifecyclePass: (renderable) => renderer.registerLifecyclePass(renderable as any),
|
|
148
154
|
unregisterLifecyclePass: (renderable) => renderer.unregisterLifecyclePass(renderable as any),
|
|
149
155
|
getSelection: () => renderer.getSelection(),
|
|
156
|
+
getCursorState: () => renderer.getCursorState(),
|
|
157
|
+
setCursorPosition: (x, y, visible = true) => renderer.setCursorPosition(x, y, visible),
|
|
158
|
+
addPostProcessFn: (processFn) => renderer.addPostProcessFn(processFn as any),
|
|
159
|
+
removePostProcessFn: (processFn) => renderer.removePostProcessFn(processFn as any),
|
|
150
160
|
copyToClipboardOSC52: (text) => renderer.copyToClipboardOSC52(text),
|
|
161
|
+
captureMouseRenderable(renderable) {
|
|
162
|
+
const capture = (renderer as unknown as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
|
|
163
|
+
if (typeof capture === "function") {
|
|
164
|
+
capture.call(renderer, renderable);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
151
167
|
write(data) {
|
|
152
168
|
if (renderer.isDestroyed) return false;
|
|
153
169
|
const writer = (renderer as unknown as { writeOut?: (payload: string | Uint8Array) => void }).writeOut;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@opentui/core";
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { RGBA, StyledText as OpenTuiStyledText, SyntaxStyle, TextAttributes as OpenTuiTextAttributes } from "@opentui/core";
|
|
2
|
-
import { createElement } from "react";
|
|
2
|
+
import { createElement, forwardRef, type ReactNode } from "react";
|
|
3
3
|
import "opentui-spinner/react";
|
|
4
4
|
import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
|
|
5
5
|
import { renderAsciiText } from "../../ui/ascii-font";
|
|
6
6
|
import { OpenTuiImageSurface } from "./image/surface";
|
|
7
7
|
import { OpenTuiChartSurface } from "./chart-surface";
|
|
8
8
|
|
|
9
|
+
interface OpenTuiPrimitiveProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createOpenTuiPrimitive(tagName: string) {
|
|
15
|
+
return forwardRef<unknown, OpenTuiPrimitiveProps>(function OpenTuiPrimitive(
|
|
16
|
+
{ children, ...props },
|
|
17
|
+
ref,
|
|
18
|
+
) {
|
|
19
|
+
return createElement(tagName as any, { ...props, ref }, children as ReactNode);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
|
|
10
24
|
const flags = typeof appAttributes === "number" ? appAttributes : 0;
|
|
11
25
|
let attributes = 0;
|
|
@@ -37,41 +51,59 @@ function mapTextContent(content: unknown): unknown {
|
|
|
37
51
|
fg: mapColor(chunk.fg),
|
|
38
52
|
bg: mapColor(chunk.bg),
|
|
39
53
|
attributes: mapTextAttributes(chunk.attributes as number | undefined),
|
|
40
|
-
})));
|
|
54
|
+
})) as any);
|
|
41
55
|
}
|
|
42
56
|
return content;
|
|
43
57
|
}
|
|
44
58
|
|
|
59
|
+
const OpenTuiBox = createOpenTuiPrimitive("box");
|
|
60
|
+
const OpenTuiScrollBox = createOpenTuiPrimitive("scrollbox");
|
|
61
|
+
const OpenTuiInput = createOpenTuiPrimitive("input");
|
|
62
|
+
const OpenTuiTextarea = createOpenTuiPrimitive("textarea");
|
|
63
|
+
const OpenTuiSpinnerMark = createOpenTuiPrimitive("spinner");
|
|
64
|
+
|
|
65
|
+
const OpenTuiText = forwardRef<unknown, TextProps>(function OpenTuiText({ children, ...props }, ref) {
|
|
66
|
+
const textProps = stripTextProps(props);
|
|
67
|
+
return createElement("text" as any, {
|
|
68
|
+
...textProps,
|
|
69
|
+
ref,
|
|
70
|
+
content: mapTextContent(textProps.content),
|
|
71
|
+
attributes: mapTextAttributes(props.attributes as number | undefined, props),
|
|
72
|
+
}, children as ReactNode);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const OpenTuiSpan = forwardRef<unknown, TextProps>(function OpenTuiSpan({ children, ...props }, ref) {
|
|
76
|
+
return createElement("span" as any, {
|
|
77
|
+
...stripTextProps(props),
|
|
78
|
+
ref,
|
|
79
|
+
attributes: mapTextAttributes(props.attributes as number | undefined, props),
|
|
80
|
+
}, children as ReactNode);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const OpenTuiStrong = forwardRef<unknown, TextProps>(function OpenTuiStrong({ children, ...props }, ref) {
|
|
84
|
+
return createElement("strong" as any, { ...stripTextProps(props), ref }, children as ReactNode);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const OpenTuiUnderline = forwardRef<unknown, TextProps>(function OpenTuiUnderline({ children, ...props }, ref) {
|
|
88
|
+
return createElement("u" as any, { ...stripTextProps(props), ref }, children as ReactNode);
|
|
89
|
+
});
|
|
90
|
+
|
|
45
91
|
export const openTuiUiHost: UiHost = {
|
|
46
92
|
kind: "opentui",
|
|
47
93
|
capabilities: {
|
|
48
94
|
nativeCharts: true,
|
|
49
95
|
},
|
|
50
|
-
Box:
|
|
51
|
-
Text:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
>
|
|
59
|
-
{children}
|
|
60
|
-
</text>
|
|
61
|
-
);
|
|
62
|
-
},
|
|
63
|
-
Span: ({ children, ...props }) => createElement("span" as any, {
|
|
64
|
-
...stripTextProps(props),
|
|
65
|
-
attributes: mapTextAttributes(props.attributes as number | undefined, props),
|
|
66
|
-
}, children),
|
|
67
|
-
Strong: ({ children, ...props }) => createElement("strong" as any, stripTextProps(props), children),
|
|
68
|
-
Underline: ({ children, ...props }) => createElement("u" as any, stripTextProps(props), children),
|
|
69
|
-
ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
|
|
70
|
-
Input: ({ children, ...props }) => createElement("input" as any, props, children),
|
|
71
|
-
Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
|
|
96
|
+
Box: OpenTuiBox as UiHost["Box"],
|
|
97
|
+
Text: OpenTuiText as UiHost["Text"],
|
|
98
|
+
Span: OpenTuiSpan as UiHost["Span"],
|
|
99
|
+
Strong: OpenTuiStrong as UiHost["Strong"],
|
|
100
|
+
Underline: OpenTuiUnderline as UiHost["Underline"],
|
|
101
|
+
ScrollBox: OpenTuiScrollBox as UiHost["ScrollBox"],
|
|
102
|
+
Input: OpenTuiInput as UiHost["Input"],
|
|
103
|
+
Textarea: OpenTuiTextarea as UiHost["Textarea"],
|
|
72
104
|
ChartSurface: OpenTuiChartSurface,
|
|
73
105
|
ImageSurface: OpenTuiImageSurface,
|
|
74
|
-
SpinnerMark:
|
|
106
|
+
SpinnerMark: OpenTuiSpinnerMark as UiHost["SpinnerMark"],
|
|
75
107
|
AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
|
|
76
108
|
const resolvedColor = color ?? fg;
|
|
77
109
|
const resolvedBackground = backgroundColor ?? bg;
|
|
@@ -11,7 +11,12 @@ import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history
|
|
|
11
11
|
import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
12
12
|
import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
|
|
13
13
|
import { selectCachedResource } from "./cache";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
hasDeepStatementHistory,
|
|
16
|
+
hasDetailedStatementRows,
|
|
17
|
+
quoteWithFreshnessExchange,
|
|
18
|
+
type CachedFinancialsSelection,
|
|
19
|
+
} from "./financials";
|
|
15
20
|
import type { ProviderRouterCoreDeps } from "./route-types";
|
|
16
21
|
|
|
17
22
|
export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
|
|
@@ -31,6 +36,10 @@ export interface ProviderRouterBatchDeps extends ProviderRouterCoreDeps {
|
|
|
31
36
|
export class ProviderRouterBatchRoutes {
|
|
32
37
|
constructor(private readonly deps: ProviderRouterBatchDeps) {}
|
|
33
38
|
|
|
39
|
+
private needsSingleFinancialsRoute(value: TickerFinancials): boolean {
|
|
40
|
+
return !(hasDetailedStatementRows(value) && hasDeepStatementHistory(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
async getQuotesBatch(
|
|
35
44
|
targets: QuoteSubscriptionTarget[],
|
|
36
45
|
options: { forceRefresh?: boolean } = {},
|
|
@@ -105,6 +114,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
105
114
|
): Promise<TickerFinancialsBatchResult[]> {
|
|
106
115
|
const forceRefresh = options.forceRefresh === true;
|
|
107
116
|
const results = new Array<TickerFinancialsBatchResult | null>(targets.length).fill(null);
|
|
117
|
+
const batchFallbacks = new Array<TickerFinancials | null>(targets.length).fill(null);
|
|
108
118
|
const misses: Array<{ index: number; target: CachedFinancialsTarget }> = [];
|
|
109
119
|
|
|
110
120
|
targets.forEach((target, index) => {
|
|
@@ -139,6 +149,8 @@ export class ProviderRouterBatchRoutes {
|
|
|
139
149
|
const entityKey = this.deps.getEntityKey(entry.target.symbol, entry.target.instrument ?? undefined);
|
|
140
150
|
const variantKey = this.deps.getTickerVariantCandidates(entry.target.exchange)[0] ?? "";
|
|
141
151
|
this.deps.cacheResource("financials", entityKey, variantKey, sourceKey, value, this.deps.resolveProviderPolicy("financials", batchProvider));
|
|
152
|
+
batchFallbacks[entry.index] = value;
|
|
153
|
+
if (this.needsSingleFinancialsRoute(value)) continue;
|
|
142
154
|
results[entry.index] = { target: entry.target, financials: value };
|
|
143
155
|
}
|
|
144
156
|
}
|
|
@@ -153,7 +165,9 @@ export class ProviderRouterBatchRoutes {
|
|
|
153
165
|
});
|
|
154
166
|
results[index] = { target, financials };
|
|
155
167
|
} catch (error) {
|
|
156
|
-
results[index] =
|
|
168
|
+
results[index] = batchFallbacks[index]
|
|
169
|
+
? { target, financials: batchFallbacks[index] }
|
|
170
|
+
: { target, financials: null, error };
|
|
157
171
|
}
|
|
158
172
|
}));
|
|
159
173
|
|
|
@@ -19,6 +19,7 @@ import { withBrokerTimeout } from "./brokers";
|
|
|
19
19
|
import {
|
|
20
20
|
deriveMarketCapFromShares,
|
|
21
21
|
hasMeaningfulProfile,
|
|
22
|
+
hasShallowStatementHistory,
|
|
22
23
|
mergeCachedFinancialRecords,
|
|
23
24
|
mergeFinancials,
|
|
24
25
|
quoteWithFreshnessExchange,
|
|
@@ -86,6 +87,10 @@ export class ProviderRouterFinancialRoutes {
|
|
|
86
87
|
if (isOptionTicker && !cached.value.quote) {
|
|
87
88
|
return quoteOnlyFinancials(cached.value);
|
|
88
89
|
}
|
|
90
|
+
if (!cached.stale && hasMeaningfulProfile(cached.value) && hasShallowStatementHistory(cached.value)) {
|
|
91
|
+
const providerResult = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context);
|
|
92
|
+
return mergeFinancials(cached.value, providerResult?.value ?? null) ?? cached.value;
|
|
93
|
+
}
|
|
89
94
|
if (!cached.stale && hasMeaningfulProfile(cached.value)) {
|
|
90
95
|
return cached.value;
|
|
91
96
|
}
|