@gloom-sh/terminal 0.18.5 → 0.19.1
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 +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- 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/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- 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/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -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/core-builtin-allowlist.json +2 -2
- 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 +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- 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/saved-copy-persistence.ts +88 -0
- 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/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- 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 +8 -21
- 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/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- 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/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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 +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- 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/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -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 +17 -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/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- 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 +34 -12
- 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/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/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/follow-binding.ts +14 -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 +5 -6
- 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/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/pane-links/commands.ts +7 -1
- 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 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- 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/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- 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/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 +10 -10
- 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/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- 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/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- 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 +2 -2
- 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/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -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 +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- 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 +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- 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/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/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/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 +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -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 +22 -4
- 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/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -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 +6 -7
- 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/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- 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/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- 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 +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- 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/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/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- 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/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 +48 -5
- 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 +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- 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/headless-market-data.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 +11 -2
- 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/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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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 +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- 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-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- 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 +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -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/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- 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/icons.tsx +7 -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 +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- 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/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- 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/create.ts +43 -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/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -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 +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- 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/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- 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-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- 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/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -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 +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- 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/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
|
+
import { readCompanyFrom } from "../../types/company-from";
|
|
2
3
|
import { exchangeRateMetadata } from "../../utils/exchange-rate-snapshot";
|
|
3
4
|
import type { ExchangeRateSnapshot } from "../../types/exchange-rate";
|
|
4
5
|
import type { TimeRange } from "../../time-series/range";
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
import type { NewsArticle, NewsQuery } from "../../types/news-source";
|
|
43
44
|
import { normalizeNewsFeed } from "../../news/news-model";
|
|
44
45
|
import { resolveCurrencyUnit } from "../../utils/currency-units";
|
|
45
|
-
import { isRoundTheClockCoin } from "../../utils/crypto-pair";
|
|
46
|
+
import { exchangeLineCoinListing, isRoundTheClockCoin, withExchangeLinesAsCoinListings } from "../../utils/crypto-pair";
|
|
46
47
|
import { canonicalExchange, canonicalTickerKey, parsePublicTickerKey } from "../../utils/exchanges";
|
|
47
48
|
import { normalizePriceHistory, priceHistoryIntervalMs, reachesLatestSettledSession } from "../../utils/price-history";
|
|
48
49
|
import { createProviderMiss, ProviderBusyError, providerMissReason, StaleHistoryError } from "../provider-errors";
|
|
@@ -315,14 +316,35 @@ function cloudResponseTargetKey(key: string, requested: Set<string>): string | u
|
|
|
315
316
|
|
|
316
317
|
/**
|
|
317
318
|
* The listing as Gloom Cloud knows it. A Hong Kong code goes out with its four
|
|
318
|
-
* digits: Cloud has no 700 or 5 on HKEX, only 0700 and 0005.
|
|
319
|
+
* digits: Cloud has no 700 or 5 on HKEX, only 0700 and 0005. A crypto
|
|
320
|
+
* exchange's line of a coin pair (BTC/GBP on COINBASE PRO), which no source
|
|
321
|
+
* quotes, goes out as the crypto venue's listing of the pair, BTC-GBP on CCC.
|
|
319
322
|
*/
|
|
320
323
|
function cloudInstrumentTarget(symbol: string, exchange?: string) {
|
|
321
324
|
rejectPredictionMarket(symbol, exchange);
|
|
322
325
|
const target = publicListingTarget(symbol, exchange);
|
|
326
|
+
const coin = exchangeLineCoinListing(target.symbol, target.exchange);
|
|
327
|
+
if (coin) return { symbol: coin.symbol, exchange: coin.exchange };
|
|
323
328
|
return target.exchange ? { ...target, symbol: hongKongListingCode(target.symbol, target.exchange) } : target;
|
|
324
329
|
}
|
|
325
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Whether a quote names a listing other than the one asked for: asked for BHP
|
|
333
|
+
* on Cboe Australia, Gloom Cloud has answered with BHP on NYSE, in dollars. A
|
|
334
|
+
* price for another instrument is worse than none. A quote that names no
|
|
335
|
+
* listing is judged by its symbol alone, as the router judges it.
|
|
336
|
+
*/
|
|
337
|
+
function pricesOtherListing(quote: Quote, target: ReturnType<typeof cloudInstrumentTarget>): boolean {
|
|
338
|
+
const metadata = quoteMetadataFromQuote(quote);
|
|
339
|
+
const named = !!(metadata.listingExchangeName || parsePublicTickerKey(quote.symbol).exchange);
|
|
340
|
+
return !quoteMetadataMatchesTarget(metadata, named ? target.symbol : parsePublicTickerKey(target.symbol).symbol, named ? target.exchange : undefined);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function otherListingMiss(ticker: string, quote: Quote) {
|
|
344
|
+
const venue = quote.listingExchangeName || quote.exchangeName || parsePublicTickerKey(quote.symbol).exchange;
|
|
345
|
+
return createProviderMiss(`Cloud quoted ${quote.symbol}${venue ? ` on ${venue}` : ""} for ${ticker}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
326
348
|
function cloudHistoryRecovery(
|
|
327
349
|
context: MarketDataRequestContext | undefined,
|
|
328
350
|
ticker: string, symbol: string, exchange: string, interval: string, start: Date, end: Date,
|
|
@@ -520,10 +542,12 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
520
542
|
return withCloudFallback(
|
|
521
543
|
async () => {
|
|
522
544
|
const response = await apiClient.getCloudQuote(target.symbol, target.exchange);
|
|
523
|
-
const
|
|
545
|
+
const answered = mapQuote(
|
|
524
546
|
unwrapRequiredCloudResponse(response, `Cloud quotes are unavailable for ${ticker}`),
|
|
525
547
|
response.providerMeta,
|
|
526
|
-
)
|
|
548
|
+
);
|
|
549
|
+
if (pricesOtherListing(answered, target)) throw otherListingMiss(ticker, answered);
|
|
550
|
+
const quote = retainRequestedQuoteSymbol(answered, ticker);
|
|
527
551
|
// A stale answer is the last price the service has. It stays flagged, so
|
|
528
552
|
// no freshness check passes it and only a request with no current quote
|
|
529
553
|
// from any source falls back to it.
|
|
@@ -585,7 +609,11 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
585
609
|
return { target, quote: null, error: createProviderMiss(`Cloud quotes are stale for ${target.symbol}`) };
|
|
586
610
|
}
|
|
587
611
|
if ((item.status === "success" || item.status === "partial") && item.data) {
|
|
588
|
-
const
|
|
612
|
+
const answered = mapQuote(item.data);
|
|
613
|
+
if (pricesOtherListing(answered, cloudInstrumentTarget(target.symbol, target.exchange))) {
|
|
614
|
+
return { target, quote: null, error: otherListingMiss(target.symbol, answered) };
|
|
615
|
+
}
|
|
616
|
+
const quote = retainRequestedQuoteSymbol(answered, target.symbol);
|
|
589
617
|
return { target, quote: item.stale === true ? { ...quote, stale: true } : quote };
|
|
590
618
|
}
|
|
591
619
|
return {
|
|
@@ -617,8 +645,9 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
617
645
|
// Perpetual markets are listings too, asked for beside the others. They are optional: a failed lookup leaves the
|
|
618
646
|
// other results, and a keystroke waits for them only briefly once the listings are in, unless the query names one.
|
|
619
647
|
const pending = searchPerpetualMarkets(query).catch(logFailure("sources.gloomberb-cloud.index", "Searching perpetual markets failed", [] as InstrumentSearchResult[]));
|
|
648
|
+
// A crypto exchange's line of a coin pair, which no source quotes, is offered as the crypto venue's listing.
|
|
620
649
|
const listings = await withCloudFallback(
|
|
621
|
-
() => apiClient.searchInstruments(query, context?.limit ?? 10),
|
|
650
|
+
async () => withExchangeLinesAsCoinListings(await apiClient.searchInstruments(query, context?.limit ?? 10)),
|
|
622
651
|
"Cloud search is unavailable",
|
|
623
652
|
).catch((error: unknown) => {
|
|
624
653
|
// Rate limited or overloaded: the search was not run, which is not a search that found nothing.
|
|
@@ -637,9 +666,12 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
637
666
|
const name = venue ? context?.listingName?.trim() || await this.listingName(ticker, venue) || undefined : undefined;
|
|
638
667
|
const filings = await withCloudFallback(async () => {
|
|
639
668
|
const response = await apiClient.getCloudSecFilings({ ticker, exchange, name, limit: count, offset: 0 });
|
|
640
|
-
|
|
669
|
+
// A foreign line's filings read from its company's home line say whose, each of them.
|
|
670
|
+
const companyFrom = readCompanyFrom(response.companyFrom);
|
|
671
|
+
return response.filings.map((filing) => companyFrom ? { ...mapCloudSecFiling(filing), companyFrom } : mapCloudSecFiling(filing));
|
|
641
672
|
}, `Cloud SEC filings are unavailable for ${ticker}`);
|
|
642
|
-
|
|
673
|
+
// The service matched a line read from its company's home line to the company itself.
|
|
674
|
+
if (!venue || filings.length === 0 || filings[0]!.companyFrom) return filings;
|
|
643
675
|
const symbol = parsePublicTickerKey(ticker).symbol;
|
|
644
676
|
// Without the listing's company the filings cannot be told apart from another company's.
|
|
645
677
|
if (!name) throw createProviderMiss(`SEC filings for ${symbol} could not be matched to its ${venue} listing`);
|
|
@@ -732,6 +764,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
732
764
|
async searchBatch(queries: string[], options: { limit?: number; signal?: AbortSignal } = {}): Promise<Map<string, InstrumentSearchResult[]> | null> {
|
|
733
765
|
const answers = await cloudSearchBatch(queries, options);
|
|
734
766
|
if (!answers) return null;
|
|
767
|
+
for (const [query, rows] of answers) answers.set(query, withExchangeLinesAsCoinListings(rows));
|
|
735
768
|
// Perpetual markets lead each answer, as `search` gives them, so a symbol in a list resolves as it does alone:
|
|
736
769
|
// one read of the board answers every query's search of it. A query that found no listing is searched again
|
|
737
770
|
// on its own, which finds them then; a board that does not answer leaves them out, as a search does.
|
|
@@ -964,6 +997,8 @@ export function createGloomberbCloudCapabilities(provider = createGloomberbCloud
|
|
|
964
997
|
return {
|
|
965
998
|
articles: cloudNewsReadArticles(read, query.ticker),
|
|
966
999
|
nextCursor: read.nextCursor ?? null,
|
|
1000
|
+
access: read.access ?? null,
|
|
1001
|
+
companyFrom: readCompanyFrom(read.companyFrom),
|
|
967
1002
|
};
|
|
968
1003
|
},
|
|
969
1004
|
async fetchNews(query: NewsQuery): Promise<NewsArticle[]> {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeNewsFeed } from "../../news/news-model";
|
|
2
2
|
import { readableNewsSource } from "../../news/source-label";
|
|
3
3
|
import { collectNewsDisplayTickers } from "../../news/ticker-symbols";
|
|
4
|
-
import type { NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
|
|
4
|
+
import type { NewsAccess, NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
|
|
5
|
+
import { CLOUD_NEWS_DELAY_HOURS } from "../../api-client/plan-rules";
|
|
5
6
|
import type {
|
|
6
7
|
apiClient,
|
|
7
8
|
CloudNewsListResponse,
|
|
@@ -134,10 +135,44 @@ export function cloudNewsParams(query: NewsQuery): CloudNewsParams {
|
|
|
134
135
|
};
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* A page of Gloom Cloud stories, with the text searched for when a coin pair's
|
|
140
|
+
* feed had none filed under it, and which news the service answered with.
|
|
141
|
+
*/
|
|
142
|
+
export type CloudNewsRead = Omit<CloudNewsListResponse, "access"> & { searchedFor?: string; access?: NewsAccess | null };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How recently the service must have seen a story for the answer to be the
|
|
146
|
+
* live wire. It holds a story back from an account without real-time news
|
|
147
|
+
* until it last saw it {@link CLOUD_NEWS_DELAY_HOURS} hours ago; the hour
|
|
148
|
+
* less leaves room for this clock and the service's to disagree.
|
|
149
|
+
*/
|
|
150
|
+
const LIVE_STORY_WINDOW_MS = (CLOUD_NEWS_DELAY_HOURS - 1) * 60 * 60 * 1000;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Which news a Gloom Cloud answer is, from the answer: what the service says
|
|
154
|
+
* when it says it; held back when the request carried no session, since the
|
|
155
|
+
* service holds news back for anyone it cannot name; live when it holds a
|
|
156
|
+
* story seen too recently to have been held back. Otherwise null: a quiet
|
|
157
|
+
* feed read live looks the same as a held-back one.
|
|
158
|
+
*/
|
|
159
|
+
export function cloudNewsAccess(
|
|
160
|
+
response: Pick<CloudNewsListResponse, "items" | "access">,
|
|
161
|
+
sentSession: boolean,
|
|
162
|
+
now = Date.now(),
|
|
163
|
+
): NewsAccess | null {
|
|
164
|
+
if (response.access === "realtime" || response.access === "delayed") return response.access;
|
|
165
|
+
if (!sentSession) return "delayed";
|
|
166
|
+
const live = response.items.some((item) => {
|
|
167
|
+
const seenAt = Date.parse(item.lastSeenAt);
|
|
168
|
+
return Number.isFinite(seenAt) && now - seenAt < LIVE_STORY_WINDOW_MS;
|
|
169
|
+
});
|
|
170
|
+
return live ? "realtime" : null;
|
|
171
|
+
}
|
|
139
172
|
|
|
140
|
-
|
|
173
|
+
/** A client that cannot say whether a session goes with its requests (a test double) is taken to send one. */
|
|
174
|
+
type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "searchCloudNews">
|
|
175
|
+
& Partial<Pick<typeof apiClient, "hasSessionCredential">>;
|
|
141
176
|
|
|
142
177
|
/**
|
|
143
178
|
* A news query's first page from Gloom Cloud. Stories are filed under company
|
|
@@ -147,13 +182,18 @@ type CloudNewsClient = Pick<typeof apiClient, "getCloudNews" | "searchCloudNews"
|
|
|
147
182
|
* no name to search keeps the empty answer.
|
|
148
183
|
*/
|
|
149
184
|
export async function readCloudNews(client: CloudNewsClient, query: NewsQuery): Promise<CloudNewsRead> {
|
|
185
|
+
// Asked in the tick the request goes out in, which is when its headers take the session.
|
|
186
|
+
const sentSession = client.hasSessionCredential?.() ?? true;
|
|
150
187
|
const response = await client.getCloudNews(cloudNewsParams(query));
|
|
151
|
-
|
|
188
|
+
const read: CloudNewsRead = { ...response, access: cloudNewsAccess(response, sentSession) };
|
|
189
|
+
if (response.items.length > 0 || query.cursor || normalizeNewsFeed(query) !== "ticker") return read;
|
|
152
190
|
const text = coinSearchName(query.ticker, query.exchange, query.listingName);
|
|
153
|
-
if (!text) return
|
|
191
|
+
if (!text) return read;
|
|
154
192
|
const found = await client.searchCloudNews(text, query.limit)
|
|
155
193
|
.catch(logFailure("sources.gloomberb-cloud.news", "Search a coin's stories failed", null));
|
|
156
|
-
return found && found.items.length > 0
|
|
194
|
+
return found && found.items.length > 0
|
|
195
|
+
? { items: found.items, nextCursor: null, searchedFor: text, access: cloudNewsAccess(found, sentSession) }
|
|
196
|
+
: read;
|
|
157
197
|
}
|
|
158
198
|
|
|
159
199
|
/** A read's stories as articles, each marked with the text a coin's fallback searched for. */
|
|
@@ -3,7 +3,7 @@ import type { AnalystResearchData, CompanyProfile, CorporateActionsData, Financi
|
|
|
3
3
|
import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
|
|
4
4
|
import { coalesceFinancialPeriodAliases, mergeFinancialStatementRows } from "../../utils/financial-statements";
|
|
5
5
|
import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
|
|
6
|
-
import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS } from "../../utils/fundamentals";
|
|
6
|
+
import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS, RETRACTED_FUNDAMENTALS_FIELDS } from "../../utils/fundamentals";
|
|
7
7
|
import { hasValidQuoteObservationTime, isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
8
8
|
import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
|
|
9
9
|
import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges";
|
|
@@ -458,12 +458,21 @@ function mergeFundamentals(primary: Fundamentals | undefined, fallback: Fundamen
|
|
|
458
458
|
const merged = mergeDefinedObject(primary, fallback);
|
|
459
459
|
if (merged) {
|
|
460
460
|
const unavailable = new Set(fallback?.unavailableFields ?? []);
|
|
461
|
-
for (const field of
|
|
461
|
+
for (const field of RETRACTED_FUNDAMENTALS_FIELDS) {
|
|
462
462
|
if (primary?.unavailableFields?.includes(field)) unavailable.add(field);
|
|
463
463
|
else if (typeof primary?.[field] === "number" && Number.isFinite(primary[field])) unavailable.delete(field);
|
|
464
464
|
}
|
|
465
465
|
if (unavailable.size) merged.unavailableFields = [...unavailable];
|
|
466
466
|
else delete merged.unavailableFields;
|
|
467
|
+
// A retracted cap is none, whatever an older observation held; its reason goes with the retraction that stands.
|
|
468
|
+
if (unavailable.has("marketCap")) {
|
|
469
|
+
delete merged.marketCap;
|
|
470
|
+
merged.marketCapUnavailable = primary?.unavailableFields?.includes("marketCap")
|
|
471
|
+
? primary.marketCapUnavailable : fallback?.marketCapUnavailable;
|
|
472
|
+
if (!merged.marketCapUnavailable) delete merged.marketCapUnavailable;
|
|
473
|
+
} else {
|
|
474
|
+
delete merged.marketCapUnavailable;
|
|
475
|
+
}
|
|
467
476
|
}
|
|
468
477
|
const dividend = primary?.dividendYield != null ? primary : fallback;
|
|
469
478
|
if (merged && dividend) {
|
|
@@ -557,7 +566,10 @@ export function mergeRefreshedFinancials(cached: TickerFinancials, fresh: Ticker
|
|
|
557
566
|
const merged = mergeFinancials(cached, fresh)!;
|
|
558
567
|
const statistics = redactUnavailableFundamentals(fresh?.fundamentals);
|
|
559
568
|
if (!statistics) return merged;
|
|
560
|
-
const update: Fundamentals = {
|
|
569
|
+
const update: Fundamentals = {
|
|
570
|
+
unavailableFields: statistics.unavailableFields,
|
|
571
|
+
...statistics.marketCapUnavailable ? { marketCapUnavailable: statistics.marketCapUnavailable } : {},
|
|
572
|
+
};
|
|
561
573
|
const currencyConflict = cached.fundamentals?.financialCurrency && statistics.financialCurrency
|
|
562
574
|
&& cached.fundamentals.financialCurrency !== statistics.financialCurrency;
|
|
563
575
|
for (const field of RETRACTABLE_VALUATION_FIELDS) {
|
|
@@ -53,6 +53,7 @@ import { ProviderRouterSearchRoutes } from "./search";
|
|
|
53
53
|
import { collectCapabilityRouteSources, normalizeRouteSource } from "./sources";
|
|
54
54
|
import type { ProviderRouterCoreDeps } from "./route-types";
|
|
55
55
|
import { withProviderConnectionHealth } from "./connection-health";
|
|
56
|
+
import { withFiledTrailingEps } from "../../time-series/filed-trailing-eps";
|
|
56
57
|
import {
|
|
57
58
|
contextFromCachedTarget,
|
|
58
59
|
getBrokerCandidates,
|
|
@@ -213,7 +214,7 @@ export class AssetDataRouter implements DataProvider {
|
|
|
213
214
|
for (const target of valid) {
|
|
214
215
|
const key = target.symbol.toUpperCase();
|
|
215
216
|
const financials = cached.get(key);
|
|
216
|
-
if (financials) cached.set(key, this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target))));
|
|
217
|
+
if (financials) cached.set(key, withFiledTrailingEps(this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target)))));
|
|
217
218
|
}
|
|
218
219
|
return cached;
|
|
219
220
|
}
|
|
@@ -263,8 +264,9 @@ export class AssetDataRouter implements DataProvider {
|
|
|
263
264
|
const dated = await this.datedAnswers(quoted, options.datesChangeBasis);
|
|
264
265
|
let next = 0;
|
|
265
266
|
const results = answered.map((result) => {
|
|
266
|
-
if (!result.financials
|
|
267
|
-
const
|
|
267
|
+
if (!result.financials) return result;
|
|
268
|
+
const quoted = result.financials.quote ? this.withDatedQuote(result.financials, dated[next++]!, listingOfResult(result)) : result.financials;
|
|
269
|
+
const financials = withFiledTrailingEps(quoted);
|
|
268
270
|
return financials === result.financials ? result : { ...result, financials };
|
|
269
271
|
});
|
|
270
272
|
const restored = new Map(results.map((result) => {
|
|
@@ -279,10 +281,11 @@ export class AssetDataRouter implements DataProvider {
|
|
|
279
281
|
const { context, datesChangeBasis } = splitDating(requestContext);
|
|
280
282
|
exchange = this.resolvePublicExchange(ticker, exchange, context);
|
|
281
283
|
const financials = await this.financialRoutes.getTickerFinancials(ticker, exchange, context);
|
|
282
|
-
if (!financials.quote) return financials;
|
|
284
|
+
if (!financials.quote) return withFiledTrailingEps(financials);
|
|
283
285
|
const listing = listingOf({ symbol: ticker, exchange }, context);
|
|
284
286
|
const [quote] = await this.datedAnswers([{ quote: financials.quote, listing }], datesChangeBasis);
|
|
285
|
-
|
|
287
|
+
// One trailing EPS and P/E for every view of the listing: the filed quarters', as PEB and GE read them.
|
|
288
|
+
return withFiledTrailingEps(this.withDatedQuote(financials, quote!, listing));
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
async getQuote(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<Quote> {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
2
|
+
import { hasPendingTickerWrites, settleTickerWrites, tickerWriteRevision } from "../data/ticker-writes";
|
|
3
|
+
import type { TickerRecord } from "../types/ticker";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persist cleanup before publishing it, retrying against edits acknowledged in between.
|
|
7
|
+
* Return the cleaned map so callers can recheck it after their config save.
|
|
8
|
+
*/
|
|
9
|
+
export async function persistTickerCleanup({
|
|
10
|
+
tickerRepository,
|
|
11
|
+
getTickers,
|
|
12
|
+
transform,
|
|
13
|
+
publish,
|
|
14
|
+
isCurrent = () => true,
|
|
15
|
+
}: {
|
|
16
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
17
|
+
getTickers: () => ReadonlyMap<string, TickerRecord>;
|
|
18
|
+
transform: (ticker: TickerRecord) => TickerRecord | null;
|
|
19
|
+
publish: (symbol: string, ticker: TickerRecord | null) => void;
|
|
20
|
+
isCurrent?: () => boolean;
|
|
21
|
+
}): Promise<ReadonlyMap<string, TickerRecord> | null> {
|
|
22
|
+
for (;;) {
|
|
23
|
+
do {
|
|
24
|
+
await settleTickerWrites(tickerRepository);
|
|
25
|
+
} while (hasPendingTickerWrites(tickerRepository));
|
|
26
|
+
if (!isCurrent()) return null;
|
|
27
|
+
const before = getTickers();
|
|
28
|
+
let wrote = false;
|
|
29
|
+
for (const symbol of before.keys()) {
|
|
30
|
+
do {
|
|
31
|
+
await settleTickerWrites(tickerRepository);
|
|
32
|
+
} while (hasPendingTickerWrites(tickerRepository));
|
|
33
|
+
if (!isCurrent()) return null;
|
|
34
|
+
const source = getTickers().get(symbol);
|
|
35
|
+
if (!source) continue;
|
|
36
|
+
const next = transform(source);
|
|
37
|
+
if (next === source) continue;
|
|
38
|
+
wrote = true;
|
|
39
|
+
const save = next ? tickerRepository.saveTicker(next) : tickerRepository.deleteTicker(symbol);
|
|
40
|
+
const revision = tickerWriteRevision(tickerRepository, symbol);
|
|
41
|
+
await save;
|
|
42
|
+
if (!isCurrent()) return null;
|
|
43
|
+
// An edit can start its save before publishing state, especially over RPC.
|
|
44
|
+
if (tickerWriteRevision(tickerRepository, symbol) !== revision || getTickers().get(symbol) !== source) continue;
|
|
45
|
+
publish(symbol, next);
|
|
46
|
+
}
|
|
47
|
+
// Include rows created or recreated while earlier rows were being saved.
|
|
48
|
+
if (!wrote && getTickers() === before) return before;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/sync/controller.ts
CHANGED
|
@@ -41,7 +41,8 @@ interface SyncRuntime {
|
|
|
41
41
|
isLocalMarketData?: LocalMarketDataLookup;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/** Kept in the view's storage; the desktop app also keeps a copy on disk (electrobun/view/view-ids.ts). */
|
|
45
|
+
export const SYNC_CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
|
|
45
46
|
const PUSH_DEBOUNCE_MS = 2500;
|
|
46
47
|
/** How soon after a change contributors date it, so an edit made just before quitting still is. */
|
|
47
48
|
const OBSERVE_DEBOUNCE_MS = 250;
|
|
@@ -60,10 +61,10 @@ function nowIso(): string {
|
|
|
60
61
|
|
|
61
62
|
function resolveClientId(): string {
|
|
62
63
|
const storage = globalThis.localStorage;
|
|
63
|
-
const existing = storage?.getItem(
|
|
64
|
+
const existing = storage?.getItem(SYNC_CLIENT_ID_STORAGE_KEY);
|
|
64
65
|
if (existing) return existing;
|
|
65
66
|
const id = `client_${crypto.randomUUID()}`;
|
|
66
|
-
storage?.setItem(
|
|
67
|
+
storage?.setItem(SYNC_CLIENT_ID_STORAGE_KEY, id);
|
|
67
68
|
return id;
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { saveConfigImmediately, scheduleConfigSave } from "../state/config-save-scheduler";
|
|
2
|
+
import { persistTickerCleanup } from "../state/ticker-cleanup";
|
|
3
|
+
import { hasPendingTickerWrites } from "../data/ticker-writes";
|
|
2
4
|
import { normalizeStoredBaseCurrency } from "../utils/base-currency";
|
|
3
5
|
import { debugLog } from "../utils/debug-log";
|
|
4
6
|
import { fnv1aHex, stableStringify } from "../utils/hash";
|
|
@@ -1184,6 +1186,23 @@ function holdingIn(positions: readonly TickerPosition[], links: readonly unknown
|
|
|
1184
1186
|
};
|
|
1185
1187
|
}
|
|
1186
1188
|
|
|
1189
|
+
/** Without a shared baseline, keep both devices' manual lots, matching identical copies one for one. */
|
|
1190
|
+
function mergeUntrackedManualPositions(local: TickerPosition[], pulled: TickerPosition[]): TickerPosition[] {
|
|
1191
|
+
const unmatched = new Map<string, number>();
|
|
1192
|
+
for (const position of local) {
|
|
1193
|
+
const key = JSON.stringify(sanitizePosition(position));
|
|
1194
|
+
unmatched.set(key, (unmatched.get(key) ?? 0) + 1);
|
|
1195
|
+
}
|
|
1196
|
+
const merged = [...local];
|
|
1197
|
+
for (const position of pulled) {
|
|
1198
|
+
const key = JSON.stringify(sanitizePosition(position));
|
|
1199
|
+
const count = unmatched.get(key) ?? 0;
|
|
1200
|
+
if (count > 0) unmatched.set(key, count - 1);
|
|
1201
|
+
else merged.push(position);
|
|
1202
|
+
}
|
|
1203
|
+
return merged;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1187
1206
|
interface PulledTickersMerge {
|
|
1188
1207
|
lastSyncedTickers: Map<string, Record<string, unknown>> | null;
|
|
1189
1208
|
/** This device's live broker portfolios, each with the side holding its latest broker sync. */
|
|
@@ -1274,8 +1293,25 @@ function mergePulledTicker(
|
|
|
1274
1293
|
holdingIn(baselinePositions, baselineLinks, portfolioId),
|
|
1275
1294
|
);
|
|
1276
1295
|
const [sidePositions, sideLinks] = takePulled ? [pulledPositions, pulledLinks] : [localPositions, localLinks];
|
|
1277
|
-
|
|
1278
|
-
|
|
1296
|
+
if (brokerSide && !merge.localOnly?.portfolioIds.has(portfolioId)) {
|
|
1297
|
+
// A broker's timestamp governs imported holdings, never hand-entered
|
|
1298
|
+
// lots. Those use the same edit comparison as a manual portfolio.
|
|
1299
|
+
const manualIn = (entries: readonly TickerPosition[]) => entries.filter((position) => (
|
|
1300
|
+
position.portfolio === portfolioId && position.broker === "manual"
|
|
1301
|
+
));
|
|
1302
|
+
const localManual = manualIn(localPositions);
|
|
1303
|
+
const manualEdited = comparable && !sameAsStored(
|
|
1304
|
+
localManual.map(sanitizePosition), manualIn(baselinePositions).map(sanitizePosition),
|
|
1305
|
+
);
|
|
1306
|
+
const pulledManual = manualIn(pulledPositions);
|
|
1307
|
+
const manual = !comparable ? mergeUntrackedManualPositions(localManual, pulledManual)
|
|
1308
|
+
: manualEdited ? localManual : pulledManual;
|
|
1309
|
+
positions.push(...sidePositions.filter((position) => position.portfolio === portfolioId && position.broker !== "manual"), ...manual);
|
|
1310
|
+
if (sideLinks.includes(portfolioId) || manual.length > 0) portfolios.push(portfolioId);
|
|
1311
|
+
} else {
|
|
1312
|
+
positions.push(...sidePositions.filter((position) => position.portfolio === portfolioId));
|
|
1313
|
+
if (sideLinks.includes(portfolioId)) portfolios.push(portfolioId);
|
|
1314
|
+
}
|
|
1279
1315
|
}
|
|
1280
1316
|
|
|
1281
1317
|
const record: TickerRecord = {
|
|
@@ -1297,8 +1333,9 @@ function mergePulledTicker(
|
|
|
1297
1333
|
function withoutUnpulledHoldings(ticker: TickerRecord, merge: PulledTickersMerge): TickerRecord | null {
|
|
1298
1334
|
const sold = (portfolioId: string) => merge.brokerPortfolios.get(portfolioId) === "pulled";
|
|
1299
1335
|
const { metadata } = ticker;
|
|
1300
|
-
const positions = metadata.positions.filter((position) => !sold(position.portfolio));
|
|
1301
|
-
const
|
|
1336
|
+
const positions = metadata.positions.filter((position) => position.broker === "manual" || !sold(position.portfolio));
|
|
1337
|
+
const held = new Set(positions.map((position) => position.portfolio));
|
|
1338
|
+
const portfolios = metadata.portfolios.filter((portfolioId) => held.has(portfolioId) || !sold(portfolioId));
|
|
1302
1339
|
const kept = positions.length === metadata.positions.length && portfolios.length === metadata.portfolios.length
|
|
1303
1340
|
? ticker
|
|
1304
1341
|
: { ...ticker, metadata: { ...metadata, positions, portfolios } };
|
|
@@ -1322,12 +1359,57 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1322
1359
|
},
|
|
1323
1360
|
apply: async (payload, { snapshot, baselinePayload, baselineState, state, getState, isCurrent, dispatch, tickerRepository, memo }) => {
|
|
1324
1361
|
const pushedAt = snapshot?.contributors[CORE_CONFIG_CONTRIBUTOR_ID]?.updatedAt;
|
|
1325
|
-
const
|
|
1362
|
+
const merge = (config: AppConfig) => mergeConfigPayload(config, payload, baselineState.config, baselinePayload, {
|
|
1326
1363
|
memo: memo?.get(),
|
|
1327
1364
|
pushedAt,
|
|
1328
|
-
}, localOnlyScope(
|
|
1365
|
+
}, localOnlyScope(config));
|
|
1366
|
+
let nextConfig = merge(state.config);
|
|
1329
1367
|
if (!nextConfig) return;
|
|
1330
|
-
const
|
|
1368
|
+
const removed = removedBrokerHoldings(state.config, nextConfig);
|
|
1369
|
+
if (removed) {
|
|
1370
|
+
// Keep the profile until row and config writes succeed, so a failed
|
|
1371
|
+
// cleanup remains visible to the next attempt.
|
|
1372
|
+
const cleanTickers = () => persistTickerCleanup({
|
|
1373
|
+
tickerRepository,
|
|
1374
|
+
getTickers: () => getState().tickers,
|
|
1375
|
+
transform: (ticker) => withoutRemovedBrokerHoldings(ticker, removed),
|
|
1376
|
+
publish: (symbol, ticker) => {
|
|
1377
|
+
const tickers = new Map(getState().tickers);
|
|
1378
|
+
if (ticker) tickers.set(symbol, ticker);
|
|
1379
|
+
else tickers.delete(symbol);
|
|
1380
|
+
dispatch({ type: "SET_TICKERS", tickers });
|
|
1381
|
+
},
|
|
1382
|
+
isCurrent,
|
|
1383
|
+
});
|
|
1384
|
+
let savedRemoval = false;
|
|
1385
|
+
for (;;) {
|
|
1386
|
+
const cleaned = await cleanTickers().catch(async (error: unknown) => {
|
|
1387
|
+
// Restore the profile if a late row fails after config was saved,
|
|
1388
|
+
// so retry can also recover after restarting the app.
|
|
1389
|
+
if (savedRemoval) await saveConfigImmediately(getState().config);
|
|
1390
|
+
throw error;
|
|
1391
|
+
});
|
|
1392
|
+
if (!cleaned || !isCurrent()) return;
|
|
1393
|
+
if (hasPendingTickerWrites(tickerRepository) || getState().tickers !== cleaned) continue;
|
|
1394
|
+
const source = getState().config;
|
|
1395
|
+
const merged = merge(source);
|
|
1396
|
+
if (!merged) return;
|
|
1397
|
+
// Preserve unrelated edits while retaining the removal already applied
|
|
1398
|
+
// to rows, including pulls from builds that merged whole profile lists.
|
|
1399
|
+
nextConfig = {
|
|
1400
|
+
...merged,
|
|
1401
|
+
brokerInstances: merged.brokerInstances.filter((instance) => !removed.instanceIds.has(instance.id)),
|
|
1402
|
+
portfolios: merged.portfolios.filter((portfolio) => !removed.portfolioIds.has(portfolio.id)),
|
|
1403
|
+
};
|
|
1404
|
+
await saveConfigImmediately(nextConfig);
|
|
1405
|
+
savedRemoval = true;
|
|
1406
|
+
if (!isCurrent()) return;
|
|
1407
|
+
if (getState().config !== source || getState().tickers !== cleaned
|
|
1408
|
+
|| hasPendingTickerWrites(tickerRepository)) continue;
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
const configChanged = !valuesEqual(nextConfig, getState().config);
|
|
1331
1413
|
if (configChanged) {
|
|
1332
1414
|
// Adopting another device's workspace rearranges the screen under the
|
|
1333
1415
|
// user, so it is worth a line in the log when a report says "my panes
|
|
@@ -1339,32 +1421,9 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1339
1421
|
});
|
|
1340
1422
|
}
|
|
1341
1423
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
1342
|
-
scheduleConfigSave(nextConfig);
|
|
1424
|
+
if (!removed) scheduleConfigSave(nextConfig);
|
|
1343
1425
|
}
|
|
1344
1426
|
if (memo && isRecord(payload)) rememberCloudLayout(payload, pushedAt, getState().config, memo, nextConfig.layouts !== state.config.layouts);
|
|
1345
|
-
if (!configChanged) return;
|
|
1346
|
-
|
|
1347
|
-
// A profile removed on another device takes its positions with it here
|
|
1348
|
-
// too; kept, this device would push them back and recreate the tickers
|
|
1349
|
-
// that removal deleted.
|
|
1350
|
-
const removed = removedBrokerHoldings(state.config, nextConfig);
|
|
1351
|
-
if (!removed) return;
|
|
1352
|
-
const nextTickers = new Map(getState().tickers);
|
|
1353
|
-
let changed = false;
|
|
1354
|
-
for (const ticker of getState().tickers.values()) {
|
|
1355
|
-
if (!isCurrent()) return;
|
|
1356
|
-
const next = withoutRemovedBrokerHoldings(ticker, removed);
|
|
1357
|
-
if (next === ticker) continue;
|
|
1358
|
-
changed = true;
|
|
1359
|
-
if (next) {
|
|
1360
|
-
await tickerRepository.saveTicker(next);
|
|
1361
|
-
nextTickers.set(next.metadata.ticker, next);
|
|
1362
|
-
} else {
|
|
1363
|
-
await tickerRepository.deleteTicker(ticker.metadata.ticker);
|
|
1364
|
-
nextTickers.delete(ticker.metadata.ticker);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
if (changed && isCurrent()) dispatch({ type: "SET_TICKERS", tickers: nextTickers });
|
|
1368
1427
|
},
|
|
1369
1428
|
};
|
|
1370
1429
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StallEvent } from "../utils/main-thread-stall";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Only the freezes a user sat through. The system slows the timers of a
|
|
5
|
+
* window behind others or out of view, sometimes to one a second, and the
|
|
6
|
+
* stall monitor cannot tell that from a freeze: a timer back a second late
|
|
7
|
+
* reads the same either way. A window that has been in front, with focus,
|
|
8
|
+
* since before the stall began ran at full speed, so its late timers are
|
|
9
|
+
* freezes. Anything else is dropped, including a real freeze the user
|
|
10
|
+
* clicked into the window during: missing one beats reporting a false one.
|
|
11
|
+
*/
|
|
12
|
+
export function forwardAttendedStalls(
|
|
13
|
+
forward: (event: StallEvent) => void,
|
|
14
|
+
target: Window = window,
|
|
15
|
+
now: () => number = () => performance.now(),
|
|
16
|
+
): { sink: (event: StallEvent) => void; dispose: () => void } {
|
|
17
|
+
const doc = target.document;
|
|
18
|
+
const attended = () => doc.visibilityState === "visible" && doc.hasFocus();
|
|
19
|
+
// Since when the window has been attended; never while it is not.
|
|
20
|
+
let attendedSince = attended() ? now() : Number.POSITIVE_INFINITY;
|
|
21
|
+
const update = () => {
|
|
22
|
+
if (!attended()) attendedSince = Number.POSITIVE_INFINITY;
|
|
23
|
+
else if (attendedSince === Number.POSITIVE_INFINITY) attendedSince = now();
|
|
24
|
+
};
|
|
25
|
+
target.addEventListener("focus", update);
|
|
26
|
+
target.addEventListener("blur", update);
|
|
27
|
+
doc.addEventListener("visibilitychange", update);
|
|
28
|
+
return {
|
|
29
|
+
sink: (event) => {
|
|
30
|
+
if (attended() && attendedSince <= now() - event.blockedMs) forward(event);
|
|
31
|
+
},
|
|
32
|
+
dispose: () => {
|
|
33
|
+
target.removeEventListener("focus", update);
|
|
34
|
+
target.removeEventListener("blur", update);
|
|
35
|
+
doc.removeEventListener("visibilitychange", update);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { StallReportState, StallReporterHost } from "./stall-reports";
|
|
4
|
+
|
|
5
|
+
/** Next to config.json: one line per stall, newest last. */
|
|
6
|
+
const STALL_LOG_FILE = "stall-log.txt";
|
|
7
|
+
/** Next to config.json: when each kind of stall was last reported, and the counts since. */
|
|
8
|
+
const STATE_FILE = "stall-reports.json";
|
|
9
|
+
/** Past this the log keeps its newer half. */
|
|
10
|
+
const LOG_MAX_BYTES = 256 * 1024;
|
|
11
|
+
|
|
12
|
+
function readCounts(value: unknown): StallReportState["pending"] {
|
|
13
|
+
const pending: StallReportState["pending"] = {};
|
|
14
|
+
if (!value || typeof value !== "object") return pending;
|
|
15
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
16
|
+
const { count, longestMs } = (entry ?? {}) as Record<string, unknown>;
|
|
17
|
+
if (typeof count === "number" && Number.isFinite(count) && typeof longestMs === "number" && Number.isFinite(longestMs)) {
|
|
18
|
+
pending[key] = { count, longestMs };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return pending;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The saved state, keeping only entries of the right shape. */
|
|
25
|
+
function parseStallReportState(text: string): StallReportState {
|
|
26
|
+
const parsed: unknown = JSON.parse(text);
|
|
27
|
+
const record = parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : {};
|
|
28
|
+
const sent: Record<string, number> = {};
|
|
29
|
+
if (record.sent && typeof record.sent === "object") {
|
|
30
|
+
for (const [key, at] of Object.entries(record.sent as Record<string, unknown>)) {
|
|
31
|
+
if (typeof at === "number" && Number.isFinite(at)) sent[key] = at;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { sent, pending: readCounts(record.pending) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trimLog(path: string): void {
|
|
38
|
+
if (statSync(path).size <= LOG_MAX_BYTES) return;
|
|
39
|
+
const text = readFileSync(path, "utf-8");
|
|
40
|
+
const keep = text.slice(text.length - LOG_MAX_BYTES / 2);
|
|
41
|
+
writeFileSync(path, keep.slice(keep.indexOf("\n") + 1), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The stall reporter for a Bun process (the terminal app, the desktop app's main process). */
|
|
45
|
+
export function createNodeStallReporterHost(options: {
|
|
46
|
+
/** The profile's data folder, or null before the config is loaded. */
|
|
47
|
+
getDataDir(): string | null;
|
|
48
|
+
}): StallReporterHost {
|
|
49
|
+
const pathFor = (file: string) => {
|
|
50
|
+
const dir = options.getDataDir();
|
|
51
|
+
return dir ? join(dir, file) : null;
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
platform: process.platform,
|
|
55
|
+
arch: process.arch,
|
|
56
|
+
appendLog: (line) => {
|
|
57
|
+
const path = pathFor(STALL_LOG_FILE);
|
|
58
|
+
if (!path) return;
|
|
59
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
60
|
+
appendFileSync(path, `${line}\n`, "utf-8");
|
|
61
|
+
trimLog(path);
|
|
62
|
+
},
|
|
63
|
+
load: () => {
|
|
64
|
+
const path = pathFor(STATE_FILE);
|
|
65
|
+
if (!path || !existsSync(path)) return { sent: {}, pending: {} };
|
|
66
|
+
return parseStallReportState(readFileSync(path, "utf-8"));
|
|
67
|
+
},
|
|
68
|
+
save: (state) => {
|
|
69
|
+
const path = pathFor(STATE_FILE);
|
|
70
|
+
if (!path) return;
|
|
71
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
72
|
+
writeFileSync(path, JSON.stringify(state), "utf-8");
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|