@gloom-sh/terminal 0.19.1 → 0.19.3
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 +7 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +58 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +6 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +49 -6
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +191 -78
- 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/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +6 -4
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +19 -3
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +64 -42
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +5 -2
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/time-series/studies.ts +0 -1628
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPluginCache } from "../../data/plugin-cache";
|
|
1
|
+
import { createPluginCache, savedCopyForTransientFailure } from "../../data/plugin-cache";
|
|
2
2
|
import { getPublishedUsEquityCalendarDay } from "../../market-data/published-us-sessions";
|
|
3
3
|
import { zonedDateKey } from "../../utils/zoned-date-time";
|
|
4
4
|
import type {
|
|
@@ -198,7 +198,9 @@ export async function loadCachedFredSeries(
|
|
|
198
198
|
const cached = stored ? withFredSourceFreshness(stored) : null;
|
|
199
199
|
if (!options?.force && !pending && cached && !cached.stale) return { ...cached, source: "cache" };
|
|
200
200
|
const result = await cache.load(cacheKey(request), loader, pending ? { ...options, force: true } : options);
|
|
201
|
-
// An early re-read is opportunistic: a failure keeps the still-fresh copy as it was.
|
|
201
|
+
// An early re-read is opportunistic: a failure keeps the still-fresh copy as it was, as not asking would have.
|
|
202
202
|
if (pending && result.source === "stale-fallback" && cached && !cached.stale && !options?.force) return { ...cached, source: "cache" };
|
|
203
|
-
|
|
203
|
+
// A refusal (a series no longer served, a signed-out session) or a reply that fails validation is thrown,
|
|
204
|
+
// whatever copy is saved: a saved copy answers only for a failure asking again may get past.
|
|
205
|
+
return withFredSourceFreshness(savedCopyForTransientFailure(result));
|
|
204
206
|
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
EarningsEvent,
|
|
19
19
|
SecFilingDocument,
|
|
20
20
|
SecFilingItem,
|
|
21
|
+
BatchedReadMethod,
|
|
21
22
|
MarketDataRequestContext,
|
|
22
23
|
QuoteBatchOptions,
|
|
23
24
|
QuoteBatchResult,
|
|
@@ -48,7 +49,7 @@ import { canonicalExchange, canonicalTickerKey, parsePublicTickerKey } from "../
|
|
|
48
49
|
import { normalizePriceHistory, priceHistoryIntervalMs, reachesLatestSettledSession } from "../../utils/price-history";
|
|
49
50
|
import { createProviderMiss, ProviderBusyError, providerMissReason, StaleHistoryError } from "../provider-errors";
|
|
50
51
|
import { parseListingDelisting } from "../../market-data/quotes/delisting";
|
|
51
|
-
import { isRetryableApiError } from "../../api-client/errors";
|
|
52
|
+
import { isRetryableApiError, markAskedTwice, sourceDidNotAnswer, wasAskedTwice } from "../../api-client/errors";
|
|
52
53
|
import { assertSecRegistrantMatches } from "../sec-registrant";
|
|
53
54
|
import { nonUsSecListingVenue } from "../../utils/sec";
|
|
54
55
|
import { publicListingTarget } from "../listing-target";
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
GLOOMBERB_CLOUD_PROVIDER_ID,
|
|
67
68
|
formatCloudDateTime,
|
|
68
69
|
formatCloudHistoryEnd,
|
|
70
|
+
isCloudSourceFailure,
|
|
69
71
|
isEmptyCloudStatus,
|
|
70
72
|
mapBatchError,
|
|
71
73
|
NOT_FOUND_REASON,
|
|
@@ -86,6 +88,7 @@ import {
|
|
|
86
88
|
boardSearchesFrom,
|
|
87
89
|
} from "./perpetuals";
|
|
88
90
|
import { predictionMarketListing } from "../../tickers/prediction-listing";
|
|
91
|
+
import { cleanListingName } from "../../tickers/company-name";
|
|
89
92
|
import { settleWithin } from "../../utils/async-deadline";
|
|
90
93
|
import { cloudSearchBatch, cloudTickerChangesBatch } from "./batched-lookups";
|
|
91
94
|
|
|
@@ -127,6 +130,7 @@ function mapCloudSecFiling(item: {
|
|
|
127
130
|
primaryDocumentUrl?: string;
|
|
128
131
|
fileNumber?: string;
|
|
129
132
|
fundSeries?: boolean;
|
|
133
|
+
pressReleaseUrl?: string;
|
|
130
134
|
}): SecFilingItem {
|
|
131
135
|
return {
|
|
132
136
|
accessionNumber: item.accessionNumber,
|
|
@@ -144,9 +148,20 @@ function mapCloudSecFiling(item: {
|
|
|
144
148
|
// Absent from an older server: no file number is not the same as an empty one.
|
|
145
149
|
...(typeof item.fileNumber === "string" ? { fileNumber: item.fileNumber } : {}),
|
|
146
150
|
...(typeof item.fundSeries === "boolean" ? { fundSeries: item.fundSeries } : {}),
|
|
151
|
+
...(typeof item.pressReleaseUrl === "string" && /^https:\/\/www\.sec\.gov\//.test(item.pressReleaseUrl)
|
|
152
|
+
? { pressReleaseUrl: item.pressReleaseUrl } : {}),
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
/** Search rows with a venue's fixed-width name cleaned of the stray code after its spaces ("Americold Realty Trust Inc. R"). */
|
|
157
|
+
function withCleanNames<T extends { name: string }>(rows: T[]): T[] {
|
|
158
|
+
return rows.map((row) => {
|
|
159
|
+
if (typeof row.name !== "string") return row;
|
|
160
|
+
const name = cleanListingName(row.name);
|
|
161
|
+
return name === row.name ? row : { ...row, name };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
function isCloudProviderMiss(error: unknown): boolean {
|
|
151
166
|
const message = error instanceof Error ? error.message : String(error);
|
|
152
167
|
return CLOUD_PROVIDER_MISS_PATTERNS.some((pattern) => pattern.test(message));
|
|
@@ -420,6 +435,8 @@ function unwrapRequiredCloudResponse<T>(response: CloudMarketResponse<T>, messag
|
|
|
420
435
|
if (isEmptyCloudStatus(response.status)) {
|
|
421
436
|
throw createProviderMiss(response.reasonCode ?? message, response.message, { notFound: response.reasonCode === NOT_FOUND_REASON });
|
|
422
437
|
}
|
|
438
|
+
// The service's own source failed on a listing read asked twice (`readListing`): not an answer that the listing has nothing.
|
|
439
|
+
if (isCloudSourceFailure(response.status) && wasAskedTwice(response)) throw markAskedTwice(sourceDidNotAnswer(response.reasonCode ?? message));
|
|
423
440
|
throw new Error(response.reasonCode ?? message);
|
|
424
441
|
}
|
|
425
442
|
|
|
@@ -647,7 +664,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
647
664
|
const pending = searchPerpetualMarkets(query).catch(logFailure("sources.gloomberb-cloud.index", "Searching perpetual markets failed", [] as InstrumentSearchResult[]));
|
|
648
665
|
// A crypto exchange's line of a coin pair, which no source quotes, is offered as the crypto venue's listing.
|
|
649
666
|
const listings = await withCloudFallback(
|
|
650
|
-
async () => withExchangeLinesAsCoinListings(await apiClient.searchInstruments(query, context?.limit ?? 10)),
|
|
667
|
+
async () => withExchangeLinesAsCoinListings(withCleanNames(await apiClient.searchInstruments(query, context?.limit ?? 10))),
|
|
651
668
|
"Cloud search is unavailable",
|
|
652
669
|
).catch((error: unknown) => {
|
|
653
670
|
// Rate limited or overloaded: the search was not run, which is not a search that found nothing.
|
|
@@ -764,7 +781,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
764
781
|
async searchBatch(queries: string[], options: { limit?: number; signal?: AbortSignal } = {}): Promise<Map<string, InstrumentSearchResult[]> | null> {
|
|
765
782
|
const answers = await cloudSearchBatch(queries, options);
|
|
766
783
|
if (!answers) return null;
|
|
767
|
-
for (const [query, rows] of answers) answers.set(query, withExchangeLinesAsCoinListings(rows));
|
|
784
|
+
for (const [query, rows] of answers) answers.set(query, withExchangeLinesAsCoinListings(withCleanNames(rows)));
|
|
768
785
|
// Perpetual markets lead each answer, as `search` gives them, so a symbol in a list resolves as it does alone:
|
|
769
786
|
// one read of the board answers every query's search of it. A query that found no listing is searched again
|
|
770
787
|
// on its own, which finds them then; a board that does not answer leaves them out, as a search does.
|
|
@@ -818,7 +835,8 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
818
835
|
exchange = target.exchange ?? "";
|
|
819
836
|
const request = toHistoryRequest(range);
|
|
820
837
|
const { response, extendedHours } = await withCloudFallback(
|
|
821
|
-
() => loadCloudHistory(request.interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, { ...request, ...(session ? { session } : {}) }
|
|
838
|
+
() => loadCloudHistory(request.interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, { ...request, ...(session ? { session } : {}) },
|
|
839
|
+
{ batch: _context?.batch }), _context),
|
|
822
840
|
`Cloud chart data is unavailable for ${ticker}`,
|
|
823
841
|
);
|
|
824
842
|
return mapCloudPriceHistory(response, target.symbol, exchange, request.interval, subtractTimeRange(new Date(), range), undefined, extendedHours);
|
|
@@ -905,11 +923,16 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
905
923
|
return mapCloudPriceHistory(response, target.symbol, exchange, interval, startDate, endDate, extendedHours);
|
|
906
924
|
}
|
|
907
925
|
|
|
926
|
+
/** Trailing history and dated expiries asked with others (`batch`) go out as batch requests while the service reads them so. */
|
|
927
|
+
batchesReads(method: BatchedReadMethod): boolean {
|
|
928
|
+
return apiClient.batchesReads(method === "getPriceHistory" ? "history" : "options");
|
|
929
|
+
}
|
|
930
|
+
|
|
908
931
|
async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
|
|
909
932
|
rejectPerpetual(ticker, exchange, "options chains");
|
|
910
933
|
const target = cloudInstrumentTarget(ticker, exchange);
|
|
911
934
|
return withCloudFallback(async () => {
|
|
912
|
-
const response = await apiClient.getCloudOptionsChain(target.symbol, target.exchange, expirationDate);
|
|
935
|
+
const response = await apiClient.getCloudOptionsChain(target.symbol, target.exchange, expirationDate, { batch: _context?.batch });
|
|
913
936
|
const chain = unwrapRequiredCloudResponse(
|
|
914
937
|
response,
|
|
915
938
|
`Cloud options chains are unavailable for ${ticker}`,
|
|
@@ -28,13 +28,34 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
28
28
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
type CloudNewsTickerLink = CloudNewsPayload["tickerLinks"][number];
|
|
32
|
+
|
|
33
|
+
const statesMarketCap = (link: CloudNewsTickerLink) => typeof link.marketCap === "number" && link.marketCap > 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One link a company: a symbol a story tags on several venues (ASML on
|
|
37
|
+
* Euronext Amsterdam and on NASDAQ) keeps its home listing, the line Gloom
|
|
38
|
+
* Cloud states the company's market cap on, else the venue tagged first. The
|
|
39
|
+
* tag stays where the company was first tagged.
|
|
40
|
+
*/
|
|
41
|
+
function homeListingLinks(links: readonly CloudNewsTickerLink[]): CloudNewsTickerLink[] {
|
|
42
|
+
const companies = new Map<string, CloudNewsTickerLink>();
|
|
43
|
+
links.forEach((link, index) => {
|
|
44
|
+
const symbol = link.symbol?.trim().toUpperCase() || `#${index}`;
|
|
45
|
+
const kept = companies.get(symbol);
|
|
46
|
+
if (!kept || (!statesMarketCap(kept) && statesMarketCap(link))) companies.set(symbol, link);
|
|
47
|
+
});
|
|
48
|
+
return [...companies.values()];
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
function mapCloudNewsTickers(
|
|
32
52
|
item: CloudNewsPayload,
|
|
33
53
|
fallbackTicker?: string,
|
|
34
54
|
): string[] {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
// Each company's listing: its canonical key, or its symbol on its venue.
|
|
56
|
+
const tickers = collectNewsDisplayTickers(homeListingLinks(item.tickerLinks).map((link) => (
|
|
57
|
+
link.canonicalTicker || (link.symbol && link.exchange ? `${link.symbol}:${link.exchange}` : link.symbol)
|
|
58
|
+
)));
|
|
38
59
|
const fallbackTickers = collectNewsDisplayTickers([fallbackTicker]);
|
|
39
60
|
if (tickers.length === 0) tickers.push(...fallbackTickers);
|
|
40
61
|
return tickers;
|
|
@@ -86,8 +86,8 @@ export function mapQuote(
|
|
|
86
86
|
const changePercent = !impossibleClose && typeof quote.changePercent === "number" && Number.isFinite(quote.changePercent)
|
|
87
87
|
? quote.changePercent
|
|
88
88
|
: Number.NaN;
|
|
89
|
-
//
|
|
90
|
-
//
|
|
89
|
+
// Generics, continuous symbols and listed contracts share their root's
|
|
90
|
+
// price basis: index futures in points and Treasury futures in 32nds.
|
|
91
91
|
const futuresBasis = isNonFutureType(quote.instrumentType) ? null
|
|
92
92
|
: futuresListingPriceBasis(quote.symbol, quote.listingExchangeName ?? quote.exchangeName);
|
|
93
93
|
const priceObservation = quote.priceBasis === "nav" ? "nav" : undefined;
|
|
@@ -234,6 +234,11 @@ function venueVolume(value: unknown): { venueVolume: { venue: string; volume: nu
|
|
|
234
234
|
: null;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/** What a served bar's volume counts, when it is a short plain phrase (`aggregate`, `3 spot venues`). */
|
|
238
|
+
function volumeBasis(value: unknown): { volumeBasis: string } | null {
|
|
239
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9 ]{0,31}$/i.test(value.trim()) ? { volumeBasis: value.trim() } : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
237
242
|
/**
|
|
238
243
|
* One served bar. `coin`: the bars are a coin's, which trades every hour of every day. Its intraday volume is the
|
|
239
244
|
* value traded in the bar on major spot venues, and a bar no venue reported has none rather than 0, so a 0 (from an
|
|
@@ -254,12 +259,17 @@ export function mapPricePoint(
|
|
|
254
259
|
close: normalizePriceValueByDivisor(point.close, divisor) ?? point.close,
|
|
255
260
|
volume: coin && point.volume === 0 ? undefined : point.volume,
|
|
256
261
|
...(venueVolume(point.venueVolume) ?? {}),
|
|
262
|
+
...(volumeBasis(point.volumeBasis) ?? {}),
|
|
257
263
|
...(point.partial === true ? { partial: true } : {}),
|
|
258
264
|
...(point.filled === true ? { filled: true } : {}),
|
|
259
265
|
...(point.unsettled === true ? { unsettled: true } : {}),
|
|
266
|
+
...(typeof point.contract === "string" && FUTURES_CONTRACT_CODE.test(point.contract) ? { contract: point.contract } : {}),
|
|
260
267
|
};
|
|
261
268
|
}
|
|
262
269
|
|
|
270
|
+
/** A listed contract as a continuous future's bar names it: root, month letter and year (NGZ26, ZQF27). */
|
|
271
|
+
const FUTURES_CONTRACT_CODE = /^[A-Z0-9]{1,4}[FGHJKMNQUVXZ]\d{2}$/;
|
|
272
|
+
|
|
263
273
|
/** Per-share bases the statistics block serves beside its multiples. */
|
|
264
274
|
const CLOUD_PER_SHARE_BASES = ["forwardEps", "dividendRate"] as const;
|
|
265
275
|
|
|
@@ -363,6 +373,11 @@ export function isEmptyCloudStatus(
|
|
|
363
373
|
return status === "empty" || status === "unsupported";
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
/** The service answered that its own source failed: `retryable_error`, or `fatal_error`, its catch-all for an upstream error. */
|
|
377
|
+
export function isCloudSourceFailure(status: CloudMarketResponse<unknown>["status"]): boolean {
|
|
378
|
+
return status === "retryable_error" || status === "fatal_error";
|
|
379
|
+
}
|
|
380
|
+
|
|
366
381
|
export function mapBatchError<T>(
|
|
367
382
|
item: CloudMarketBatchItem<T>,
|
|
368
383
|
fallbackMessage: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataServiceUnreachableError, isNetworkFailure } from "../utils/network-failure";
|
|
2
|
+
import { wasAskedTwice } from "../api-client/errors";
|
|
2
3
|
|
|
3
4
|
// Registered, so a miss raised by another copy of this module (a plugin that
|
|
4
5
|
// resolved `gloomberb/market-data` from the source tree) is still recognized.
|
|
@@ -129,6 +130,16 @@ export function isNoProviderMessage(message: string): boolean {
|
|
|
129
130
|
return /\bprovider available for\b/i.test(message) || notATickerSymbol(message) !== null;
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* What a read says when Gloom's data service answered that its source failed,
|
|
135
|
+
* having been asked twice (`readListing`): the listing is not said to have no
|
|
136
|
+
* data, since asked again in a moment it may. "for SYMBOL" closes the first
|
|
137
|
+
* sentence, so a table's note can name several symbols in front of it.
|
|
138
|
+
*/
|
|
139
|
+
export function serviceDidNotAnswerMessage(symbol?: string): string {
|
|
140
|
+
return `Gloom's data service did not answer${symbol ? ` for ${symbol}` : ""}. Try again in a moment.`;
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
/** The router's generic answer that no provider served a request, holding the failure behind it when a provider failed rather than missed. */
|
|
133
144
|
class NoProviderError extends Error {
|
|
134
145
|
constructor(message: string, readonly failure: unknown) {
|
|
@@ -139,9 +150,11 @@ class NoProviderError extends Error {
|
|
|
139
150
|
/**
|
|
140
151
|
* The router's final error when no provider answered. A reason a provider gave
|
|
141
152
|
* replaces the generic wording, and so does the service's answer that no
|
|
142
|
-
* listing exists for `symbol` ("Not a ticker"),
|
|
143
|
-
*
|
|
144
|
-
*
|
|
153
|
+
* listing exists for `symbol` ("Not a ticker"), a request that never reached
|
|
154
|
+
* the data service, and a listing read the data service failed to answer twice
|
|
155
|
+
* (`wasAskedTwice`, `serviceDidNotAnswerMessage`), each of which says that
|
|
156
|
+
* rather than that no source has the symbol; any other failure, a 5xx on a read
|
|
157
|
+
* that is not asked again among them, keeps the generic message.
|
|
145
158
|
*/
|
|
146
159
|
export function noProviderError(message: string, note: ProviderMissNote | undefined, symbol?: string): Error {
|
|
147
160
|
if (note?.reason) return createProviderMiss(note.reason, note.reason);
|
|
@@ -149,9 +162,19 @@ export function noProviderError(message: string, note: ProviderMissNote | undefi
|
|
|
149
162
|
if (isNetworkFailure(note?.failure)) {
|
|
150
163
|
return note!.failure instanceof DataServiceUnreachableError ? note!.failure : new DataServiceUnreachableError(note!.failure);
|
|
151
164
|
}
|
|
165
|
+
if (wasAskedTwice(note?.failure)) return new NoProviderError(serviceDidNotAnswerMessage(symbol), note!.failure);
|
|
152
166
|
return new NoProviderError(message, note?.failure);
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The router's answer that no source served a read because one failed (a
|
|
171
|
+
* timeout, a rate limit, a refused session), not because none has it: asked
|
|
172
|
+
* again shortly it may answer.
|
|
173
|
+
*/
|
|
174
|
+
export function isSourceFailure(error: unknown): boolean {
|
|
175
|
+
return error instanceof NoProviderError && error.failure !== undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
/**
|
|
156
179
|
* Whether a failed read means no source has what was asked for: a source's
|
|
157
180
|
* miss, or the router's answer that none served it when none failed another
|
|
@@ -781,6 +781,11 @@ export class ProviderRouterHistoryRoutes {
|
|
|
781
781
|
throw noProviderError(coin ? `No source could load history for ${request.target.symbol} just now` : request.missingProviderError,
|
|
782
782
|
attempts.misses, request.target.symbol);
|
|
783
783
|
}
|
|
784
|
+
// A window of dates no source could answer, because one failed rather than had nothing, is that failure:
|
|
785
|
+
// an empty answer would read as no bars in the window (`history --from` printing "0 bars").
|
|
786
|
+
if (!providerResult && attempts.misses.failure !== undefined && !reportedGaps.some((value) => value.points.length > 0)) {
|
|
787
|
+
throw noProviderError(`No source could load history for ${request.target.symbol} just now`, attempts.misses, request.target.symbol);
|
|
788
|
+
}
|
|
784
789
|
return withReportedGaps(providerResult?.value ?? { points: [], resolution: historyResolutionForInterval(request.interval) });
|
|
785
790
|
}
|
|
786
791
|
|
|
@@ -9,6 +9,7 @@ import { createDefaultConfig } from "../../types/config";
|
|
|
9
9
|
import type {
|
|
10
10
|
CachedAssetArgs,
|
|
11
11
|
CachedAssetMethod,
|
|
12
|
+
BatchedReadMethod,
|
|
12
13
|
CachedFinancialsTarget,
|
|
13
14
|
DataProvider,
|
|
14
15
|
MarketDataRequestContext,
|
|
@@ -631,6 +632,18 @@ export class AssetDataRouter implements DataProvider {
|
|
|
631
632
|
return this.providersInPriorityOrder()[0]?.name ?? null;
|
|
632
633
|
}
|
|
633
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Whether reads of `method` asked together go out as batch requests: only
|
|
637
|
+
* when every source the router could ask for them does, each broker that
|
|
638
|
+
* reads them and each provider. A caller asking many at once then never
|
|
639
|
+
* sends them one by one to a source that takes them one by one.
|
|
640
|
+
*/
|
|
641
|
+
batchesReads(method: BatchedReadMethod): boolean {
|
|
642
|
+
if (this.getBrokerCandidatesForContext(undefined, false).some(({ broker }) => typeof broker[method] === "function")) return false;
|
|
643
|
+
const providers = this.providersInPriorityOrder().filter((provider) => typeof provider[method] === "function");
|
|
644
|
+
return providers.length > 0 && providers.every((provider) => provider.batchesReads?.(method) === true);
|
|
645
|
+
}
|
|
646
|
+
|
|
634
647
|
private providersInPriorityOrder(): DataProvider[] {
|
|
635
648
|
const providers = [...this.sortedProviders()];
|
|
636
649
|
const fallbackProvider = this.fallbackSource?.market ?? null;
|
|
@@ -39,6 +39,7 @@ import { getBrokerInstance } from "../utils/broker-instances";
|
|
|
39
39
|
import { withoutMissingWatchlists } from "../portfolio/mutations";
|
|
40
40
|
import { mergeSuffixedListings, moveTargetWeights } from "../portfolio/suffixed-listings";
|
|
41
41
|
import { positionContractMultiplier } from "../portfolio/position-metrics";
|
|
42
|
+
import { reportTelemetryConfig } from "../telemetry/live-config";
|
|
42
43
|
import {
|
|
43
44
|
clearLeftoverBrokerHoldings,
|
|
44
45
|
knownBrokerHoldings,
|
|
@@ -421,9 +422,36 @@ function collectCoreConfigPayload(config: AppConfig) {
|
|
|
421
422
|
// Completion is monotonic in synced state. A device only advertises the
|
|
422
423
|
// completed state, while resumable progress and incomplete state stay local.
|
|
423
424
|
onboardingComplete: config.onboardingComplete === true ? true : undefined,
|
|
425
|
+
// Only whether Attention Counts is off: see mergedAttentionTelemetry.
|
|
426
|
+
attentionOff: config.telemetry?.attention === false,
|
|
424
427
|
});
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Attention Counts off belongs to the signed-in account, not to one device:
|
|
432
|
+
* an off made on another device turns it off here, and an explicit on made
|
|
433
|
+
* there afterwards turns it back on. The default never overrides either. An
|
|
434
|
+
* edit made here since the last sync wins, like any setting. No synced copy,
|
|
435
|
+
* or one from before `attentionOff` existed, reads as not off: a device's
|
|
436
|
+
* first sync takes an off already on the account, and keeps its own off
|
|
437
|
+
* instead of taking an on.
|
|
438
|
+
*/
|
|
439
|
+
function mergedAttentionTelemetry(
|
|
440
|
+
config: AppConfig,
|
|
441
|
+
baselineConfig: AppConfig,
|
|
442
|
+
payload: Record<string, unknown>,
|
|
443
|
+
lastSynced: Record<string, unknown> | null,
|
|
444
|
+
): AppConfig["telemetry"] {
|
|
445
|
+
// A copy pushed by an older build says nothing either way.
|
|
446
|
+
if (typeof payload.attentionOff !== "boolean") return config.telemetry;
|
|
447
|
+
const localOff = config.telemetry?.attention === false;
|
|
448
|
+
if (payload.attentionOff === localOff) return config.telemetry;
|
|
449
|
+
const editedHere = localOff !== (baselineConfig.telemetry?.attention === false)
|
|
450
|
+
|| localOff !== (lastSynced?.attentionOff === true);
|
|
451
|
+
if (editedHere) return config.telemetry;
|
|
452
|
+
return { ...config.telemetry, attention: !payload.attentionOff };
|
|
453
|
+
}
|
|
454
|
+
|
|
427
455
|
function positionValueBase(
|
|
428
456
|
ticker: TickerRecord,
|
|
429
457
|
position: TickerPosition,
|
|
@@ -957,6 +985,8 @@ function mergeConfigPayload(
|
|
|
957
985
|
assign("valueFlashingEnabled");
|
|
958
986
|
assign("recentTickers");
|
|
959
987
|
assign("recentCommands");
|
|
988
|
+
const telemetry = mergedAttentionTelemetry(config, baselineConfig, payload, lastSynced);
|
|
989
|
+
if (telemetry !== config.telemetry) next.telemetry = telemetry;
|
|
960
990
|
// Sync can complete onboarding on another device, but never reopen it.
|
|
961
991
|
// Resumable progress remains local until this installation completes it.
|
|
962
992
|
if (
|
|
@@ -1420,6 +1450,8 @@ export const coreConfigSyncContributor: SyncContributor = {
|
|
|
1420
1450
|
activeLayoutIndex: nextConfig.activeLayoutIndex,
|
|
1421
1451
|
});
|
|
1422
1452
|
}
|
|
1453
|
+
// An Attention Counts off from another device drops unsent counts before React commits.
|
|
1454
|
+
if (nextConfig.telemetry !== getState().config.telemetry) reportTelemetryConfig(nextConfig.telemetry);
|
|
1423
1455
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
1424
1456
|
if (!removed) scheduleConfigSave(nextConfig);
|
|
1425
1457
|
}
|
|
@@ -2,9 +2,11 @@ import { logFailure } from "../utils/log-failure";
|
|
|
2
2
|
import { apiClient } from "../api-client";
|
|
3
3
|
import type { AttentionAction, AttentionCountsPayload } from "../api-client/telemetry";
|
|
4
4
|
import { CHART_COMPOSER_PANE_ID, TICKER_RESEARCH_PANE_ID, type TelemetryConfig } from "../types/config";
|
|
5
|
+
import { attentionNoticeShown } from "./attention-setting";
|
|
5
6
|
import { telemetryOptedOut } from "./crash-reports";
|
|
6
|
-
import { subscribeTelemetryConfig } from "./live-config";
|
|
7
|
+
import { currentTelemetryConfig, subscribeTelemetryConfig } from "./live-config";
|
|
7
8
|
import { automationActive } from "./usage-counts";
|
|
9
|
+
import { cloudSyncController } from "../sync/controller";
|
|
8
10
|
|
|
9
11
|
const FLUSH_MS = 60_000;
|
|
10
12
|
const MAX_AGE_MS = 120_000;
|
|
@@ -13,12 +15,19 @@ const MAX_EVENTS = 100;
|
|
|
13
15
|
const SYMBOL = /^[A-Z0-9^][A-Z0-9.^_:/=-]{0,63}$/;
|
|
14
16
|
const ACTIONS = new Set<AttentionAction>(["des", "chart", "quote", "option_chain", "watchlist_add"]);
|
|
15
17
|
|
|
16
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Whether `account`'s ticker opens count. On by default: an explicit `true`
|
|
20
|
+
* counts, `false` never does, and while the setting is untouched the account
|
|
21
|
+
* counts only once this installation has drawn the notice for it.
|
|
22
|
+
*/
|
|
17
23
|
export function attentionCountsEnabled(
|
|
18
24
|
config: { telemetry?: TelemetryConfig } | null | undefined,
|
|
25
|
+
account: string | null,
|
|
19
26
|
env: Record<string, string | undefined> = {},
|
|
20
27
|
): boolean {
|
|
21
|
-
|
|
28
|
+
const telemetry = config?.telemetry;
|
|
29
|
+
if (telemetryOptedOut(env) || telemetry?.attention === false) return false;
|
|
30
|
+
return telemetry?.attention === true || attentionNoticeShown(telemetry, account);
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
interface AttentionHost {
|
|
@@ -136,26 +145,75 @@ export class AttentionCounter {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
let counter: AttentionCounter | null = null;
|
|
148
|
+
let collectionAllowed: (() => boolean) | null = null;
|
|
149
|
+
|
|
150
|
+
interface AttentionCounterOptions {
|
|
151
|
+
/** False under a privacy signal (`DO_NOT_TRACK`, browser Do Not Track or GPC): nothing counts and no notice shows. */
|
|
152
|
+
allowed(): boolean;
|
|
153
|
+
/** The config the surface launched with; the app's live switches replace it once reported. */
|
|
154
|
+
launchConfig(): { telemetry?: TelemetryConfig } | null | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* Whether this surface has the signed-in account's synced settings, so an
|
|
157
|
+
* Attention Counts off made on another device has arrived before anything
|
|
158
|
+
* counts. Defaults to this session's cloud sync having pulled them; without
|
|
159
|
+
* cloud sync nothing counts. A popped-out desktop window gets them from the
|
|
160
|
+
* main window instead.
|
|
161
|
+
*/
|
|
162
|
+
accountSettingsPulled?(): boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cloudSettingsPulled(): boolean {
|
|
166
|
+
return cloudSyncController.getStatus().lastPullAt !== null;
|
|
167
|
+
}
|
|
139
168
|
|
|
140
169
|
/** Installed only by interactive terminal, desktop and web entry points. */
|
|
141
|
-
export function installAttentionCounter(
|
|
170
|
+
export function installAttentionCounter({
|
|
171
|
+
allowed,
|
|
172
|
+
launchConfig,
|
|
173
|
+
accountSettingsPulled = cloudSettingsPulled,
|
|
174
|
+
}: AttentionCounterOptions): () => void {
|
|
142
175
|
counter?.dispose();
|
|
143
176
|
const next = new AttentionCounter({
|
|
144
|
-
isEnabled
|
|
177
|
+
isEnabled: () => allowed()
|
|
178
|
+
&& accountSettingsPulled()
|
|
179
|
+
&& attentionCountsEnabled(currentTelemetryConfig(launchConfig()), apiClient.getCurrentUser()?.id ?? null),
|
|
145
180
|
verifiedSession: () => apiClient.getVerifiedSessionIdentity(),
|
|
146
181
|
send: (payload, signal) => apiClient.reportAttentionCounts(payload, signal),
|
|
147
182
|
});
|
|
148
183
|
counter = next;
|
|
184
|
+
collectionAllowed = allowed;
|
|
149
185
|
const config = subscribeTelemetryConfig(() => next.refresh());
|
|
150
186
|
const auth = apiClient.subscribeCurrentUser(() => next.refresh());
|
|
151
187
|
return () => {
|
|
152
188
|
next.dispose();
|
|
153
189
|
config();
|
|
154
190
|
auth();
|
|
155
|
-
if (counter === next)
|
|
191
|
+
if (counter === next) {
|
|
192
|
+
counter = null;
|
|
193
|
+
collectionAllowed = null;
|
|
194
|
+
}
|
|
156
195
|
};
|
|
157
196
|
}
|
|
158
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Whether this surface counts at all: an interactive entry point installed
|
|
200
|
+
* the collector and no privacy signal is set. The notice shows only here, so
|
|
201
|
+
* CLI commands, `shot`, tests and opted-out environments never draw it.
|
|
202
|
+
*/
|
|
203
|
+
export function attentionCollectionAvailable(): boolean {
|
|
204
|
+
try {
|
|
205
|
+
return counter !== null && collectionAllowed?.() === true;
|
|
206
|
+
} catch {
|
|
207
|
+
// Intentional ignore: an unreadable privacy signal counts as set.
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Sends what the installed counter holds now instead of after its delay. */
|
|
213
|
+
export async function __flushAttentionCountsForTests(): Promise<void> {
|
|
214
|
+
await counter?.flush();
|
|
215
|
+
}
|
|
216
|
+
|
|
159
217
|
export function captureAttentionAction(): (symbol: string, action: AttentionAction) => void {
|
|
160
218
|
return counter?.capture() ?? (() => {});
|
|
161
219
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TelemetryConfig } from "../types/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Attention Counts setting, apart from the collector. It is on by
|
|
5
|
+
* default: `telemetry.attention: false` turns it off and `true` is an
|
|
6
|
+
* explicit on. While it is unset, an account counts only after this
|
|
7
|
+
* installation has drawn the one-time notice for it; an explicit choice
|
|
8
|
+
* either way never needs the notice. While signed in, an off is the
|
|
9
|
+
* account's: cloud sync carries it to the account's other devices
|
|
10
|
+
* (`mergedAttentionTelemetry` in sync/core-contributors.ts).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What switching off says, from the notice or the command bar: counting stops, published totals stay. */
|
|
14
|
+
export const ATTENTION_OFF_MESSAGE = "Attention Counts is off. Totals already published can't be withdrawn.";
|
|
15
|
+
|
|
16
|
+
/** How many accounts an installation remembers showing the notice to. */
|
|
17
|
+
export const ATTENTION_NOTICE_ACCOUNTS = 20;
|
|
18
|
+
|
|
19
|
+
export function attentionNoticeShown(telemetry: TelemetryConfig | undefined, account: string | null): boolean {
|
|
20
|
+
return account !== null && (telemetry?.attentionNoticeShownTo?.includes(account) ?? false);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether `account` is waiting for the notice: the setting is untouched and this installation never showed it. */
|
|
24
|
+
export function attentionNoticeDue(telemetry: TelemetryConfig | undefined, account: string | null): boolean {
|
|
25
|
+
return account !== null && telemetry?.attention === undefined && !attentionNoticeShown(telemetry, account);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Records that the notice was drawn for `account`. The same object when it already was. */
|
|
29
|
+
export function withAttentionNoticeShown(telemetry: TelemetryConfig | undefined, account: string): TelemetryConfig | undefined {
|
|
30
|
+
if (attentionNoticeShown(telemetry, account)) return telemetry;
|
|
31
|
+
const accounts = [...(telemetry?.attentionNoticeShownTo ?? []), account].slice(-ATTENTION_NOTICE_ACCOUNTS);
|
|
32
|
+
return { ...telemetry, attentionNoticeShownTo: accounts };
|
|
33
|
+
}
|
|
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, release } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { writePrivateFileSync } from "../utils/private-files";
|
|
5
6
|
import type { CrashReportSurface } from "../api-client";
|
|
6
7
|
import type { TelemetryConfig } from "../types/config";
|
|
7
8
|
import {
|
|
@@ -109,7 +110,8 @@ export function createNodeCrashReporterHost(options: NodeCrashReporterHostOption
|
|
|
109
110
|
const dir = dataDir();
|
|
110
111
|
if (!dir) return;
|
|
111
112
|
const reports = [...readSpool(dir), report].slice(-MAX_SPOOLED_REPORTS);
|
|
112
|
-
|
|
113
|
+
// Readable by the user alone: a report's stack names the user's files.
|
|
114
|
+
writePrivateFileSync(spoolPath(dir), JSON.stringify(reports));
|
|
113
115
|
},
|
|
114
116
|
clearSpool: () => {
|
|
115
117
|
const dir = dataDir();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
|
+
import type { StallClockReading } from "../utils/main-thread-stall";
|
|
3
|
+
|
|
4
|
+
/** macOS `clock_gettime_nsec_np` clocks: awake time (stops in sleep), and this thread's CPU time. */
|
|
5
|
+
const DARWIN_CLOCK_UPTIME_RAW = 8;
|
|
6
|
+
const DARWIN_CLOCK_THREAD_CPUTIME_ID = 16;
|
|
7
|
+
/** Linux `clock_gettime` clocks: awake time (stops in suspend), and this thread's CPU time. */
|
|
8
|
+
const LINUX_CLOCK_MONOTONIC = 1;
|
|
9
|
+
const LINUX_CLOCK_THREAD_CPUTIME_ID = 3;
|
|
10
|
+
/** Windows counts both in 100 ns units. */
|
|
11
|
+
const WINDOWS_TICKS_PER_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
type PlatformClocks = () => Pick<StallClockReading, "awakeMs" | "threadCpuMs">;
|
|
14
|
+
|
|
15
|
+
function processCpuMs(): number | null {
|
|
16
|
+
if (typeof process.cpuUsage !== "function") return null;
|
|
17
|
+
const usage = process.cpuUsage();
|
|
18
|
+
return (usage.user + usage.system) / 1_000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Windows: QueryUnbiasedInterruptTime leaves out sleep and hibernation (the
|
|
23
|
+
* clock timers read keeps counting through them), and GetThreadTimes gives
|
|
24
|
+
* the calling thread's kernel and user time.
|
|
25
|
+
*/
|
|
26
|
+
function windowsClocks(): PlatformClocks {
|
|
27
|
+
const kernel32 = dlopen("kernel32.dll", {
|
|
28
|
+
QueryUnbiasedInterruptTime: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
29
|
+
// A handle as a 64-bit integer: the pseudo-handle is -2, which a pointer as a JS number would not keep exactly.
|
|
30
|
+
GetCurrentThread: { args: [], returns: FFIType.u64 },
|
|
31
|
+
GetThreadTimes: { args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.bool },
|
|
32
|
+
});
|
|
33
|
+
// A pseudo-handle: whichever thread passes it means itself.
|
|
34
|
+
const thread = kernel32.symbols.GetCurrentThread();
|
|
35
|
+
const awake = new BigUint64Array(1);
|
|
36
|
+
// Creation, exit, kernel and user time.
|
|
37
|
+
const times = new BigUint64Array(4);
|
|
38
|
+
return () => ({
|
|
39
|
+
awakeMs: kernel32.symbols.QueryUnbiasedInterruptTime(ptr(awake)) ? Number(awake[0]!) / WINDOWS_TICKS_PER_MS : null,
|
|
40
|
+
threadCpuMs: thread && kernel32.symbols.GetThreadTimes(thread, ptr(times), ptr(times, 8), ptr(times, 16), ptr(times, 24))
|
|
41
|
+
? Number(times[2]! + times[3]!) / WINDOWS_TICKS_PER_MS
|
|
42
|
+
: null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function darwinClocks(): PlatformClocks {
|
|
47
|
+
const libc = dlopen("/usr/lib/libSystem.B.dylib", {
|
|
48
|
+
clock_gettime_nsec_np: { args: [FFIType.u32], returns: FFIType.u64 },
|
|
49
|
+
});
|
|
50
|
+
const read = (clock: number): number | null => {
|
|
51
|
+
const nanoseconds = Number(libc.symbols.clock_gettime_nsec_np(clock));
|
|
52
|
+
return nanoseconds > 0 ? nanoseconds / 1e6 : null;
|
|
53
|
+
};
|
|
54
|
+
return () => ({ awakeMs: read(DARWIN_CLOCK_UPTIME_RAW), threadCpuMs: read(DARWIN_CLOCK_THREAD_CPUTIME_ID) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function linuxClocks(): PlatformClocks {
|
|
58
|
+
const libc = dlopen("libc.so.6", {
|
|
59
|
+
clock_gettime: { args: [FFIType.i32, FFIType.ptr], returns: FFIType.i32 },
|
|
60
|
+
});
|
|
61
|
+
// struct timespec: seconds, nanoseconds.
|
|
62
|
+
const time = new BigInt64Array(2);
|
|
63
|
+
const read = (clock: number): number | null => (
|
|
64
|
+
libc.symbols.clock_gettime(clock, ptr(time)) === 0 ? Number(time[0]!) * 1_000 + Number(time[1]!) / 1e6 : null
|
|
65
|
+
);
|
|
66
|
+
return () => ({ awakeMs: read(LINUX_CLOCK_MONOTONIC), threadCpuMs: read(LINUX_CLOCK_THREAD_CPUTIME_ID) });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function platformClocks(): PlatformClocks | null {
|
|
70
|
+
try {
|
|
71
|
+
if (process.platform === "win32") return windowsClocks();
|
|
72
|
+
if (process.platform === "darwin") return darwinClocks();
|
|
73
|
+
if (process.platform === "linux") return linuxClocks();
|
|
74
|
+
} catch {
|
|
75
|
+
// Intentional ignore: without the platform's clocks a stall still counts, only its cause stays unknown.
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The clocks the stall monitor reads on each check, for the thread that
|
|
82
|
+
* calls this: the thread's CPU time is the caller's own. A platform whose
|
|
83
|
+
* clocks cannot be loaded gives the process's CPU time alone.
|
|
84
|
+
*/
|
|
85
|
+
export function createStallClocks(): () => StallClockReading {
|
|
86
|
+
const platform = platformClocks();
|
|
87
|
+
return () => ({
|
|
88
|
+
awakeMs: null,
|
|
89
|
+
threadCpuMs: null,
|
|
90
|
+
...platform?.(),
|
|
91
|
+
processCpuMs: processCpuMs(),
|
|
92
|
+
});
|
|
93
|
+
}
|