gloomberb 0.9.8 → 0.9.10
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/package.json +1 -1
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +0 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +0 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app.tsx +1 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +0 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/index.test.ts +0 -10
- package/src/market-data/coordinator/index.ts +1 -18
- package/src/market-data/coordinator/quotes.ts +0 -6
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/resolution.ts +2 -2
- package/src/market-data/request-types.ts +0 -5
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/form-components.tsx +0 -120
- package/src/plugins/builtin/account-management/model.ts +3 -27
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -1
- package/src/plugins/builtin/chat/content/index.tsx +1 -2
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -1
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/sidebar.test.tsx +15 -5
- package/src/plugins/builtin/chat/test-harness.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +1 -24
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +44 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/financial-routes.ts +25 -3
- package/src/sources/provider-router/index.test.ts +167 -0
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +13 -8
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/sync/controller.test.ts +151 -0
- package/src/sync/controller.ts +92 -57
- package/src/sync/core-contributors.ts +45 -21
- package/src/sync/react.ts +6 -8
- package/src/sync/types.ts +3 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/ui/host.tsx +2 -1
- package/src/ui/index.tsx +1 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -87,7 +87,7 @@ export function PriceSelectorDialog({
|
|
|
87
87
|
const inputRef = useRef<InputRenderable>(null);
|
|
88
88
|
|
|
89
89
|
useEffect(() => {
|
|
90
|
-
if (mode === "input") inputRef.current?.focus();
|
|
90
|
+
if (mode === "input") inputRef.current?.focus?.();
|
|
91
91
|
}, [mode]);
|
|
92
92
|
|
|
93
93
|
useDialogKeyboard((event) => {
|
|
@@ -108,7 +108,7 @@ export function PriceSelectorDialog({
|
|
|
108
108
|
setMode("input");
|
|
109
109
|
setIndex(CUSTOM_INDEX);
|
|
110
110
|
}
|
|
111
|
-
} else if (event.name === "return") {
|
|
111
|
+
} else if (event.name === "enter" || event.name === "return") {
|
|
112
112
|
if (presets[index]) {
|
|
113
113
|
resolve(String(presets[index].value));
|
|
114
114
|
}
|
|
@@ -124,7 +124,7 @@ export function PriceSelectorDialog({
|
|
|
124
124
|
if (event.name === "up" || (event.name === "tab" && presets.length > 0)) {
|
|
125
125
|
setMode("list");
|
|
126
126
|
setIndex(presets.length - 1);
|
|
127
|
-
} else if (event.name === "return") {
|
|
127
|
+
} else if (event.name === "enter" || event.name === "return") {
|
|
128
128
|
resolve(customValue.trim());
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -4,7 +4,6 @@ import { TickerBadge } from "./index";
|
|
|
4
4
|
import { ExternalLinkText, openUrl } from "../../ui";
|
|
5
5
|
import { tokenizeInlineContent } from "../../../utils/inline-content-tokenizer";
|
|
6
6
|
import type { InlineTickerCatalogEntry } from "../../../state/hooks/inline-tickers";
|
|
7
|
-
import { displayWidth } from "../../../utils/format";
|
|
8
7
|
import { splitLongTextSegmentByDisplayWidth } from "../../../utils/text-wrap";
|
|
9
8
|
import { blendHex, colors } from "../../../theme/colors";
|
|
10
9
|
|
|
@@ -88,7 +88,7 @@ export function PageStackView({
|
|
|
88
88
|
>
|
|
89
89
|
<Box height={1} flexDirection="row">
|
|
90
90
|
<Box
|
|
91
|
-
onMouseDown={(event) => {
|
|
91
|
+
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
92
92
|
goBack(event);
|
|
93
93
|
}}
|
|
94
94
|
backgroundColor={colors.selected}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { TextAttributes } from "../../ui";
|
|
5
4
|
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
6
5
|
import { ChoiceDialog } from "./choice-dialog";
|
|
7
6
|
import { DataTable, type DataTableColumn } from "./data-table";
|
|
@@ -171,37 +170,6 @@ function DataTableHorizontalScrollHarness({
|
|
|
171
170
|
);
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
function DataTableColumnGapHarness() {
|
|
175
|
-
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
176
|
-
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
177
|
-
const items = [{ id: "row-0", left: "AA", right: "BB" }];
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<DataTableView
|
|
181
|
-
rootWidth={8}
|
|
182
|
-
rootHeight={4}
|
|
183
|
-
selection={{ kind: "none" }}
|
|
184
|
-
columns={[
|
|
185
|
-
{ id: "left", label: "L", width: 2, align: "left" },
|
|
186
|
-
{ id: "right", label: "R", width: 2, align: "left" },
|
|
187
|
-
]}
|
|
188
|
-
items={items}
|
|
189
|
-
sortColumnId={null}
|
|
190
|
-
sortDirection="asc"
|
|
191
|
-
onHeaderClick={() => {}}
|
|
192
|
-
headerScrollRef={headerScrollRef}
|
|
193
|
-
scrollRef={scrollRef}
|
|
194
|
-
getItemKey={(row) => row.id}
|
|
195
|
-
renderCell={(row, column) => ({ text: column.id === "left" ? row.left : row.right })}
|
|
196
|
-
emptyStateTitle="No rows."
|
|
197
|
-
columnGap={0}
|
|
198
|
-
horizontalPadding={0}
|
|
199
|
-
fillAvailableWidth={false}
|
|
200
|
-
showHorizontalScrollbar={false}
|
|
201
|
-
/>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
173
|
function DataTableVirtualizationHarness() {
|
|
206
174
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
207
175
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
@@ -392,31 +360,6 @@ describe("shared UI kit", () => {
|
|
|
392
360
|
expect(lastSelected).toBe("chart");
|
|
393
361
|
});
|
|
394
362
|
|
|
395
|
-
test("marks the active bare tab when it has keyboard focus", async () => {
|
|
396
|
-
testSetup = await testRender(
|
|
397
|
-
<Tabs
|
|
398
|
-
tabs={[
|
|
399
|
-
{ label: "One", value: "one" },
|
|
400
|
-
{ label: "Two", value: "two" },
|
|
401
|
-
]}
|
|
402
|
-
activeValue="one"
|
|
403
|
-
onSelect={() => {}}
|
|
404
|
-
variant="bare"
|
|
405
|
-
compact
|
|
406
|
-
focused
|
|
407
|
-
/>,
|
|
408
|
-
{ width: 24, height: 2 },
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
await act(async () => {
|
|
412
|
-
await testSetup!.renderOnce();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
const activeSpan = testSetup.captureSpans().lines[0]?.spans.find((span) => span.text.includes("One"));
|
|
416
|
-
expect(activeSpan).toBeDefined();
|
|
417
|
-
expect((activeSpan!.attributes & TextAttributes.UNDERLINE)).toBe(TextAttributes.UNDERLINE);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
363
|
test("selects tabs by clicking their text labels", async () => {
|
|
421
364
|
let lastSelected = "overview";
|
|
422
365
|
const selectedValues: string[] = [];
|
|
@@ -794,27 +737,6 @@ describe("shared UI kit", () => {
|
|
|
794
737
|
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
795
738
|
});
|
|
796
739
|
|
|
797
|
-
test("renders data table columns without spacer cells when column gap is zero", async () => {
|
|
798
|
-
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
799
|
-
testSetup = await testRender(
|
|
800
|
-
<AppContext value={{ state, dispatch: () => {} }}>
|
|
801
|
-
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
802
|
-
<DataTableColumnGapHarness />
|
|
803
|
-
</PaneInstanceProvider>
|
|
804
|
-
</AppContext>,
|
|
805
|
-
{ width: 8, height: 4 },
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
await act(async () => {
|
|
809
|
-
await testSetup!.renderOnce();
|
|
810
|
-
await testSetup!.renderOnce();
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const frame = testSetup.captureCharFrame();
|
|
814
|
-
expect(frame).toContain("AABB");
|
|
815
|
-
expect(frame).not.toContain("AA BB");
|
|
816
|
-
});
|
|
817
|
-
|
|
818
740
|
test("hides data table horizontal scrolling when content fits", async () => {
|
|
819
741
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
820
742
|
tableScrollBoxForTest = null;
|
|
@@ -60,9 +60,9 @@ function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefine
|
|
|
60
60
|
const docked = (() => {
|
|
61
61
|
const raw = (value as PanePlacementMemory).docked;
|
|
62
62
|
if (!raw || typeof raw !== "object") return undefined;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const rawPath = (raw as { path?: unknown }).path;
|
|
64
|
+
const path = Array.isArray(rawPath)
|
|
65
|
+
? rawPath.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
|
|
66
66
|
: undefined;
|
|
67
67
|
const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
|
|
68
68
|
? (raw as { anchorInstanceId: string }).anchorInstanceId
|
|
@@ -291,6 +291,12 @@ export class ResourceStore {
|
|
|
291
291
|
}
|
|
292
292
|
records.push(record);
|
|
293
293
|
}
|
|
294
|
+
if (options.touch !== false) {
|
|
295
|
+
for (const record of records) {
|
|
296
|
+
const touchedAt = this.touch(record);
|
|
297
|
+
if (touchedAt != null) record.lastAccessedAt = touchedAt;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
294
300
|
return records;
|
|
295
301
|
}
|
|
296
302
|
|
|
@@ -35,6 +35,7 @@ describe("AppPersistence", () => {
|
|
|
35
35
|
}, {
|
|
36
36
|
cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
|
|
37
37
|
schemaVersion: 1,
|
|
38
|
+
fetchedAt: 1,
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
expect(persistence.resources.get<{ price: number }>({
|
|
@@ -43,6 +44,10 @@ describe("AppPersistence", () => {
|
|
|
43
44
|
entityKey: "AAPL",
|
|
44
45
|
sourceKey: "provider:yahoo",
|
|
45
46
|
}, { allowExpired: true })?.value.price).toBe(123);
|
|
47
|
+
const listed = persistence.resources.list<{ price: number }>({
|
|
48
|
+
namespace: "market", kind: "quote", entityKey: "AAPL",
|
|
49
|
+
}, { allowExpired: true });
|
|
50
|
+
expect(listed[0]?.lastAccessedAt).toBeGreaterThan(1);
|
|
46
51
|
persistence.close();
|
|
47
52
|
});
|
|
48
53
|
|
package/src/data/ticker-store.ts
CHANGED
|
@@ -47,11 +47,4 @@ export class TickerStore {
|
|
|
47
47
|
this.db.query("DELETE FROM tickers WHERE symbol = ?").run(symbol);
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
count(): number {
|
|
52
|
-
const row = withSqliteBusyRetry("count tickers", () => (
|
|
53
|
-
this.db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM tickers").get()
|
|
54
|
-
));
|
|
55
|
-
return row?.count ?? 0;
|
|
56
|
-
}
|
|
57
50
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
|
|
2
|
-
import type { NewsArticle } from "../../news/types";
|
|
3
2
|
import type { OptionsChain } from "../../types/financials";
|
|
4
|
-
import type {
|
|
3
|
+
import type { OptionsRequest, SecFilingsRequest } from "../request-types";
|
|
5
4
|
import type { QueryEntry } from "../result-types";
|
|
6
5
|
import { QueryStore } from "../query-store";
|
|
7
6
|
import {
|
|
8
7
|
ARTICLE_SUMMARY_CACHE_TTL_MS,
|
|
9
8
|
EXPECTED_EMPTY,
|
|
10
9
|
FX_CACHE_TTL_MS,
|
|
11
|
-
NEWS_CACHE_TTL_MS,
|
|
12
10
|
OPTIONS_CACHE_TTL_MS,
|
|
13
11
|
SEC_CONTENT_CACHE_TTL_MS,
|
|
14
12
|
SEC_FILINGS_CACHE_TTL_MS,
|
|
@@ -23,7 +21,6 @@ import {
|
|
|
23
21
|
import {
|
|
24
22
|
buildArticleSummaryKey,
|
|
25
23
|
buildFxKey,
|
|
26
|
-
buildNewsKey,
|
|
27
24
|
buildOptionsKey,
|
|
28
25
|
buildSecContentKey,
|
|
29
26
|
buildSecDocumentsKey,
|
|
@@ -42,46 +39,6 @@ interface AuxiliaryLoaderOptions<T> {
|
|
|
42
39
|
load: (startedAt: number) => Promise<QueryEntry<T>>;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
interface NewsDataProvider extends DataProvider {
|
|
46
|
-
getNews?: (query: {
|
|
47
|
-
feed: "ticker";
|
|
48
|
-
ticker: string;
|
|
49
|
-
exchange?: string;
|
|
50
|
-
tickerTier: "primary";
|
|
51
|
-
limit?: number;
|
|
52
|
-
}) => Promise<NewsArticle[]>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function loadNewsEntry(options: {
|
|
56
|
-
dataProvider: DataProvider;
|
|
57
|
-
request: NewsRequest;
|
|
58
|
-
store: QueryStore<NewsArticle[]>;
|
|
59
|
-
runSingleFlight: RunSingleFlight;
|
|
60
|
-
}): Promise<QueryEntry<NewsArticle[]>> {
|
|
61
|
-
const { dataProvider, request, store, runSingleFlight } = options;
|
|
62
|
-
const key = buildNewsKey(request);
|
|
63
|
-
return loadAuxiliaryEntry({
|
|
64
|
-
dataProvider,
|
|
65
|
-
key,
|
|
66
|
-
store,
|
|
67
|
-
ttlMs: NEWS_CACHE_TTL_MS,
|
|
68
|
-
runSingleFlight,
|
|
69
|
-
load: async (startedAt) => {
|
|
70
|
-
const newsProvider = dataProvider as NewsDataProvider;
|
|
71
|
-
if (!newsProvider.getNews) throw new Error("No news provider available");
|
|
72
|
-
const data = await newsProvider.getNews({
|
|
73
|
-
feed: "ticker",
|
|
74
|
-
ticker: request.instrument.symbol,
|
|
75
|
-
exchange: request.instrument.exchange ?? "",
|
|
76
|
-
tickerTier: "primary",
|
|
77
|
-
limit: request.count ?? 50,
|
|
78
|
-
});
|
|
79
|
-
const attempts = [createAttempt(dataProvider.id, startedAt, data.length > 0 ? "success" : "empty", data.length === 0 ? "NO_DATA" : undefined)];
|
|
80
|
-
return store.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
42
|
export function loadOptionsEntry(options: {
|
|
86
43
|
dataProvider: DataProvider;
|
|
87
44
|
request: OptionsRequest;
|
|
@@ -8,7 +8,6 @@ const EMPTY_MESSAGE = "No data available";
|
|
|
8
8
|
export const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
|
|
9
9
|
export const SNAPSHOT_CACHE_TTL_MS = 5 * 60_000;
|
|
10
10
|
export const CHART_CACHE_TTL_MS = 10 * 60_000;
|
|
11
|
-
export const NEWS_CACHE_TTL_MS = 2 * 60_000;
|
|
12
11
|
export const OPTIONS_CACHE_TTL_MS = 10 * 60_000;
|
|
13
12
|
export const SEC_FILINGS_CACHE_TTL_MS = 10 * 60_000;
|
|
14
13
|
export const SEC_CONTENT_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
@@ -155,13 +155,8 @@ describe("MarketDataCoordinator", () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it("reuses fresh empty tab query results instead of refetching on reopen", async () => {
|
|
158
|
-
let newsCalls = 0;
|
|
159
158
|
let optionsCalls = 0;
|
|
160
159
|
const provider = createProvider({
|
|
161
|
-
getNews: async () => {
|
|
162
|
-
newsCalls += 1;
|
|
163
|
-
return [];
|
|
164
|
-
},
|
|
165
160
|
getOptionsChain: async () => {
|
|
166
161
|
optionsCalls += 1;
|
|
167
162
|
return {
|
|
@@ -175,16 +170,11 @@ describe("MarketDataCoordinator", () => {
|
|
|
175
170
|
const coordinator = new MarketDataCoordinator(provider);
|
|
176
171
|
const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
177
172
|
|
|
178
|
-
const firstNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
179
|
-
const secondNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
180
173
|
const firstOptions = await coordinator.loadOptions({ instrument });
|
|
181
174
|
const secondOptions = await coordinator.loadOptions({ instrument });
|
|
182
175
|
|
|
183
|
-
expect(firstNews.error?.reasonCode).toBe("NO_DATA");
|
|
184
|
-
expect(secondNews.error?.reasonCode).toBe("NO_DATA");
|
|
185
176
|
expect(firstOptions.error?.reasonCode).toBe("NO_DATA");
|
|
186
177
|
expect(secondOptions.error?.reasonCode).toBe("NO_DATA");
|
|
187
|
-
expect(newsCalls).toBe(1);
|
|
188
178
|
expect(optionsCalls).toBe(1);
|
|
189
179
|
});
|
|
190
180
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
|
|
2
|
-
import type { NewsArticle } from "../../news/types";
|
|
3
2
|
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
4
|
-
import type { ChartRequest, InstrumentRef,
|
|
3
|
+
import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "../request-types";
|
|
5
4
|
import { QueryStore } from "../query-store";
|
|
6
5
|
import type { QueryEntry } from "../result-types";
|
|
7
6
|
import {
|
|
@@ -9,7 +8,6 @@ import {
|
|
|
9
8
|
buildChartKey,
|
|
10
9
|
buildFxKey,
|
|
11
10
|
buildTickerFinancialsSnapshot,
|
|
12
|
-
buildNewsKey,
|
|
13
11
|
buildOptionsKey,
|
|
14
12
|
buildQuoteKey,
|
|
15
13
|
buildSecContentKey,
|
|
@@ -41,7 +39,6 @@ import {
|
|
|
41
39
|
import {
|
|
42
40
|
loadArticleSummaryEntry,
|
|
43
41
|
loadFxRateEntry,
|
|
44
|
-
loadNewsEntry,
|
|
45
42
|
loadOptionsEntry,
|
|
46
43
|
loadSecFilingContentEntry,
|
|
47
44
|
loadSecFilingDocumentsEntry,
|
|
@@ -73,7 +70,6 @@ export class MarketDataCoordinator {
|
|
|
73
70
|
private readonly fundamentalsStore = new QueryStore<TickerFinancials["fundamentals"]>((key) => this.events.bump(key));
|
|
74
71
|
private readonly statementsStore = new QueryStore<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>((key) => this.events.bump(key));
|
|
75
72
|
private readonly chartStore = new QueryStore<PricePoint[]>((key) => this.events.bump(key));
|
|
76
|
-
private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.events.bump(key));
|
|
77
73
|
private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.events.bump(key));
|
|
78
74
|
private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.events.bump(key));
|
|
79
75
|
private readonly secDocumentsStore = new QueryStore<SecFilingDocument[]>((key) => this.events.bump(key));
|
|
@@ -124,10 +120,6 @@ export class MarketDataCoordinator {
|
|
|
124
120
|
return this.chartStore.get(buildChartKey(request));
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
getNewsEntry(request: NewsRequest): QueryEntry<NewsArticle[]> {
|
|
128
|
-
return this.newsStore.get(buildNewsKey(request));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
123
|
getOptionsEntry(request: OptionsRequest): QueryEntry<OptionsChain> {
|
|
132
124
|
return this.optionsStore.get(buildOptionsKey(request));
|
|
133
125
|
}
|
|
@@ -311,15 +303,6 @@ export class MarketDataCoordinator {
|
|
|
311
303
|
});
|
|
312
304
|
}
|
|
313
305
|
|
|
314
|
-
async loadNews(request: NewsRequest): Promise<QueryEntry<NewsArticle[]>> {
|
|
315
|
-
return loadNewsEntry({
|
|
316
|
-
dataProvider: this.dataProvider,
|
|
317
|
-
request,
|
|
318
|
-
store: this.newsStore,
|
|
319
|
-
runSingleFlight: (key, task) => this.runSingleFlight(key, task),
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
306
|
async loadOptions(request: OptionsRequest): Promise<QueryEntry<OptionsChain>> {
|
|
324
307
|
return loadOptionsEntry({
|
|
325
308
|
dataProvider: this.dataProvider,
|
|
@@ -228,7 +228,6 @@ export class QuoteSubscriptionManager {
|
|
|
228
228
|
private nextQuoteSubscriptionId = 1;
|
|
229
229
|
private quoteSubscriptionDispose: (() => void) | null = null;
|
|
230
230
|
private quoteSubscriptionSignature = "";
|
|
231
|
-
private pendingQuoteSubscriptionKeys = new Set<string>();
|
|
232
231
|
|
|
233
232
|
constructor(
|
|
234
233
|
private readonly dataProvider: DataProvider,
|
|
@@ -250,7 +249,6 @@ export class QuoteSubscriptionManager {
|
|
|
250
249
|
if (existing) {
|
|
251
250
|
existing.targets.set(subscriptionId, target);
|
|
252
251
|
existing.target = mergeQuoteSubscriptionTargets(existing.targets.values()) ?? target;
|
|
253
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
254
252
|
if (existing.removeTimer) {
|
|
255
253
|
clearTimeout(existing.removeTimer);
|
|
256
254
|
existing.removeTimer = null;
|
|
@@ -262,7 +260,6 @@ export class QuoteSubscriptionManager {
|
|
|
262
260
|
targets: new Map([[subscriptionId, target]]),
|
|
263
261
|
removeTimer: null,
|
|
264
262
|
});
|
|
265
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
266
263
|
}
|
|
267
264
|
this.flush();
|
|
268
265
|
|
|
@@ -274,7 +271,6 @@ export class QuoteSubscriptionManager {
|
|
|
274
271
|
const mergedTarget = mergeQuoteSubscriptionTargets(existing.targets.values());
|
|
275
272
|
if (mergedTarget) {
|
|
276
273
|
existing.target = mergedTarget;
|
|
277
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
278
274
|
this.flush();
|
|
279
275
|
continue;
|
|
280
276
|
}
|
|
@@ -283,7 +279,6 @@ export class QuoteSubscriptionManager {
|
|
|
283
279
|
const current = this.quoteSubscriptions.get(key);
|
|
284
280
|
if (!current || current.targets.size > 0) return;
|
|
285
281
|
this.quoteSubscriptions.delete(key);
|
|
286
|
-
this.pendingQuoteSubscriptionKeys.delete(key);
|
|
287
282
|
this.flush();
|
|
288
283
|
}, QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS);
|
|
289
284
|
}
|
|
@@ -292,7 +287,6 @@ export class QuoteSubscriptionManager {
|
|
|
292
287
|
|
|
293
288
|
private flush(): void {
|
|
294
289
|
if (!this.dataProvider.subscribeQuotes) return;
|
|
295
|
-
this.pendingQuoteSubscriptionKeys = new Set();
|
|
296
290
|
|
|
297
291
|
const activeEntries = [...this.quoteSubscriptions.entries()]
|
|
298
292
|
.filter(([, entry]) => entry.targets.size > 0 || entry.removeTimer)
|
|
@@ -40,6 +40,7 @@ function quote(symbol: string, price: number, overrides: Partial<Quote> = {}): Q
|
|
|
40
40
|
|
|
41
41
|
async function flushCoordinator(): Promise<void> {
|
|
42
42
|
await Promise.resolve();
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
describe("MarketDataCoordinator key subscriptions", () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MarketState, Quote, QuoteFieldProvenance } from "../../types/financials";
|
|
2
|
-
import { colors } from "../../theme/colors";
|
|
2
|
+
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
3
|
+
|
|
4
|
+
const CLOSED_CHANGE_MUTING_RATIO = 0.55;
|
|
3
5
|
|
|
4
6
|
export interface ActiveQuoteDisplay {
|
|
5
7
|
price: number;
|
|
@@ -45,6 +47,22 @@ export function marketStateDot(state?: MarketState): string {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export function isClosedMarketState(state?: MarketState): boolean {
|
|
51
|
+
return state === "CLOSED" || state === "PREPRE" || state === "POSTPOST";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Closed prices are final snapshots, so they should not look live or directional. */
|
|
55
|
+
export function marketPriceColor(change: number, state?: MarketState): string {
|
|
56
|
+
return isClosedMarketState(state) ? colors.textDim : priceColor(change);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Preserve a closed session's direction while visually distinguishing it from a live move. */
|
|
60
|
+
export function marketChangeColor(change: number, state?: MarketState): string {
|
|
61
|
+
const directionalColor = priceColor(change);
|
|
62
|
+
if (!isClosedMarketState(state) || change === 0) return directionalColor;
|
|
63
|
+
return blendHex(directionalColor, colors.textDim, CLOSED_CHANGE_MUTING_RATIO);
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/** Short exchange display name */
|
|
49
67
|
export function exchangeShortName(exchangeName?: string, fullExchangeName?: string): string {
|
|
50
68
|
if (!exchangeName && !fullExchangeName) return "";
|
|
@@ -147,7 +147,7 @@ function assignField(
|
|
|
147
147
|
quote: QuoteContribution | undefined,
|
|
148
148
|
): void {
|
|
149
149
|
if (!quote) return;
|
|
150
|
-
const value = (quote as Record<string, unknown>)[field];
|
|
150
|
+
const value = (quote as unknown as Record<string, unknown>)[field];
|
|
151
151
|
if (value === undefined) return;
|
|
152
152
|
(target as Record<string, unknown>)[field] = value;
|
|
153
153
|
provenance.fields ??= {};
|
|
@@ -161,7 +161,7 @@ function pickField(
|
|
|
161
161
|
candidates: QuoteContribution[],
|
|
162
162
|
): QuoteContribution | undefined {
|
|
163
163
|
for (const candidate of candidates) {
|
|
164
|
-
if ((candidate as Record<string, unknown>)[field] === undefined) continue;
|
|
164
|
+
if ((candidate as unknown as Record<string, unknown>)[field] === undefined) continue;
|
|
165
165
|
assignField(target, provenance, field, candidate);
|
|
166
166
|
return candidate;
|
|
167
167
|
}
|
|
@@ -19,17 +19,6 @@ export interface ProviderAttempt {
|
|
|
19
19
|
message?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
interface ProviderResult<T> {
|
|
23
|
-
status: ProviderStatus;
|
|
24
|
-
data: T | null;
|
|
25
|
-
providerId: string;
|
|
26
|
-
latencyMs: number;
|
|
27
|
-
completeness: "full" | "partial";
|
|
28
|
-
reasonCode?: ProviderReasonCode;
|
|
29
|
-
asOf?: number;
|
|
30
|
-
staleAt?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
type QueryPhase = "idle" | "loading" | "ready" | "refreshing" | "error";
|
|
34
23
|
|
|
35
24
|
export interface QueryEntry<T> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MarketDataRequestContext } from "../types/data-provider";
|
|
2
2
|
import type { Quote, TickerFinancials } from "../types/financials";
|
|
3
|
-
import type { InstrumentRef,
|
|
3
|
+
import type { InstrumentRef, OptionsRequest, SecFilingsRequest, ChartRequest } from "./request-types";
|
|
4
4
|
import type { QueryEntry } from "./result-types";
|
|
5
5
|
import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
|
|
6
6
|
import { normalizePriceHistory } from "../utils/price-history";
|
|
@@ -61,10 +61,6 @@ export function buildChartKey(request: ChartRequest): string {
|
|
|
61
61
|
].join(":");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function buildNewsKey(request: NewsRequest): string {
|
|
65
|
-
return `news:${buildInstrumentKey(request.instrument)}:${request.count ?? 50}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
64
|
export function buildOptionsKey(request: OptionsRequest): string {
|
|
69
65
|
return `options:${buildInstrumentKey(request.instrument)}:${request.expirationDate ?? "default"}`;
|
|
70
66
|
}
|
package/src/news/aggregator.ts
CHANGED
|
@@ -58,7 +58,11 @@ export class NewsService {
|
|
|
58
58
|
if (this.pollTimer !== null) {
|
|
59
59
|
void this.pollActiveQueries();
|
|
60
60
|
}
|
|
61
|
-
return () =>
|
|
61
|
+
return () => {
|
|
62
|
+
if (this.sources.get(source.id) === source) {
|
|
63
|
+
this.unregister(source.id);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
unregister(sourceId: string): void {
|
|
@@ -3,24 +3,6 @@ import { cloneLayout, createDefaultConfig, createPaneInstance, DEFAULT_PORTFOLIO
|
|
|
3
3
|
import { deletePaneSetting, getPaneSettingValue, setPaneSetting } from "./pane-settings";
|
|
4
4
|
|
|
5
5
|
describe("pane settings helpers", () => {
|
|
6
|
-
test("seeds built-in default panes with persisted settings", () => {
|
|
7
|
-
const config = createDefaultConfig("/tmp/gloomberb-pane-settings");
|
|
8
|
-
const portfolioPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
9
|
-
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
10
|
-
|
|
11
|
-
expect(portfolioPane?.settings).toMatchObject({
|
|
12
|
-
collectionScope: "all",
|
|
13
|
-
viewMode: "table",
|
|
14
|
-
});
|
|
15
|
-
expect(portfolioPane?.settings?.columnIds).toEqual(DEFAULT_PORTFOLIO_COLUMN_IDS);
|
|
16
|
-
expect(detailPane?.settings).toEqual({
|
|
17
|
-
chartRangePreset: "5Y",
|
|
18
|
-
chartResolution: "auto",
|
|
19
|
-
hideTabs: false,
|
|
20
|
-
lockedTabId: "overview",
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
6
|
test("cloneLayout deep-clones pane settings", () => {
|
|
25
7
|
const layout = cloneLayout(createDefaultConfig("/tmp/gloomberb-pane-settings").layout);
|
|
26
8
|
const cloned = cloneLayout(layout);
|