@gloom-sh/terminal 0.18.5 → 0.19.0
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/patches/electrobun@1.18.1.patch +140 -7
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +17 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +11 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/types.ts +70 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/market.ts +72 -26
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +61 -12
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +55 -34
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +8 -0
- package/src/cli/glossary.ts +25 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +180 -52
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +86 -3
- package/src/cli/pane-functions/function-help.ts +13 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +49 -14
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +22 -11
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +71 -137
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +336 -77
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +24 -6
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +33 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +27 -3
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +44 -3
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/financials.ts +32 -1
- package/src/types/headless.ts +21 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -1129,6 +1129,19 @@ export const zhCN: Record<string, string> = {
|
|
|
1129
1129
|
"{symbol}'s figures are not collected yet; the next read stores them.": "尚未采集 {symbol} 的数据;下次读取时会保存。",
|
|
1130
1130
|
"{symbol} files with the SEC as a trust or pool, not as an open-end fund, and its sponsor's site is not one Gloom reads.": "{symbol} 在 SEC 登记为信托或资金池,而非开放式基金,且 Gloom 不读取其发起人的网站。",
|
|
1131
1131
|
"No figures for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets list none for it yet.": "暂无 {symbol} 的数据:Gloom 不读取其发起人的网站,SEC 的招募说明书和 N-PORT 数据集中也尚未列出。",
|
|
1132
|
+
"est. {range}": "估计 {range}",
|
|
1133
|
+
"Shares outstanding": "流通份额",
|
|
1134
|
+
"1M flows": "1个月资金流",
|
|
1135
|
+
"Structure": "结构",
|
|
1136
|
+
"Closed-end fund": "封闭式基金",
|
|
1137
|
+
"BDC": "BDC",
|
|
1138
|
+
"est.": "估计",
|
|
1139
|
+
"{count} shares": "{count} 份",
|
|
1140
|
+
"No figures for {symbol}: it files with the SEC as a closed-end fund or BDC, but neither the filings Gloom reads nor its sponsor's site state any for it yet.": "暂无 {symbol} 的数据:它以封闭式基金或 BDC 身份向 SEC 申报,但 Gloom 读取的申报文件和其发起人的网站均尚未列出任何数据。",
|
|
1141
|
+
"No NAV for {symbol}: {publisher} publishes figures for it, but no NAV.": "暂无 {symbol} 的资产净值:{publisher} 公布了其数据,但未公布资产净值。",
|
|
1142
|
+
"No NAV for {symbol}: its SEC filings state net assets but no NAV per share, and its sponsor's NAV is not one Gloom reads.": "暂无 {symbol} 的资产净值:其 SEC 申报文件列出了净资产,但未列出每股资产净值,Gloom 也不读取其发起人公布的资产净值。",
|
|
1143
|
+
"No NAV for {symbol}: its sponsor's site is not one Gloom reads, and a trust files no NAV with the SEC's fund data sets.": "暂无 {symbol} 的资产净值:Gloom 不读取其发起人的网站,信托也不在 SEC 的基金数据集中申报资产净值。",
|
|
1144
|
+
"No NAV for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets state no NAV.": "暂无 {symbol} 的资产净值:Gloom 不读取其发起人的网站,SEC 的招募说明书和 N-PORT 数据集中也没有资产净值。",
|
|
1132
1145
|
"No figures for {symbol}: it lists as a fund, but the SEC's lists do not name it yet and its sponsor's site is not one Gloom reads.": "暂无 {symbol} 的数据:它以基金身份上市,但 SEC 的名单尚未收录,且 Gloom 不读取其发起人的网站。",
|
|
1133
1146
|
"No fund facts for {symbol}: it is not on the SEC's lists of US-listed funds.": "{symbol} 没有基金概况:它不在 SEC 的美国上市基金名单中。",
|
|
1134
1147
|
"No fund facts for {symbol} yet.": "{symbol} 暂无基金概况。",
|
|
@@ -1474,8 +1487,11 @@ export const zhCN: Record<string, string> = {
|
|
|
1474
1487
|
"Answers cite filings, calls and news.": "回答引用公告、电话会议和新闻。",
|
|
1475
1488
|
"{ticker} is open. {shortcut}, then AP, adds more.": "{ticker} 已打开。{shortcut} 然后输入 AP 可添加更多。",
|
|
1476
1489
|
"Later: {shortcut}, then AP.": "之后:{shortcut},然后输入 AP。",
|
|
1477
|
-
"Enter: next field, then add
|
|
1478
|
-
"
|
|
1490
|
+
"Enter: next field, then add": "Enter:下一字段,最后添加",
|
|
1491
|
+
"Add one ticker to continue": "添加一个代码即可继续",
|
|
1492
|
+
"Add one ticker to continue.": "添加一个代码即可继续。",
|
|
1493
|
+
"Esc, then Enter: continue": "Esc,然后 Enter:继续",
|
|
1494
|
+
"Enter to add a ticker": "按 Enter 添加代码",
|
|
1479
1495
|
" already added": " 已添加",
|
|
1480
1496
|
"Credentials stay on this device.": "凭据仅保存在此设备上。",
|
|
1481
1497
|
"Ask Gloom": "Ask Gloom",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -1129,6 +1129,19 @@ export const zhTW: Record<string, string> = {
|
|
|
1129
1129
|
"{symbol}'s figures are not collected yet; the next read stores them.": "尚未蒐集 {symbol} 的數據;下次讀取時會儲存。",
|
|
1130
1130
|
"{symbol} files with the SEC as a trust or pool, not as an open-end fund, and its sponsor's site is not one Gloom reads.": "{symbol} 在 SEC 登記為信託或資金池,而非開放式基金,且 Gloom 不讀取其發起人的網站。",
|
|
1131
1131
|
"No figures for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets list none for it yet.": "暫無 {symbol} 的數據:Gloom 不讀取其發起人的網站,SEC 的公開說明書與 N-PORT 資料集中也尚未列出。",
|
|
1132
|
+
"est. {range}": "估計 {range}",
|
|
1133
|
+
"Shares outstanding": "流通單位數",
|
|
1134
|
+
"1M flows": "1個月資金流",
|
|
1135
|
+
"Structure": "結構",
|
|
1136
|
+
"Closed-end fund": "封閉式基金",
|
|
1137
|
+
"BDC": "BDC",
|
|
1138
|
+
"est.": "估計",
|
|
1139
|
+
"{count} shares": "{count} 單位",
|
|
1140
|
+
"No figures for {symbol}: it files with the SEC as a closed-end fund or BDC, but neither the filings Gloom reads nor its sponsor's site state any for it yet.": "暫無 {symbol} 的數據:它以封閉式基金或 BDC 身分向 SEC 申報,但 Gloom 讀取的申報文件與其發起人的網站均尚未列出任何數據。",
|
|
1141
|
+
"No NAV for {symbol}: {publisher} publishes figures for it, but no NAV.": "暫無 {symbol} 的資產淨值:{publisher} 公布了其數據,但未公布資產淨值。",
|
|
1142
|
+
"No NAV for {symbol}: its SEC filings state net assets but no NAV per share, and its sponsor's NAV is not one Gloom reads.": "暫無 {symbol} 的資產淨值:其 SEC 申報文件列出了淨資產,但未列出每股資產淨值,Gloom 也不讀取其發起人公布的資產淨值。",
|
|
1143
|
+
"No NAV for {symbol}: its sponsor's site is not one Gloom reads, and a trust files no NAV with the SEC's fund data sets.": "暫無 {symbol} 的資產淨值:Gloom 不讀取其發起人的網站,信託也不在 SEC 的基金資料集中申報資產淨值。",
|
|
1144
|
+
"No NAV for {symbol}: its sponsor's site is not one Gloom reads, and the SEC's prospectus and N-PORT data sets state no NAV.": "暫無 {symbol} 的資產淨值:Gloom 不讀取其發起人的網站,SEC 的公開說明書與 N-PORT 資料集中也沒有資產淨值。",
|
|
1132
1145
|
"No figures for {symbol}: it lists as a fund, but the SEC's lists do not name it yet and its sponsor's site is not one Gloom reads.": "暫無 {symbol} 的數據:它以基金身分上市,但 SEC 的名單尚未收錄,且 Gloom 不讀取其發起人的網站。",
|
|
1133
1146
|
"No fund facts for {symbol}: it is not on the SEC's lists of US-listed funds.": "{symbol} 沒有基金概況:它不在 SEC 的美國上市基金名單中。",
|
|
1134
1147
|
"No fund facts for {symbol} yet.": "{symbol} 暫無基金概況。",
|
|
@@ -1474,8 +1487,11 @@ export const zhTW: Record<string, string> = {
|
|
|
1474
1487
|
"Answers cite filings, calls and news.": "回答引用申報、法說會和新聞。",
|
|
1475
1488
|
"{ticker} is open. {shortcut}, then AP, adds more.": "{ticker} 已開啟。{shortcut} 然後輸入 AP 可新增更多。",
|
|
1476
1489
|
"Later: {shortcut}, then AP.": "之後:{shortcut},然後輸入 AP。",
|
|
1477
|
-
"Enter: next field, then add
|
|
1478
|
-
"
|
|
1490
|
+
"Enter: next field, then add": "Enter:下一欄位,最後新增",
|
|
1491
|
+
"Add one ticker to continue": "新增一個代號即可繼續",
|
|
1492
|
+
"Add one ticker to continue.": "新增一個代號即可繼續。",
|
|
1493
|
+
"Esc, then Enter: continue": "Esc,然後 Enter:繼續",
|
|
1494
|
+
"Enter to add a ticker": "按 Enter 新增代號",
|
|
1479
1495
|
" already added": " 已新增",
|
|
1480
1496
|
"Credentials stay on this device.": "憑證僅保存在此裝置上。",
|
|
1481
1497
|
"Ask Gloom": "Ask Gloom",
|
|
@@ -41,9 +41,9 @@ export function ratioCount(ratio: number): string {
|
|
|
41
41
|
return Number.isInteger(ratio) ? String(ratio) : String(Number(ratio.toPrecision(6)));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** What a home line says of its receipt: `1 GGAL ADR = 10 of these shares
|
|
44
|
+
/** What a home line says of its receipt: `1 GGAL ADR = 10 of these shares`, and `1 of these shares` for one. */
|
|
45
45
|
function homeLineRatioText(pair: DepositaryPair): string {
|
|
46
|
-
return `1 ${pair.receipt} ADR = ${ratioCount(pair.ratio)} of these
|
|
46
|
+
return `1 ${pair.receipt} ADR = ${ratioCount(pair.ratio)} of these shares`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { apiClient } from "../api-client";
|
|
2
|
+
import type { FundFactsPayload, FundFlows } from "../api-client/fund-facts";
|
|
3
|
+
import { isBondFactsShape } from "./bond-funds";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A fund facts answer as every reader takes it (the ETF pane, DES, CMP,
|
|
7
|
+
* `ticker`, `fundamentals`): each figure dated and published, the parts a
|
|
8
|
+
* server before them leaves out still readable, anything malformed refused.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fact = (value: unknown) => value === null || (typeof value === "object" && value !== null && "value" in value && "source" in value);
|
|
12
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
13
|
+
|
|
14
|
+
function isMetalHoldingsShape(value: FundFactsPayload["metalHoldings"]): boolean {
|
|
15
|
+
return value == null || ((value.metal === "gold" || value.metal === "silver") && typeof value.asOf === "string" && finite(value.ounces)
|
|
16
|
+
&& (value.tonnes === null || finite(value.tonnes)) && typeof value.source?.publisher === "string"
|
|
17
|
+
&& (value.change === null || (typeof value.change?.since === "string" && finite(value.change.ounces))));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Absent from servers before it; otherwise a dated factor with its reset period. */
|
|
21
|
+
const leverageShape = (value: FundFactsPayload["leverage"]) => value === undefined || value === null
|
|
22
|
+
|| (fact(value) && typeof value.value === "number" && Number.isFinite(value.value) && (value.reset === "daily" || value.reset === "monthly"));
|
|
23
|
+
|
|
24
|
+
/** A NAV's estimate range, when the sponsor published one: absent, or two ends in order. */
|
|
25
|
+
const estimateShape = (nav: FundFactsPayload["nav"]) => !nav?.estimate
|
|
26
|
+
|| (finite(nav.estimate.low) && finite(nav.estimate.high) && nav.estimate.low <= nav.estimate.high);
|
|
27
|
+
|
|
28
|
+
/** Absent from servers before them; otherwise a dated count, and a change between two dated counts. */
|
|
29
|
+
const sharesShape = (value: FundFactsPayload["sharesOutstanding"]) => value == null || (fact(value) && finite(value.value));
|
|
30
|
+
const flowsShape = (value: FundFlows | null | undefined) => value == null
|
|
31
|
+
|| (finite(value.shares) && (value.value === null || finite(value.value)) && typeof value.from === "string" && typeof value.asOf === "string");
|
|
32
|
+
|
|
33
|
+
export function validateFundFacts(data: FundFactsPayload): FundFactsPayload {
|
|
34
|
+
if (!data || typeof data.ticker !== "string" || typeof data.covered !== "boolean"
|
|
35
|
+
|| ![data.expenseRatio, data.nav, data.premiumDiscount, data.netAssets, data.sponsor, data.inception, data.index].every(fact)
|
|
36
|
+
|| !estimateShape(data.nav) || !sharesShape(data.sharesOutstanding) || !flowsShape(data.flows)
|
|
37
|
+
|| !isBondFactsShape(data.bond) || !leverageShape(data.leverage) || !isMetalHoldingsShape(data.metalHoldings)) {
|
|
38
|
+
throw new Error("Unreadable fund facts response");
|
|
39
|
+
}
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `GET /cloud/fund-facts/:ticker`, read and checked. */
|
|
44
|
+
export async function readFundFacts(
|
|
45
|
+
symbol: string,
|
|
46
|
+
client: Pick<typeof apiClient, "getFundFacts"> = apiClient,
|
|
47
|
+
signal?: AbortSignal,
|
|
48
|
+
): Promise<FundFactsPayload> {
|
|
49
|
+
return validateFundFacts(await (signal ? client.getFundFacts(symbol, { signal }) : client.getFundFacts(symbol)));
|
|
50
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Quote } from "../../types/financials";
|
|
|
4
4
|
import { canonicalExchange } from "../../utils/exchanges";
|
|
5
5
|
import { isFiniteNumber } from "../../utils/guards";
|
|
6
6
|
import { formatUtcDayTime } from "../../utils/utc-time";
|
|
7
|
+
import { fxPrintTradingDay } from "../../utils/fx-market-hours";
|
|
7
8
|
import { zonedDateKey } from "../../utils/zoned-date-time";
|
|
8
9
|
import { futuresChangeBasis, futuresSettleText, isFuturesInstrument } from "../futures-contract";
|
|
9
10
|
import { quoteTradingDay } from "../quotes/day-range";
|
|
@@ -94,6 +95,18 @@ function isCurrencyPair(quote: Quote): boolean {
|
|
|
94
95
|
|| kindFromListingSyntax(quote.symbol, quote.listingExchangeName || quote.exchangeName) === "currency";
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The trading day a quote describes (quoteTradingDay); for a currency pair,
|
|
100
|
+
* whose venue has no clock, the FX trading day of its print, which turns at
|
|
101
|
+
* 17:00 New York, so every pair on a board is dated alike. Null for a pair's
|
|
102
|
+
* print made while FX is shut, which belongs to no trading day.
|
|
103
|
+
*/
|
|
104
|
+
function tradingDayOf(quote: Quote): string | null {
|
|
105
|
+
const day = quoteTradingDay(quote);
|
|
106
|
+
if (day || !isCurrencyPair(quote) || !Number.isFinite(quote.lastUpdated) || quote.lastUpdated <= 0) return day;
|
|
107
|
+
return fxPrintTradingDay(quote.lastUpdated);
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
/** A coin on the spot crypto venue: its day is the UTC day. */
|
|
98
111
|
function isSpotCoin(quote: Quote): boolean {
|
|
99
112
|
return quoteVenue(quote) === "CCC";
|
|
@@ -154,7 +167,7 @@ export function isChangeBasisDatedWithoutCloses(quote: Quote, now = Date.now()):
|
|
|
154
167
|
* nothing dates it.
|
|
155
168
|
*/
|
|
156
169
|
export function changeBasisSessionDate(quote: Quote): string | null {
|
|
157
|
-
const date = regularSessionDisplayDate(quote) ??
|
|
170
|
+
const date = regularSessionDisplayDate(quote) ?? tradingDayOf(quote);
|
|
158
171
|
return date && ISO_DATE.test(date) ? date : null;
|
|
159
172
|
}
|
|
160
173
|
|
|
@@ -179,7 +192,7 @@ export function quoteChangeBasis(
|
|
|
179
192
|
if (!display || !isFiniteNumber(display.change)) return null;
|
|
180
193
|
if (quote.changeBasis === "24h" || isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return { kind: "24h" };
|
|
181
194
|
if (isSpotCoin(quote)) return { kind: "utc day" };
|
|
182
|
-
const sessionDate = (session === "active" ? quote.changeSessionDate : regularSessionDisplayDate(quote)) ??
|
|
195
|
+
const sessionDate = (session === "active" ? quote.changeSessionDate : regularSessionDisplayDate(quote)) ?? tradingDayOf(quote);
|
|
183
196
|
if (isFuturesInstrument(quote.symbol, quote.instrumentType)) {
|
|
184
197
|
const basis = futuresChangeBasis(terms ?? { settlement: null }, { ...display, sessionDate }, quoteVenue(quote));
|
|
185
198
|
return basis ? { kind: basis.kind, date: basis.session } : { kind: "previous close" };
|
|
@@ -211,7 +224,7 @@ export function quoteSettleText(
|
|
|
211
224
|
export function quotePreviousCloseDate(quote: Quote | null | undefined): string | null {
|
|
212
225
|
if (!quote || !isFiniteNumber(quote.previousClose) || quote.changeBasis != null) return null;
|
|
213
226
|
if (isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return null;
|
|
214
|
-
const sessionDate = quote.changeSessionDate ??
|
|
227
|
+
const sessionDate = quote.changeSessionDate ?? tradingDayOf(quote);
|
|
215
228
|
if (!sessionDate || !ISO_DATE.test(sessionDate)) return null;
|
|
216
229
|
if (isSpotCoin(quote)) return dayBefore(sessionDate);
|
|
217
230
|
if (isFuturesInstrument(quote.symbol, quote.instrumentType)) {
|
|
@@ -628,6 +628,31 @@ function currencyMarks(currency: string, normalizedCurrency: string, code = fals
|
|
|
628
628
|
return code ? { prefix: "", suffix: ` ${normalizedCurrency}` } : { prefix: getCurrencySymbol(normalizedCurrency), suffix: "" };
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
/**
|
|
632
|
+
* A London line shown in pounds and served in the pence it trades in. The
|
|
633
|
+
* code is compared in its own case: `GBp` (and `GBX`) is a price already in
|
|
634
|
+
* pence, which says so itself (630.8p) and needs no pence figure beside it.
|
|
635
|
+
*/
|
|
636
|
+
export function isPenceQuotedLine(quote: Pick<Quote, "currency" | "providerPriceDivisor"> | null | undefined): boolean {
|
|
637
|
+
return quote?.currency?.trim() === "GBP" && quote.providerPriceDivisor === 100;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* A London line's price in the pence it trades in, as the exchange and its
|
|
642
|
+
* announcements write it: `630.8p` for £6.308, with the decimals of a penny
|
|
643
|
+
* the pounds are shown to (`poundDecimals`) beyond two. Null for a price not
|
|
644
|
+
* quoted in pence.
|
|
645
|
+
*/
|
|
646
|
+
export function penceFigure(
|
|
647
|
+
value: number | null | undefined,
|
|
648
|
+
quote: Pick<Quote, "currency" | "providerPriceDivisor"> | null | undefined,
|
|
649
|
+
poundDecimals: number,
|
|
650
|
+
): string | null {
|
|
651
|
+
if (value == null || !Number.isFinite(value) || !isPenceQuotedLine(quote)) return null;
|
|
652
|
+
const digits = Math.max(0, Math.min(4, poundDecimals - 2));
|
|
653
|
+
return `${getNumberFormatter(digits, digits, true).format(value * 100)}${minorUnitSign("GBp")}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
631
656
|
export function formatMarketPriceWithCurrency(
|
|
632
657
|
value: number | undefined,
|
|
633
658
|
currency = "USD",
|
|
@@ -32,7 +32,8 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
|
|
|
32
32
|
TSXV: 9 * 60 + 30,
|
|
33
33
|
CSE: 9 * 60 + 30,
|
|
34
34
|
FWB2: 8 * 60,
|
|
35
|
-
|
|
35
|
+
// Continuous trading opens at 09:00 Berlin time, after the opening auction; before it is pre-trading.
|
|
36
|
+
XETRA: 9 * 60,
|
|
36
37
|
LSE: 8 * 60,
|
|
37
38
|
EPA: 9 * 60,
|
|
38
39
|
AMS: 9 * 60,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRoundTheClockCoin } from "../utils/crypto-pair";
|
|
2
|
+
import { currencyPairCode, isCurrencyCode } from "../utils/currency-pair";
|
|
2
3
|
import { parsePublicTickerKey } from "../utils/exchanges";
|
|
3
4
|
import { parseFuturesGeneric } from "../utils/futures-generic";
|
|
4
5
|
|
|
@@ -34,23 +35,24 @@ const FUTURES_ALTERNATIVES: Readonly<Record<string, ListedOptionsAlternative>> =
|
|
|
34
35
|
ZN: { symbol: "IEF", name: "iShares 7-10 Year Treasury Bond ETF" },
|
|
35
36
|
"6E": { symbol: "FXE", name: "Invesco CurrencyShares Euro Trust" },
|
|
36
37
|
"6J": { symbol: "FXY", name: "Invesco CurrencyShares Japanese Yen Trust" },
|
|
38
|
+
"6B": { symbol: "FXB", name: "Invesco CurrencyShares British Pound Sterling Trust" },
|
|
37
39
|
BTC: { symbol: "IBIT", name: "iShares Bitcoin Trust" },
|
|
38
40
|
ETH: { symbol: "ETHA", name: "iShares Ethereum Trust" },
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
const MONTH_CODE_CONTRACT = /^([A-Z0-9]{1,3}?)[FGHJKMNQUVXZ]\d{1,4}$/;
|
|
42
44
|
|
|
43
|
-
/** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. */
|
|
44
|
-
function futuresRoot(symbol: string): string | null {
|
|
45
|
+
/** The futures root a symbol names: GC=F, GCZ26 and the generic GC1 all give GC. `known` says which roots a bare contract may carry. */
|
|
46
|
+
function futuresRoot(symbol: string, known: (root: string) => boolean = (root) => !!FUTURES_ALTERNATIVES[root]): string | null {
|
|
45
47
|
const ticker = symbol.trim().toUpperCase().replace(/[.:][A-Z]+$/, "");
|
|
46
48
|
if (ticker.endsWith("=F")) {
|
|
47
49
|
const body = ticker.slice(0, -2);
|
|
48
|
-
return
|
|
50
|
+
return known(body) ? body : MONTH_CODE_CONTRACT.exec(body)?.[1] ?? body;
|
|
49
51
|
}
|
|
50
52
|
const generic = parseFuturesGeneric(ticker);
|
|
51
53
|
if (generic) return generic.root;
|
|
52
54
|
const contract = MONTH_CODE_CONTRACT.exec(ticker)?.[1];
|
|
53
|
-
return contract &&
|
|
55
|
+
return contract && known(contract) ? contract : null;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** The listed fund whose options stand in for an instrument with no chain, if there is one. */
|
|
@@ -127,3 +129,78 @@ export function coinOptionsAlias(code: string) {
|
|
|
127
129
|
return pair ? { symbol: pair, note: `${key} read as ${pair}, the coin's options. For the US fund that trades as ${key}: gloomberb fn ${code} ${key}:ARCA` } : null;
|
|
128
130
|
};
|
|
129
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The CME currency futures, each as the pair it settles on, in the order the
|
|
135
|
+
* pair is quoted: 6E is EUR/USD, 6J USD/JPY. No chain of options on any of
|
|
136
|
+
* them is served here, as none is on the spot pair.
|
|
137
|
+
*/
|
|
138
|
+
const FX_FUTURES_PAIRS: Readonly<Record<string, string>> = {
|
|
139
|
+
"6E": "EURUSD", "6J": "USDJPY", "6B": "GBPUSD", "6A": "AUDUSD", "6C": "USDCAD", "6S": "USDCHF", "6N": "NZDUSD", "6M": "USDMXN",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** The US-listed currency funds whose options stand in for an FX pair's volatility, by the currency they hold. */
|
|
143
|
+
const FX_VOL_FUNDS: Readonly<Record<string, string>> = { EUR: "FXE", JPY: "FXY", GBP: "FXB" };
|
|
144
|
+
/** The fund that holds the dollar against a basket, for a pair none of the single-currency funds holds. */
|
|
145
|
+
const DOLLAR_FUND = "UUP";
|
|
146
|
+
const FX_VOL_FUND_LIST = [...Object.values(FX_VOL_FUNDS), DOLLAR_FUND];
|
|
147
|
+
|
|
148
|
+
/** The pair a symbol is: a spot pair in any spelling (EURUSD=X, EUR/USD), or a currency future (6E=F, 6EZ26, 6E1) as the pair it settles on; null for anything else. */
|
|
149
|
+
export function fxVolatilityPair(symbol: string, exchange?: string): string | null {
|
|
150
|
+
const spot = currencyPairCode(symbol, exchange);
|
|
151
|
+
// Metals quoted against a currency (XAUUSD=X) are no currency pair.
|
|
152
|
+
if (spot) return isCurrencyCode(spot.slice(0, 3)) && isCurrencyCode(spot.slice(3)) ? spot : null;
|
|
153
|
+
const root = futuresRoot(symbol, (candidate) => !!FX_FUTURES_PAIRS[candidate]);
|
|
154
|
+
return root ? FX_FUTURES_PAIRS[root] ?? null : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The listed fund that stands in for an FX pair's volatility: the one that
|
|
159
|
+
* holds the pair's euro, yen or pound, else the dollar index fund.
|
|
160
|
+
*/
|
|
161
|
+
function fxVolatilityFunds(pair: string): string[] {
|
|
162
|
+
const held = [pair.slice(0, 3), pair.slice(3)].map((code) => FX_VOL_FUNDS[code]).filter((fund): fund is string => !!fund);
|
|
163
|
+
return held.length > 0 ? held : FX_VOL_FUND_LIST;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* What exists instead for the volatility of an FX pair or currency future,
|
|
168
|
+
* worded as one line for the CLI: OTC FX volatility is not served, the
|
|
169
|
+
* currency funds' options are the nearest listed thing (the one that holds
|
|
170
|
+
* the pair's euro, yen or pound, else any of them), and HVT reads the pair's
|
|
171
|
+
* realized volatility. Null for anything that is no FX pair. `code` is the
|
|
172
|
+
* function asked (OVDV), `symbol` as typed.
|
|
173
|
+
*/
|
|
174
|
+
function fxVolatilityAnswer(code: string, symbol: string): string | null {
|
|
175
|
+
const pair = fxVolatilityPair(symbol);
|
|
176
|
+
if (!pair) return null;
|
|
177
|
+
const typed = symbol.trim().toUpperCase();
|
|
178
|
+
const funds = fxVolatilityFunds(pair);
|
|
179
|
+
// VCA screens every fund; the others read the pair's own, or the dollar's when it has none.
|
|
180
|
+
const read = code === "VCA" ? funds.join(",") : funds === FX_VOL_FUND_LIST ? DOLLAR_FUND : funds[0]!;
|
|
181
|
+
return `OTC FX volatility is not covered for ${typed}. Currency-ETF options are the listed proxy: `
|
|
182
|
+
+ `${funds.join(", ")} (gloom fn ${code} ${read}). Realized volatility: gloom fn HVT ${typed}.`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The same for a pane, which has no commands to give: what is not covered,
|
|
187
|
+
* the funds whose options stand in, and HVT for realized volatility.
|
|
188
|
+
*/
|
|
189
|
+
export function fxVolatilityNote(symbol: string, exchange?: string): string | null {
|
|
190
|
+
const pair = fxVolatilityPair(symbol, exchange);
|
|
191
|
+
if (!pair) return null;
|
|
192
|
+
return `OTC FX volatility is not covered for ${symbol.trim().toUpperCase()}. Currency-ETF options are the listed proxy: `
|
|
193
|
+
+ `${fxVolatilityFunds(pair).join(", ")}. HVT shows the pair's realized volatility.`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A volatility function's `refuseSymbol` for FX (OVDV, VCA, HIVG): an FX pair
|
|
198
|
+
* or currency future is answered before any request, with one line saying
|
|
199
|
+
* OTC FX volatility is not covered and what to read instead.
|
|
200
|
+
*/
|
|
201
|
+
export function fxVolatilityRefusal(code: string) {
|
|
202
|
+
return (key: string): { message: string; details?: string } | null => {
|
|
203
|
+
const message = fxVolatilityAnswer(code, key);
|
|
204
|
+
return message ? { message } : null;
|
|
205
|
+
};
|
|
206
|
+
}
|
package/src/news/aggregator.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logFailure } from "../utils/log-failure";
|
|
2
2
|
import type { NewsCapability } from "../capabilities";
|
|
3
3
|
import type { ConnectionHealthRegistry } from "../core/connection-health";
|
|
4
|
-
import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
|
|
4
|
+
import type { NewsAccess, NewsArticle, NewsQuery, NewsQueryState } from "./types";
|
|
5
5
|
import { isAppVisible, subscribeAppVisibility } from "../state/app/activity";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_GLOBAL_QUERY,
|
|
@@ -28,6 +28,18 @@ export interface NewsServiceOptions {
|
|
|
28
28
|
connectionHealth?: ConnectionHealthRegistry;
|
|
29
29
|
/** Defaults to the app's visibility; injectable for tests. */
|
|
30
30
|
visibility?: { isVisible(): boolean; subscribe(listener: () => void): () => void };
|
|
31
|
+
/**
|
|
32
|
+
* The session the sources read news with. Its key changes when what the
|
|
33
|
+
* service answers changes: a session restored at launch, a sign-in or
|
|
34
|
+
* sign-out, an upgrade. Every open query is then read again and its stories
|
|
35
|
+
* replaced, so a feed read without the session is not left on screen.
|
|
36
|
+
*/
|
|
37
|
+
session?: NewsSessionSignal;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NewsSessionSignal {
|
|
41
|
+
key(): string;
|
|
42
|
+
subscribe(listener: () => void): () => void;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
export type NewsQueryListener = (state: NewsQueryState) => void;
|
|
@@ -50,17 +62,30 @@ interface SourceFetchResult {
|
|
|
50
62
|
failedSourceIds: string[];
|
|
51
63
|
errors: string[];
|
|
52
64
|
nextCursor: string | null;
|
|
65
|
+
access: NewsAccess | null;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
interface NewsQueryEntry {
|
|
56
69
|
query: NewsQuery;
|
|
57
70
|
state: NewsQueryState;
|
|
58
71
|
inFlight: Promise<NewsQueryState> | null;
|
|
72
|
+
/** The session generation the read in flight went out under. */
|
|
73
|
+
inFlightGeneration: number;
|
|
59
74
|
loadMoreInFlight: Promise<void> | null;
|
|
75
|
+
loadMoreGeneration: number;
|
|
76
|
+
/** The session generation the stories on screen were read under. */
|
|
77
|
+
generation: number;
|
|
60
78
|
refs: number;
|
|
61
79
|
lastAccessedAt: number;
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/** Held back beats live: one source the service held back is enough to say the list is not the live wire. */
|
|
83
|
+
function combineNewsAccess(values: ReadonlyArray<NewsAccess | null | undefined>): NewsAccess | null {
|
|
84
|
+
if (values.includes("delayed")) return "delayed";
|
|
85
|
+
if (values.includes("realtime")) return "realtime";
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
function newsCapabilityPriority(source: NewsCapability): number {
|
|
65
90
|
return source.priority ?? 1000;
|
|
66
91
|
}
|
|
@@ -85,6 +110,12 @@ export class NewsService {
|
|
|
85
110
|
private readonly visibility: NonNullable<NewsServiceOptions["visibility"]>;
|
|
86
111
|
private lastPollAt = 0;
|
|
87
112
|
private unsubscribeVisibility: (() => void) | null = null;
|
|
113
|
+
private readonly session?: NewsSessionSignal;
|
|
114
|
+
private sessionKey: string | null = null;
|
|
115
|
+
/** Bumped when the session changes; a read from an older generation is dropped. */
|
|
116
|
+
private sessionGeneration = 0;
|
|
117
|
+
private sessionRefetchQueued = false;
|
|
118
|
+
private unsubscribeSession: (() => void) | null = null;
|
|
88
119
|
|
|
89
120
|
constructor(options: NewsServiceOptions = {}) {
|
|
90
121
|
const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -94,6 +125,7 @@ export class NewsService {
|
|
|
94
125
|
this.now = options.now ?? Date.now;
|
|
95
126
|
this.connectionHealth = options.connectionHealth;
|
|
96
127
|
this.visibility = options.visibility ?? { isVisible: isAppVisible, subscribe: subscribeAppVisibility };
|
|
128
|
+
this.session = options.session;
|
|
97
129
|
}
|
|
98
130
|
|
|
99
131
|
register(source: NewsCapability): () => void {
|
|
@@ -125,6 +157,10 @@ export class NewsService {
|
|
|
125
157
|
this.clearPollTimer();
|
|
126
158
|
this.scheduleNextPoll();
|
|
127
159
|
});
|
|
160
|
+
if (this.session) {
|
|
161
|
+
this.sessionKey = this.session.key();
|
|
162
|
+
this.unsubscribeSession = this.session.subscribe(() => this.handleSessionChange());
|
|
163
|
+
}
|
|
128
164
|
this.scheduleNextPoll();
|
|
129
165
|
}
|
|
130
166
|
|
|
@@ -132,9 +168,46 @@ export class NewsService {
|
|
|
132
168
|
this.polling = false;
|
|
133
169
|
this.unsubscribeVisibility?.();
|
|
134
170
|
this.unsubscribeVisibility = null;
|
|
171
|
+
this.unsubscribeSession?.();
|
|
172
|
+
this.unsubscribeSession = null;
|
|
135
173
|
this.clearPollTimer();
|
|
136
174
|
}
|
|
137
175
|
|
|
176
|
+
/**
|
|
177
|
+
* A new session answers with other news: the live wire once a Pro session
|
|
178
|
+
* is in place, the held-back one after signing out. Reads still out under
|
|
179
|
+
* the old session are dropped, open queries are read again and replace
|
|
180
|
+
* their stories rather than merge into them, and closed ones are forgotten.
|
|
181
|
+
* A token and its user arrive as separate changes, so the reads wait for
|
|
182
|
+
* the end of the tick and go out once.
|
|
183
|
+
*/
|
|
184
|
+
private handleSessionChange(): void {
|
|
185
|
+
if (!this.session) return;
|
|
186
|
+
const key = this.session.key();
|
|
187
|
+
if (key === this.sessionKey) return;
|
|
188
|
+
this.sessionKey = key;
|
|
189
|
+
this.sessionGeneration++;
|
|
190
|
+
if (this.sessionRefetchQueued) return;
|
|
191
|
+
this.sessionRefetchQueued = true;
|
|
192
|
+
queueMicrotask(() => {
|
|
193
|
+
this.sessionRefetchQueued = false;
|
|
194
|
+
if (!this.polling) return;
|
|
195
|
+
let dropped = false;
|
|
196
|
+
for (const [queryKey, entry] of this.queries) {
|
|
197
|
+
if (entry.refs > 0) continue;
|
|
198
|
+
this.queries.delete(queryKey);
|
|
199
|
+
dropped = true;
|
|
200
|
+
}
|
|
201
|
+
if (dropped) {
|
|
202
|
+
this.rebuildArticlePool();
|
|
203
|
+
this.notify();
|
|
204
|
+
}
|
|
205
|
+
for (const entry of this.queries.values()) {
|
|
206
|
+
void this.refreshQuery(entry.query, true);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
138
211
|
private pollInFlight = false;
|
|
139
212
|
|
|
140
213
|
private clearPollTimer(): void {
|
|
@@ -226,6 +299,7 @@ export class NewsService {
|
|
|
226
299
|
return;
|
|
227
300
|
}
|
|
228
301
|
|
|
302
|
+
const generation = this.sessionGeneration;
|
|
229
303
|
const request = (async () => {
|
|
230
304
|
entry.state = { ...entry.state, loadingMore: true };
|
|
231
305
|
this.notify();
|
|
@@ -234,6 +308,12 @@ export class NewsService {
|
|
|
234
308
|
...normalized,
|
|
235
309
|
cursor: entry.state.nextCursor ?? undefined,
|
|
236
310
|
});
|
|
311
|
+
// An older page from another session must not join the list the new session read.
|
|
312
|
+
if (generation !== this.sessionGeneration || entry.generation !== generation) {
|
|
313
|
+
entry.state = { ...entry.state, loadingMore: false };
|
|
314
|
+
this.notify();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
237
317
|
entry.state = {
|
|
238
318
|
...entry.state,
|
|
239
319
|
loadingMore: false,
|
|
@@ -253,6 +333,7 @@ export class NewsService {
|
|
|
253
333
|
}
|
|
254
334
|
})();
|
|
255
335
|
entry.loadMoreInFlight = request;
|
|
336
|
+
entry.loadMoreGeneration = generation;
|
|
256
337
|
await request;
|
|
257
338
|
}
|
|
258
339
|
|
|
@@ -318,7 +399,9 @@ export class NewsService {
|
|
|
318
399
|
showLoading: boolean,
|
|
319
400
|
): Promise<NewsQueryState> {
|
|
320
401
|
const entry = this.getOrCreateQueryEntry(query);
|
|
321
|
-
|
|
402
|
+
const generation = this.sessionGeneration;
|
|
403
|
+
// A read that went out under an older session answers for that session: a new one goes out beside it.
|
|
404
|
+
if (entry.inFlight && entry.inFlightGeneration === generation) return entry.inFlight;
|
|
322
405
|
|
|
323
406
|
const current = entry.state;
|
|
324
407
|
if (showLoading) {
|
|
@@ -333,14 +416,17 @@ export class NewsService {
|
|
|
333
416
|
const promise = (async () => {
|
|
334
417
|
try {
|
|
335
418
|
const result = await this.fetchFromSources(query);
|
|
419
|
+
if (generation !== this.sessionGeneration) return this.currentRead(entry);
|
|
336
420
|
if (result.sourceIds.length === 0 && result.failedSourceIds.length > 0) {
|
|
337
421
|
throw new Error("News sources unavailable.");
|
|
338
422
|
}
|
|
339
|
-
if (entry.loadMoreInFlight) return entry.state;
|
|
423
|
+
if (entry.loadMoreInFlight && entry.loadMoreGeneration === generation) return entry.state;
|
|
340
424
|
const incoming = filterNewsArticlesForQuery(dedupeNewsArticles(result.articles), query);
|
|
425
|
+
// Stories read under another session are another feed (held back, or live): the new read replaces them.
|
|
426
|
+
const sameSession = entry.generation === generation;
|
|
341
427
|
// The stories a coin's fallback found are not the pair's own: once stories are filed under it, they replace them.
|
|
342
|
-
const existing = incoming.length > 0 && entry.state.articles.length > 0
|
|
343
|
-
&& (incoming[0]!.searchedFor ?? null) !== (entry.state.articles[0]!.searchedFor ?? null)
|
|
428
|
+
const existing = !sameSession || (incoming.length > 0 && entry.state.articles.length > 0
|
|
429
|
+
&& (incoming[0]!.searchedFor ?? null) !== (entry.state.articles[0]!.searchedFor ?? null))
|
|
344
430
|
? []
|
|
345
431
|
: entry.state.articles;
|
|
346
432
|
const incomingIds = new Set(incoming.map((article) => article.id));
|
|
@@ -363,13 +449,17 @@ export class NewsService {
|
|
|
363
449
|
sourceIds: result.sourceIds,
|
|
364
450
|
nextCursor: hasOlderPages ? entry.state.nextCursor : result.nextCursor,
|
|
365
451
|
loadingMore: entry.state.loadingMore,
|
|
452
|
+
// A page that does not say keeps what this session's earlier answer said.
|
|
453
|
+
access: result.access ?? (sameSession ? entry.state.access ?? null : null),
|
|
366
454
|
};
|
|
367
455
|
entry.state = state;
|
|
456
|
+
entry.generation = generation;
|
|
368
457
|
entry.lastAccessedAt = this.now();
|
|
369
458
|
this.rebuildArticlePool();
|
|
370
459
|
this.notify();
|
|
371
460
|
return state;
|
|
372
461
|
} catch (error) {
|
|
462
|
+
if (generation !== this.sessionGeneration) return this.currentRead(entry);
|
|
373
463
|
const state: NewsQueryState = {
|
|
374
464
|
...current,
|
|
375
465
|
phase: "error",
|
|
@@ -380,15 +470,21 @@ export class NewsService {
|
|
|
380
470
|
this.notify();
|
|
381
471
|
return state;
|
|
382
472
|
} finally {
|
|
383
|
-
entry.inFlight = null;
|
|
473
|
+
if (entry.inFlightGeneration === generation) entry.inFlight = null;
|
|
384
474
|
this.pruneInactiveQueries();
|
|
385
475
|
}
|
|
386
476
|
})();
|
|
387
477
|
|
|
388
478
|
entry.inFlight = promise;
|
|
479
|
+
entry.inFlightGeneration = generation;
|
|
389
480
|
return promise;
|
|
390
481
|
}
|
|
391
482
|
|
|
483
|
+
/** What a dropped read answers its caller: the current session's read when one is out, else the state as it is. */
|
|
484
|
+
private currentRead(entry: NewsQueryEntry): Promise<NewsQueryState> | NewsQueryState {
|
|
485
|
+
return entry.inFlight && entry.inFlightGeneration === this.sessionGeneration ? entry.inFlight : entry.state;
|
|
486
|
+
}
|
|
487
|
+
|
|
392
488
|
private getOrCreateQueryEntry(query: NewsQuery): NewsQueryEntry {
|
|
393
489
|
const key = buildNewsQueryKey(query);
|
|
394
490
|
const now = this.now();
|
|
@@ -402,7 +498,10 @@ export class NewsService {
|
|
|
402
498
|
query,
|
|
403
499
|
state: createIdleNewsQueryState(),
|
|
404
500
|
inFlight: null,
|
|
501
|
+
inFlightGeneration: this.sessionGeneration,
|
|
405
502
|
loadMoreInFlight: null,
|
|
503
|
+
loadMoreGeneration: this.sessionGeneration,
|
|
504
|
+
generation: this.sessionGeneration,
|
|
406
505
|
refs: 0,
|
|
407
506
|
lastAccessedAt: now,
|
|
408
507
|
};
|
|
@@ -451,7 +550,7 @@ export class NewsService {
|
|
|
451
550
|
private async readSourcePage(
|
|
452
551
|
source: NewsCapability,
|
|
453
552
|
query: NewsQuery,
|
|
454
|
-
): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null }> {
|
|
553
|
+
): Promise<{ articles: NewsArticle[]; nextCursor: string | null; error?: string | null; access?: NewsAccess | null }> {
|
|
455
554
|
if (source.provider.fetchNewsPage) {
|
|
456
555
|
const page = await this.trackSourceRequest(
|
|
457
556
|
source,
|
|
@@ -462,6 +561,7 @@ export class NewsService {
|
|
|
462
561
|
articles: page.articles.map((article) => markDetailCapableArticle(source, article)),
|
|
463
562
|
nextCursor: page.nextCursor ?? null,
|
|
464
563
|
error: page.error,
|
|
564
|
+
access: page.access,
|
|
465
565
|
};
|
|
466
566
|
}
|
|
467
567
|
const articles = (await this.trackSourceRequest(
|
|
@@ -484,6 +584,7 @@ export class NewsService {
|
|
|
484
584
|
failedSourceIds,
|
|
485
585
|
errors: page.error ? [page.error] : [],
|
|
486
586
|
nextCursor: page.nextCursor,
|
|
587
|
+
access: page.access ?? null,
|
|
487
588
|
};
|
|
488
589
|
if (page.articles.length > 0) return result;
|
|
489
590
|
firstEmpty ??= result;
|
|
@@ -491,7 +592,7 @@ export class NewsService {
|
|
|
491
592
|
failedSourceIds.push(newsCapabilitySourceId(source));
|
|
492
593
|
}
|
|
493
594
|
}
|
|
494
|
-
return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds, errors: [], nextCursor: null };
|
|
595
|
+
return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds, errors: [], nextCursor: null, access: null };
|
|
495
596
|
}
|
|
496
597
|
|
|
497
598
|
private async fetchMergedNews(query: NewsQuery, sources: NewsCapability[]): Promise<SourceFetchResult> {
|
|
@@ -505,6 +606,7 @@ export class NewsService {
|
|
|
505
606
|
const errors: string[] = [];
|
|
506
607
|
const sourceIds: string[] = [];
|
|
507
608
|
const failedSourceIds: string[] = [];
|
|
609
|
+
const access: Array<NewsAccess | null | undefined> = [];
|
|
508
610
|
let nextCursor: string | null = null;
|
|
509
611
|
settled.forEach((result, index) => {
|
|
510
612
|
if (result.status !== "fulfilled") {
|
|
@@ -515,9 +617,10 @@ export class NewsService {
|
|
|
515
617
|
articles.push(...result.value.page.articles);
|
|
516
618
|
if (result.value.page.error) errors.push(result.value.page.error);
|
|
517
619
|
sourceIds.push(newsCapabilitySourceId(result.value.source));
|
|
620
|
+
access.push(result.value.page.access);
|
|
518
621
|
nextCursor ??= result.value.page.nextCursor;
|
|
519
622
|
});
|
|
520
|
-
return { articles, sourceIds, failedSourceIds, errors, nextCursor };
|
|
623
|
+
return { articles, sourceIds, failedSourceIds, errors, nextCursor, access: combineNewsAccess(access) };
|
|
521
624
|
}
|
|
522
625
|
|
|
523
626
|
private trackSourceRequest<T>(
|
|
@@ -550,6 +653,7 @@ export class NewsService {
|
|
|
550
653
|
sourceIds: [...new Set([...entry.state.sourceIds, newsCapabilitySourceId(source)])],
|
|
551
654
|
nextCursor: entry.state.nextCursor,
|
|
552
655
|
loadingMore: false,
|
|
656
|
+
access: entry.state.access ?? null,
|
|
553
657
|
};
|
|
554
658
|
changed = true;
|
|
555
659
|
}
|