gloomberb 0.19.0 → 0.19.2
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 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -70,19 +70,23 @@ async function routeAnswers(route: FreeRoute, check: NonNullable<FreeRoute["answ
|
|
|
70
70
|
* symbol the routes read `<ticker>` and the first is named; without a way to
|
|
71
71
|
* read data, only a route that answers for every symbol is.
|
|
72
72
|
*/
|
|
73
|
-
async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<FreeRoute | null> {
|
|
73
|
+
async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<{ route: FreeRoute | null; noneHasData: boolean }> {
|
|
74
74
|
const { symbol, context } = lock;
|
|
75
|
-
if (!symbol) return routes[0] ?? null;
|
|
75
|
+
if (!symbol) return { route: routes[0] ?? null, noneHasData: false };
|
|
76
76
|
// Routes past the first that needs no check are never named, so they are not checked.
|
|
77
77
|
const unchecked = routes.findIndex((route) => !route.answers);
|
|
78
78
|
const candidates = unchecked < 0 ? routes : routes.slice(0, unchecked + 1);
|
|
79
79
|
const deadline = Date.now() + ROUTE_CHECK_WAIT_MS;
|
|
80
80
|
const checks = candidates.map((route) => !route.answers ? Promise.resolve(true)
|
|
81
|
-
: context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(
|
|
81
|
+
: context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(null));
|
|
82
|
+
const answered: Array<boolean | null> = [];
|
|
82
83
|
for (const [index, check] of checks.entries()) {
|
|
83
|
-
|
|
84
|
+
const answer = await check;
|
|
85
|
+
if (answer) return { route: candidates[index]!, noneHasData: false };
|
|
86
|
+
answered.push(answer);
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
// Every check answered no: a failed or stalled one is not known to.
|
|
89
|
+
return { route: null, noneHasData: candidates.length > 0 && answered.every((answer) => answer === false) };
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -114,6 +118,14 @@ export async function proLockNote(
|
|
|
114
118
|
* command repeats the ticker only when the reader gave one. Default `ticker`.
|
|
115
119
|
*/
|
|
116
120
|
argument?: "ticker" | "optional" | "none";
|
|
121
|
+
/**
|
|
122
|
+
* For a function that reads what its free routes read (RISK the SEC's
|
|
123
|
+
* 10-Ks and 20-Fs, FLOW the listing's options): the sentence the note
|
|
124
|
+
* says when every route answered that the symbol has none. The note then
|
|
125
|
+
* names no command for the symbol, which would read nothing once unlocked
|
|
126
|
+
* either: IAG:LSE files neither form, and LHA:XETRA has no options served.
|
|
127
|
+
*/
|
|
128
|
+
noneFor?: (ticker: string) => string;
|
|
117
129
|
},
|
|
118
130
|
): Promise<string> {
|
|
119
131
|
const { gate, symbol, token: typed, verifyEmail } = lock;
|
|
@@ -129,12 +141,15 @@ export async function proLockNote(
|
|
|
129
141
|
const unlock = gate === "pro"
|
|
130
142
|
? `To read ${pane.shows} in ${token}, upgrade in the app (gloom, then Ctrl+P, UPGRADE)`
|
|
131
143
|
: `To read ${pane.shows} in ${token}, open the app (gloom), ${account}${pro ? ", then upgrade (Ctrl+P, UPGRADE)" : ""}`;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
144
|
+
const { route: free, noneHasData } = pane.free ? await answeringFreeRoute(lock, pane.free) : { route: null, noneHasData: false };
|
|
145
|
+
// Unlocking reads nothing for a symbol none of the function's own data covers: it is named only where it has some.
|
|
146
|
+
const uncovered = noneHasData && !!pane.noneFor;
|
|
147
|
+
const bare = pane.argument === "none" || (pane.argument === "optional" && (!symbol || uncovered));
|
|
148
|
+
const command = bare ? `gloom fn ${token}` : `gloom fn ${token} ${uncovered ? "<ticker>" : ticker}`;
|
|
135
149
|
return [
|
|
136
150
|
`${token} ${what}: ${pane.does}. ${without} it shows none of ${pane.shows}.`,
|
|
137
151
|
...free ? [`Free, with no sign-in: ${free.text(ticker, symbol ?? "the company")}`] : [],
|
|
152
|
+
...uncovered ? [pane.noneFor!(ticker)] : [],
|
|
138
153
|
`${unlock}; ${command} then reads with that session.`,
|
|
139
154
|
].join("\n");
|
|
140
155
|
}
|
|
@@ -47,6 +47,14 @@ export function barResolutionFromDates(dates: readonly (string | number | Date)[
|
|
|
47
47
|
/** Company filings read from EDGAR. Old filings are not stale; there is no schedule to miss. */
|
|
48
48
|
export const SEC_FILINGS: HeadlessPaneFreshness = { source: "SEC EDGAR", status: "not-a-feed", basis: "filed data", oldest: null };
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A list of filings, dated by its newest one's filing date and by when it was
|
|
52
|
+
* read: `newest filing Tue 27 Oct 2026 · retrieved Wed 28 Oct 2026 19:07 UTC`.
|
|
53
|
+
*/
|
|
54
|
+
export const SEC_FILING_ROWS: HeadlessPaneFreshness = {
|
|
55
|
+
...SEC_FILINGS, observedKey: "filingDate", asOfLabel: "newest filing", retrievedToo: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
50
58
|
/**
|
|
51
59
|
* What a filings view opened inside a pane that hosts others (Issuer Debt in
|
|
52
60
|
* Fixed Income) declares as its template's `reportFreshness`, so a capture
|
|
@@ -14,7 +14,7 @@ import { displayWidth } from "../../../utils/format";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/** The one placeholder for a value the source does not give. */
|
|
17
|
-
const NO_VALUE = "--";
|
|
17
|
+
export const NO_VALUE = "--";
|
|
18
18
|
|
|
19
19
|
export const missingCell = (colors: ThemeColors): DataTableCell => ({ text: NO_VALUE, value: null, color: colors.textMuted });
|
|
20
20
|
|
|
@@ -74,9 +74,9 @@ export function createSeriesCache(kind: string, staleMs: number): SeriesCache {
|
|
|
74
74
|
let generation = 0;
|
|
75
75
|
let persistence: PluginPersistence | null = null;
|
|
76
76
|
const withFailure = <T extends SeriesCacheLoadResult>(key: string, result: T): T => {
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
? { ...result, stale: true, source: "stale-fallback", refreshError:
|
|
77
|
+
// A failure is one whatever it said: an Error without a message still leaves the copy a saved one.
|
|
78
|
+
return failures.has(key)
|
|
79
|
+
? { ...result, stale: true, source: "stale-fallback", refreshError: failures.get(key) }
|
|
80
80
|
: result;
|
|
81
81
|
};
|
|
82
82
|
const loadEntry: SeriesCache["loadEntry"] = async (key, loader, options) => {
|
|
@@ -103,7 +103,7 @@ export function createSeriesCache(kind: string, staleMs: number): SeriesCache {
|
|
|
103
103
|
if (owner === generation && requests.get(key) === request) requests.delete(key);
|
|
104
104
|
}
|
|
105
105
|
}, options);
|
|
106
|
-
return withFailure(key, { ...data, fetchedAt, stale, source, ...(
|
|
106
|
+
return withFailure(key, { ...data, fetchedAt, stale, source, ...(source === "stale-fallback" ? { refreshError } : {}) });
|
|
107
107
|
};
|
|
108
108
|
const reset = () => {
|
|
109
109
|
generation += 1;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* What a loaded catalog series says of an old copy. A copy that answered
|
|
3
|
+
* because its refresh failed carries when it was saved (`savedAt`), which the
|
|
4
|
+
* chart's footer shows as "saved copy from …", the note every pane gives it.
|
|
5
|
+
* A cached copy the source has not updated since is a note on the chart.
|
|
6
|
+
*/
|
|
7
|
+
export function cachedSeriesNote(resource: { stale?: boolean; refreshError?: string | null; savedAt?: number | null }): {
|
|
8
|
+
warning?: string;
|
|
9
|
+
savedAt?: number;
|
|
10
|
+
} {
|
|
11
|
+
if (resource.savedAt != null && Number.isFinite(resource.savedAt)) return { savedAt: resource.savedAt };
|
|
12
|
+
if (resource.refreshError) return { warning: `Refresh failed (${resource.refreshError}); showing cached data.` };
|
|
13
|
+
return resource.stale ? { warning: "Showing cached data; the source has not updated since." } : {};
|
|
5
14
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadShortVolume } from "./client";
|
|
4
4
|
|
|
5
5
|
/** The share of a ticker's daily on-exchange volume reported as short sales. */
|
|
@@ -14,10 +14,9 @@ export const SHORT_VOLUME_SERIES: readonly SeriesDefinition[] = [{
|
|
|
14
14
|
input: "ticker",
|
|
15
15
|
load: async ({ ticker }) => {
|
|
16
16
|
const resource = await loadShortVolume(ticker!, "nms");
|
|
17
|
-
const warning = cachedSeriesWarning(resource);
|
|
18
17
|
return {
|
|
19
18
|
points: resource.payload.history.map((point) => ({ date: point.date, value: point.ratioPercent })),
|
|
20
|
-
...(
|
|
19
|
+
...cachedSeriesNote(resource),
|
|
21
20
|
};
|
|
22
21
|
},
|
|
23
22
|
}];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { unavailableText } from "../../../components/ui/status";
|
|
2
2
|
import { apiClient } from "../../../api-client";
|
|
3
|
-
import {
|
|
3
|
+
import { isAccessDenied } from "../../../api-client/errors";
|
|
4
4
|
import type { SocialMentionDayPosts, SocialMentionPost, SocialMentionsPayload, SocialMentionsRange } from "../../../api-client/social-mentions";
|
|
5
|
-
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
|
+
import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
|
|
6
6
|
import { CLOUD_SESSION_REQUIRED } from "../shared/research-cloud-session";
|
|
7
|
+
import { unavailableOnServer } from "../shared/cloud-resource";
|
|
7
8
|
|
|
8
9
|
export const socialMentionsCache = createPluginCache<SocialMentionsPayload>({
|
|
9
10
|
kind: "social-mentions", source: "gloom-cloud", schemaVersion: 1,
|
|
@@ -56,12 +57,15 @@ export function validateSocialMentions(payload: SocialMentionsPayload, symbol: s
|
|
|
56
57
|
export async function fetchSocialMentions(symbol: string, range: SocialMentionsRange = "1y",
|
|
57
58
|
client: Pick<typeof apiClient, "getCloudSocialMentions"> = apiClient): Promise<SocialMentionsPayload> {
|
|
58
59
|
try { return validateSocialMentions(await client.getCloudSocialMentions(symbol, range), symbol, range); }
|
|
59
|
-
catch (error) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
catch (error) { throw unavailableOnServer(error, unavailableText("social mentions")); }
|
|
61
|
+
}
|
|
62
|
+
export interface SocialMentionsResource {
|
|
63
|
+
payload: SocialMentionsPayload;
|
|
64
|
+
stale: boolean;
|
|
65
|
+
refreshError: string | null;
|
|
66
|
+
/** When the copy shown was saved, when it is one because its refresh failed. */
|
|
67
|
+
savedAt?: number;
|
|
63
68
|
}
|
|
64
|
-
export interface SocialMentionsResource { payload: SocialMentionsPayload; stale: boolean; refreshError: string | null }
|
|
65
69
|
const cacheKey = (symbol: string, range: SocialMentionsRange) => `${range}:${symbol}`;
|
|
66
70
|
export function cachedSocialMentions(symbol: string, range: SocialMentionsRange): SocialMentionsResource | null {
|
|
67
71
|
const cached = socialMentionsCache.get(cacheKey(symbol, range), { allowExpired: true });
|
|
@@ -70,9 +74,10 @@ export function cachedSocialMentions(symbol: string, range: SocialMentionsRange)
|
|
|
70
74
|
catch { return null; }
|
|
71
75
|
}
|
|
72
76
|
export async function loadSocialMentions(symbol: string, range: SocialMentionsRange, force = false): Promise<SocialMentionsResource> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null
|
|
77
|
+
// A refusal (a signed-out session, any other 4xx but 408 and 429) or a reply that fails validation shows as itself.
|
|
78
|
+
const result = savedCopyForTransientFailure(await socialMentionsCache.load(cacheKey(symbol, range), () => fetchSocialMentions(symbol, range), { force }));
|
|
79
|
+
return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null,
|
|
80
|
+
...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}) };
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadSocialMentions } from "./client";
|
|
4
4
|
|
|
5
5
|
/** How often a ticker is posted about each day, over the last year. */
|
|
@@ -14,10 +14,9 @@ export const SOCIAL_MENTION_SERIES: readonly SeriesDefinition[] = [{
|
|
|
14
14
|
style: "columns",
|
|
15
15
|
load: async ({ ticker }) => {
|
|
16
16
|
const resource = await loadSocialMentions(ticker!, "1y");
|
|
17
|
-
const warning = cachedSeriesWarning(resource);
|
|
18
17
|
return {
|
|
19
18
|
points: resource.payload.x.days.map((row) => ({ date: row.day, value: row.mentions })),
|
|
20
|
-
...(
|
|
19
|
+
...cachedSeriesNote(resource),
|
|
21
20
|
};
|
|
22
21
|
},
|
|
23
22
|
}];
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from "./types";
|
|
11
11
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
12
12
|
import { apiClient } from "../../../api-client";
|
|
13
|
-
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
13
|
+
import { ApiRequestError, isPermanentClientError, isTransientFailure } from "../../../api-client/errors";
|
|
14
14
|
import { httpFetch } from "../../../utils/http-transport";
|
|
15
15
|
import { asRecord, finiteOrNull } from "../../../utils/guards";
|
|
16
16
|
import { logFailure } from "../../../utils/log-failure";
|
|
@@ -30,7 +30,14 @@ const FORMS_13F_CACHE_POLICY = {
|
|
|
30
30
|
} as const;
|
|
31
31
|
|
|
32
32
|
export interface Forms13FReadOptions {
|
|
33
|
+
/** A part of the answer that could not be read, said beside what was. */
|
|
33
34
|
onWarning?: (warning: string) => void;
|
|
35
|
+
/**
|
|
36
|
+
* A read answered from the copy saved at `savedAt` (ms) because its refresh
|
|
37
|
+
* failed. Only a caller that says when its copy was saved (the footer's
|
|
38
|
+
* "saved copy from", a command's closing line) is answered from one.
|
|
39
|
+
*/
|
|
40
|
+
onSavedCopy?: (savedAt: number) => void;
|
|
34
41
|
forceRefresh?: boolean;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -150,18 +157,17 @@ async function fetchForms13F<T>(
|
|
|
150
157
|
} catch (error) {
|
|
151
158
|
options.signal?.throwIfAborted();
|
|
152
159
|
if (current()) failedRefreshes.add(key);
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
// A saved copy answers only for a failure asking again may get past; a refusal or a reply that
|
|
161
|
+
// fails validation is the error, and a missing or denied resource also drops its copy.
|
|
162
|
+
if (!isTransientFailure(error)) {
|
|
163
|
+
if (isPermanentClientError(error) && current()) store?.deleteResource(CACHE_KIND, key, { sourceKey: CACHE_SOURCE });
|
|
155
164
|
throw error;
|
|
156
165
|
}
|
|
157
166
|
const retained = options.cache !== false
|
|
158
167
|
? store?.getResource<T>(CACHE_KIND, key, { ...cacheOptions, allowExpired: true }) : null;
|
|
159
|
-
// A caller may retain old research only when it can
|
|
160
|
-
if (!retained || !options.
|
|
161
|
-
|
|
162
|
-
const age = Number.isFinite(retrieved.getTime()) ? `retrieved ${retrieved.toISOString()}` : "with unavailable retrieval time";
|
|
163
|
-
const reason = (error instanceof Error ? error.message : String(error)).trim() || "Request failed";
|
|
164
|
-
options.onWarning(`${key}: refresh failed: ${reason}. Retained data ${age}.`);
|
|
168
|
+
// A caller may retain old research only when it can say when it was saved.
|
|
169
|
+
if (!retained || !options.onSavedCopy || !Number.isFinite(retained.fetchedAt)) throw error;
|
|
170
|
+
options.onSavedCopy(retained.fetchedAt);
|
|
165
171
|
return retained.value;
|
|
166
172
|
} finally {
|
|
167
173
|
if (activeRequests.get(key) === slot.promise) activeRequests.delete(key);
|
|
@@ -106,6 +106,8 @@ export interface BrowserLoadResult {
|
|
|
106
106
|
period?: string;
|
|
107
107
|
quarter?: string;
|
|
108
108
|
warning?: string;
|
|
109
|
+
/** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
|
|
110
|
+
savedAt?: number;
|
|
109
111
|
hasMore?: boolean;
|
|
110
112
|
nextOffset?: number;
|
|
111
113
|
reportsLoaded?: Promise<void>;
|
|
@@ -115,14 +117,34 @@ export async function loadBrowserRows(
|
|
|
115
117
|
tab: ThirteenFBrowserTab,
|
|
116
118
|
query: string,
|
|
117
119
|
signal?: AbortSignal,
|
|
118
|
-
options:
|
|
120
|
+
options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { offset?: number; limit?: number } = {},
|
|
119
121
|
): Promise<BrowserLoadResult> {
|
|
120
122
|
const warnings: string[] = [];
|
|
121
|
-
const result
|
|
123
|
+
const page: { result?: BrowserLoadResult; savedAt?: number } = {};
|
|
124
|
+
const onSavedCopy = (savedAt: number) => {
|
|
125
|
+
page.savedAt = Math.min(page.savedAt ?? savedAt, savedAt);
|
|
126
|
+
// The rows' reports answer after the page does, and their copies date it too.
|
|
127
|
+
if (page.result) page.result.savedAt = page.savedAt;
|
|
128
|
+
options.onSavedCopy?.(savedAt);
|
|
129
|
+
};
|
|
130
|
+
const result = await loadBrowserPage(tab, query, signal, { ...options, onSavedCopy, onWarning: warning => warnings.push(warning) });
|
|
131
|
+
page.result = result;
|
|
132
|
+
if (page.savedAt != null) result.savedAt = page.savedAt;
|
|
122
133
|
if (warnings.length) result.warning = [...new Set([result.warning, ...warnings].filter(Boolean))].join(" ") || undefined;
|
|
123
134
|
return result;
|
|
124
135
|
}
|
|
125
136
|
|
|
137
|
+
/** The oldest saved copy a load answered from (`savedAt`), each also passed on to `onSavedCopy`. */
|
|
138
|
+
function savedCopies(onSavedCopy?: (savedAt: number) => void) {
|
|
139
|
+
const copies: { savedAt?: number; note: (savedAt: number) => void } = {
|
|
140
|
+
note: (savedAt) => {
|
|
141
|
+
copies.savedAt = Math.min(copies.savedAt ?? savedAt, savedAt);
|
|
142
|
+
onSavedCopy?.(savedAt);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return copies;
|
|
146
|
+
}
|
|
147
|
+
|
|
126
148
|
async function loadBrowserPage(
|
|
127
149
|
tab: ThirteenFBrowserTab,
|
|
128
150
|
query: string,
|
|
@@ -136,13 +158,13 @@ async function loadBrowserPage(
|
|
|
136
158
|
const offset = Math.max(0, options.offset ?? 0);
|
|
137
159
|
const browserLimit = Math.max(1, options.limit ?? BROWSER_PAGE_LIMIT);
|
|
138
160
|
const latestLimit = Math.max(1, options.limit ?? LATEST_FILINGS_PAGE_LIMIT);
|
|
139
|
-
const apiOptions = { forceRefresh: options.forceRefresh, onWarning: options.onWarning };
|
|
161
|
+
const apiOptions = { forceRefresh: options.forceRefresh, onWarning: options.onWarning, onSavedCopy: options.onSavedCopy };
|
|
140
162
|
const pageApiOptions = { ...apiOptions, offset };
|
|
141
163
|
|
|
142
164
|
if (tab === "performance") {
|
|
143
165
|
const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
|
|
144
166
|
const quarters = priorQuarters(quarter);
|
|
145
|
-
const history = await loadRankingHistory(quarters, signal,
|
|
167
|
+
const history = await loadRankingHistory(quarters, signal, apiOptions);
|
|
146
168
|
const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
|
|
147
169
|
const rows = buildBrowserRows({ topFunds, source: "performance" }).map(row => ({ ...row, priorReturns: quarters.map((previous, index) => ({ quarter: previous, value: history[index]?.get(row.cik) ?? null })) }));
|
|
148
170
|
const result: BrowserLoadResult = {
|
|
@@ -437,10 +459,11 @@ export async function loadFundPerformance(
|
|
|
437
459
|
cik: string,
|
|
438
460
|
fallbackName: string,
|
|
439
461
|
signal?: AbortSignal,
|
|
440
|
-
options:
|
|
462
|
+
options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { splitFactor?: SplitFactorLookup } = {},
|
|
441
463
|
): Promise<FundPerformanceResult> {
|
|
442
464
|
const warnings: string[] = [];
|
|
443
|
-
const
|
|
465
|
+
const saved = savedCopies(options.onSavedCopy);
|
|
466
|
+
const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
|
|
444
467
|
const detail = await loadFundDetail(cik, fallbackName, signal, options);
|
|
445
468
|
const period = detail.latestForm?.periodOfReport;
|
|
446
469
|
const quarter = period ? periodToQuarter(period) : null;
|
|
@@ -482,7 +505,9 @@ export async function loadFundPerformance(
|
|
|
482
505
|
: filed?.skipped.length ? `No share position held through the quarter to ${period} that could be priced across it.`
|
|
483
506
|
: `No share position held through the quarter to ${period}.`;
|
|
484
507
|
const warning = [...new Set([...(detail.warnings ?? []), ...warnings])].join(" ") || undefined;
|
|
508
|
+
const savedAt = Math.min(detail.savedAt ?? Infinity, saved.savedAt ?? Infinity);
|
|
485
509
|
return {
|
|
510
|
+
...(Number.isFinite(savedAt) ? { savedAt } : {}),
|
|
486
511
|
rows: [row], ...(quarter ? { quarter } : {}), ...(period ? { period } : {}), hasMore: false, basis, ranking,
|
|
487
512
|
...(filed?.splits.length ? { splits: filed.splits } : {}),
|
|
488
513
|
...(filed?.skipped.length ? { skipped: filed.skipped } : {}),
|
|
@@ -496,11 +521,12 @@ export async function loadFundDetail(
|
|
|
496
521
|
fallbackName: string,
|
|
497
522
|
signal?: AbortSignal,
|
|
498
523
|
/** `filingGap: false` skips EDGAR's filing list, for a load that never shows it (an overlap). */
|
|
499
|
-
options:
|
|
524
|
+
options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { filingGap?: boolean } = {},
|
|
500
525
|
): Promise<FundDetailData> {
|
|
501
526
|
const now = new Date();
|
|
502
527
|
const warnings: string[] = [];
|
|
503
|
-
const
|
|
528
|
+
const saved = savedCopies(options.onSavedCopy);
|
|
529
|
+
const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
|
|
504
530
|
|
|
505
531
|
const forms = await listThirteenFForms(
|
|
506
532
|
cik,
|
|
@@ -554,6 +580,7 @@ export async function loadFundDetail(
|
|
|
554
580
|
previousReport,
|
|
555
581
|
warnings,
|
|
556
582
|
filingGap,
|
|
583
|
+
...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}),
|
|
557
584
|
};
|
|
558
585
|
}
|
|
559
586
|
|
|
@@ -562,12 +589,13 @@ export async function loadFilingPositions(
|
|
|
562
589
|
accessionNumber: string,
|
|
563
590
|
signal?: AbortSignal,
|
|
564
591
|
options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
|
|
565
|
-
): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[] }> {
|
|
592
|
+
): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[]; savedAt?: number }> {
|
|
566
593
|
const warnings: string[] = [];
|
|
594
|
+
const saved = savedCopies();
|
|
567
595
|
const result = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
|
|
568
|
-
...options, onWarning: warning => warnings.push(warning),
|
|
596
|
+
...options, onSavedCopy: saved.note, onWarning: warning => warnings.push(warning),
|
|
569
597
|
});
|
|
570
|
-
return { ...result, warnings };
|
|
598
|
+
return { ...result, warnings, ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}) };
|
|
571
599
|
}
|
|
572
600
|
|
|
573
601
|
/** `2026Q2` for a quarter end such as 2026-06-30. */
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
HeadlessShotState,
|
|
7
7
|
} from "../../../types/plugin";
|
|
8
8
|
import { formatShortDate } from "../../../utils/datetime-format";
|
|
9
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
9
10
|
import { normalizeCik, searchThirteenFFunds, TOP_FUNDS_MAX_LIMIT } from "./api";
|
|
10
11
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
11
12
|
import { REPORT_PAGE, REPORT_ROWS } from "../shared/headless-options";
|
|
@@ -229,15 +230,40 @@ export interface ThirteenFHeadlessDependencies {
|
|
|
229
230
|
loadEtfHoldings?(ticker: string, ctx: HeadlessPaneContext): Promise<FundMembersPayload>;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
/** The oldest copy a load's 13F reads answered from because their refresh failed, by the load's context. */
|
|
234
|
+
const savedCopies = new WeakMap<HeadlessPaneContext, { savedAt: number | null }>();
|
|
235
|
+
|
|
236
|
+
/** How a 13F read for the load `ctx` says it answered from the copy saved at `savedAt`. */
|
|
237
|
+
const noteSavedCopy = (ctx: HeadlessPaneContext) => (savedAt: number) => {
|
|
238
|
+
const entry = savedCopies.get(ctx);
|
|
239
|
+
if (entry) entry.savedAt = Math.min(entry.savedAt ?? savedAt, savedAt);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* One load, whose report ends with when the oldest saved copy it answered
|
|
244
|
+
* from was saved (`couldn't refresh; saved copy from <time>`), as every
|
|
245
|
+
* command says it, instead of listing each failed refresh as an error.
|
|
246
|
+
*/
|
|
247
|
+
async function readingSavedCopies<T extends object>(ctx: HeadlessPaneContext, run: () => Promise<T>): Promise<T> {
|
|
248
|
+
const entry: { savedAt: number | null } = { savedAt: null };
|
|
249
|
+
savedCopies.set(ctx, entry);
|
|
250
|
+
try {
|
|
251
|
+
const result = await run();
|
|
252
|
+
return entry.savedAt == null ? result : servedFromSavedCopy(result, entry.savedAt);
|
|
253
|
+
} finally {
|
|
254
|
+
if (savedCopies.get(ctx) === entry) savedCopies.delete(ctx);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
232
258
|
const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
|
|
233
259
|
loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
|
|
234
260
|
tab,
|
|
235
261
|
query,
|
|
236
262
|
ctx.signal,
|
|
237
|
-
{ limit },
|
|
263
|
+
{ limit, onSavedCopy: noteSavedCopy(ctx) },
|
|
238
264
|
),
|
|
239
265
|
// An overlap never shows a fund's missed quarter, so it does not ask EDGAR.
|
|
240
|
-
loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args) }),
|
|
266
|
+
loadDetail: (cik, name, args, ctx) => loadFundDetail(cik, name, ctx.signal, { filingGap: !isOverlapLoad(args), onSavedCopy: noteSavedCopy(ctx) }),
|
|
241
267
|
loadSplitFactors: (detail, ctx) => loadSplitFactors(
|
|
242
268
|
detail,
|
|
243
269
|
(ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
|
|
@@ -247,6 +273,7 @@ const defaultDependencies: Required<ThirteenFHeadlessDependencies> = {
|
|
|
247
273
|
searchFunds: (query, ctx) => searchThirteenFFunds(query, THIRTEENF_MAX_LIMIT, ctx.signal),
|
|
248
274
|
loadPerformance: (cik, name, ctx) => loadFundPerformance(cik, name, ctx.signal, {
|
|
249
275
|
splitFactor: (ticker, after, through) => corporateSplitFactor(ctx.marketData, ticker, after, through),
|
|
276
|
+
onSavedCopy: noteSavedCopy(ctx),
|
|
250
277
|
}),
|
|
251
278
|
loadEtfFunds: async (ctx) => (await fetchFunds(ctx.apiClient)).funds,
|
|
252
279
|
loadEtfHoldings: (ticker, ctx) => fetchMembers(ticker, ctx.apiClient),
|
|
@@ -541,7 +568,7 @@ export function createThirteenFHeadless(
|
|
|
541
568
|
],
|
|
542
569
|
describe: (args, result) => thirteenFTitle(args, result?.metadata),
|
|
543
570
|
shotState: async (args, load) => thirteenFShotState(args, (await load()).metadata ?? {}),
|
|
544
|
-
|
|
571
|
+
load: (args, ctx) => readingSavedCopies(ctx, async () => {
|
|
545
572
|
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
546
573
|
const requestedView = String(args.options.view) as HeadlessThirteenFView;
|
|
547
574
|
const compare = typeof args.options.compare === "string" ? args.options.compare.trim() : "";
|
|
@@ -920,7 +947,7 @@ export function createThirteenFHeadless(
|
|
|
920
947
|
],
|
|
921
948
|
},
|
|
922
949
|
};
|
|
923
|
-
},
|
|
950
|
+
}),
|
|
924
951
|
};
|
|
925
952
|
}
|
|
926
953
|
|
|
@@ -10,6 +10,7 @@ import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
|
10
10
|
import { useMineTickers } from "../shared/mine-tickers";
|
|
11
11
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
12
12
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
13
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
13
14
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
14
15
|
import { cachedFunds, cachedMembers, loadFunds, loadMembers } from "../members/client";
|
|
15
16
|
import { coveredFund } from "../members/model";
|
|
@@ -115,7 +116,10 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
|
|
|
115
116
|
const period = peer ? overlapPeriod(data, peer) : null;
|
|
116
117
|
const mismatch = peer && !period;
|
|
117
118
|
const listError = etf ? etfFile.error : error ?? paged.error?.message ?? paged.moreError?.message ?? null;
|
|
118
|
-
|
|
119
|
+
// Either side answered from a copy a failed refresh left says when it was saved.
|
|
120
|
+
const etfSavedAt = etf ? etfFile.data?.savedAt : null;
|
|
121
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo([data.savedAt, peer?.savedAt, etfSavedAt]), [data.savedAt, peer?.savedAt, etfSavedAt]);
|
|
122
|
+
usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, info: savedInfo, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
|
|
119
123
|
usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [
|
|
120
124
|
...(peer?.warnings ?? []),
|
|
121
125
|
...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : []),
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
DataTableStackView,
|
|
4
4
|
DataTableView,
|
|
5
|
-
EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
|
|
5
|
+
EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneFooter, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
|
|
6
6
|
type DataTableRootKeyContext, type PageRequest, type PaneFooterSegment, type PaneHint, type StatItem
|
|
7
7
|
} from "../../../components";
|
|
8
8
|
import { useShortcut } from "../../../react/input";
|
|
@@ -26,7 +26,7 @@ import { usePaneStatusFooter, usePaneStatusLinkFooter } from "../../../component
|
|
|
26
26
|
import { corporateSplitFactor, loadBrowserRows, loadFilingPositions, loadFundDetail, loadSplitFactors } from "./data";
|
|
27
27
|
import { FundOverlapView } from "./overlap-pane";
|
|
28
28
|
import { ThirteenFCrowdingPane, ThirteenFTickerHoldingsView } from "./signals-pane";
|
|
29
|
-
import { PaneFooterScope } from "../../../components/layout/pane/footer";
|
|
29
|
+
import { PaneFooterScope, savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
30
30
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
31
31
|
import { useClaimResearchTabKeys } from "../ticker-detail/research-tab-keys";
|
|
32
32
|
import {
|
|
@@ -129,6 +129,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
129
129
|
const notes = [...(pagesSettled ? browser.pages.map((page) => page.warning) : []), browser.moreError?.message];
|
|
130
130
|
return [...new Set(notes.filter(Boolean))].join(" ") || null;
|
|
131
131
|
}, [browser.moreError, browser.pages, pagesSettled, reportPaint]);
|
|
132
|
+
// A page a failed refresh answered from a saved copy says when it was saved, as every pane does.
|
|
133
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo(pagesSettled ? browser.pages.map((page) => page.savedAt) : []),
|
|
134
|
+
[browser.pages, pagesSettled, reportPaint]);
|
|
132
135
|
const [detailSeed, setDetailSeed] = useState<FundSeed | null>(() => (
|
|
133
136
|
initialCik ? { cik: String(initialCik), name: normalizedQuery || String(initialCik) } : null
|
|
134
137
|
));
|
|
@@ -166,7 +169,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
166
169
|
}, { allowEditable: true });
|
|
167
170
|
|
|
168
171
|
const browserSort = useMemo(() => browserSortFor(sortPreference, browserMode), [sortPreference, browserMode]);
|
|
169
|
-
|
|
172
|
+
// The rows' reports fill the rows in place after their page answers (from the network or a saved copy);
|
|
173
|
+
// a row the table drew before then is drawn again only as a new object.
|
|
174
|
+
const sortedRows = useMemo(() => sortBrowserRows(rows.map((row) => ({ ...row })), browserSort), [rows, browserSort, reportPaint]);
|
|
170
175
|
const columns = useMemo(() => {
|
|
171
176
|
if (browserMode !== "performance") return buildBrowserColumns(false);
|
|
172
177
|
const history = rows[0]?.priorReturns ?? [];
|
|
@@ -202,7 +207,8 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
202
207
|
const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
|
|
203
208
|
...(loadingMore ? [{ id: "loading-more", parts: [{ text: "loading more", tone: "muted" as const }] }] : []),
|
|
204
209
|
...(warning ? [{ id: "warning", parts: [{ text: warning, tone: "warning" as const }] }] : []),
|
|
205
|
-
|
|
210
|
+
...savedInfo,
|
|
211
|
+
], [loadingMore, warning, savedInfo]);
|
|
206
212
|
const searchHints = useMemo(() => [{ id: "search", key: "/", label: "search", onPress: focusSearch }], [focusSearch]);
|
|
207
213
|
usePaneStatusFooter({
|
|
208
214
|
registrationId: THIRTEENF_PANE_ID,
|
|
@@ -544,6 +550,9 @@ export function FundDetailView({
|
|
|
544
550
|
const detailHints = useMemo<PaneHint[]>(() => hasHoldingFiling
|
|
545
551
|
? [{ id: "filing", key: "f", label: "iling", title: "Open Filing", onPress: openSelectedFilingInPane }]
|
|
546
552
|
: [], [hasHoldingFiling, openSelectedFilingInPane]);
|
|
553
|
+
// An open filing and the overlap tab say when their own saved copies are from.
|
|
554
|
+
const detailSavedInfo = useMemo(() => savedCopyFooterInfo(openFiling || activeTab === "overlap" ? [] : [data?.savedAt]),
|
|
555
|
+
[activeTab, data?.savedAt, openFiling]);
|
|
547
556
|
usePaneStatusLinkFooter({
|
|
548
557
|
registrationId: "thirteenf-detail",
|
|
549
558
|
focused,
|
|
@@ -551,6 +560,7 @@ export function FundDetailView({
|
|
|
551
560
|
source: openFiling ? "SEC 13F" : null,
|
|
552
561
|
loading: activeTab !== "overlap" && status === "loading",
|
|
553
562
|
error: activeTab === "overlap" ? null : error,
|
|
563
|
+
info: detailSavedInfo,
|
|
554
564
|
showOpenHint: true,
|
|
555
565
|
hints: detailHints,
|
|
556
566
|
});
|
|
@@ -590,7 +600,7 @@ export function FundDetailView({
|
|
|
590
600
|
onBack={closeOpenFiling}
|
|
591
601
|
detailTitle={openFiling ? `${openFiling.periodOfReport} filing` : "13F filing"}
|
|
592
602
|
detailContent={openFiling ? (
|
|
593
|
-
<FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} />
|
|
603
|
+
<FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} sourceSavedAt={data?.savedAt} />
|
|
594
604
|
) : (
|
|
595
605
|
<Box flexGrow={1} />
|
|
596
606
|
)}
|
|
@@ -659,11 +669,14 @@ function FilingDetailView({
|
|
|
659
669
|
filing,
|
|
660
670
|
width,
|
|
661
671
|
sourceWarnings,
|
|
672
|
+
sourceSavedAt,
|
|
662
673
|
}: {
|
|
663
674
|
focused: boolean;
|
|
664
675
|
filing: FundTimelineRow;
|
|
665
676
|
width: number;
|
|
666
677
|
sourceWarnings: string[];
|
|
678
|
+
/** When the fund's saved copy was saved, when a failed refresh left it on screen. */
|
|
679
|
+
sourceSavedAt?: number;
|
|
667
680
|
}) {
|
|
668
681
|
const { pinTicker } = usePluginTickerActions();
|
|
669
682
|
const [sortPreference, setSortPreference] = usePluginPaneState<FundSortPreference<FilingPositionColumnId>>(
|
|
@@ -711,6 +724,9 @@ function FilingDetailView({
|
|
|
711
724
|
);
|
|
712
725
|
|
|
713
726
|
usePaneNoticeFooter({ registrationId: "thirteenf-filing-notices", notices: [...new Set([...sourceWarnings, ...warnings])], focused });
|
|
727
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo([sourceSavedAt, ...(pagesSettled ? positions.pages.map((page) => page.savedAt) : [])]),
|
|
728
|
+
[pagesSettled, positions.pages, sourceSavedAt]);
|
|
729
|
+
usePaneFooter("thirteenf-filing-saved", () => ({ info: savedInfo }), [savedInfo]);
|
|
714
730
|
|
|
715
731
|
// An untyped amendment may replace or add to the report, so only it is a caveat.
|
|
716
732
|
const kind = amendmentKind(filing);
|
|
@@ -99,6 +99,8 @@ export interface FundDetailData {
|
|
|
99
99
|
warnings?: string[];
|
|
100
100
|
/** Set when the latest report predates the newest quarter past its deadline. */
|
|
101
101
|
filingGap?: ThirteenFFilingGap | null;
|
|
102
|
+
/** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
|
|
103
|
+
savedAt?: number;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** A manager a 13F-NT names as reporting its holdings. */
|