gloomberb 0.18.4 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +169 -5
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +31 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +171 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +13 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +6 -12
- package/src/api-client/types.ts +74 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +42 -27
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +1 -1
- 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/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +138 -46
- package/src/cli/commands/options-chain.ts +107 -10
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +176 -39
- package/src/cli/commands/search.ts +39 -3
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +57 -35
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +10 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +73 -1
- package/src/cli/history-rows.ts +87 -16
- package/src/cli/index.ts +2 -0
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +195 -56
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +111 -16
- package/src/cli/pane-functions/function-help.ts +23 -13
- package/src/cli/pane-functions/headless.ts +56 -12
- package/src/cli/pane-functions/index.ts +58 -16
- package/src/cli/pane-functions/market-session.ts +81 -9
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +35 -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 +30 -13
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +26 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +61 -5
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +46 -7
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- 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/types.ts +4 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/display.tsx +5 -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/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/saved-copies.ts +55 -16
- 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 +427 -216
- package/src/i18n/es.ts +19 -3
- package/src/i18n/ja.ts +19 -3
- package/src/i18n/ko.ts +19 -3
- package/src/i18n/zh-cn.ts +19 -3
- package/src/i18n/zh-tw.ts +19 -3
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +48 -7
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +141 -4
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +380 -82
- package/src/plugins/builtin/alerts/command.ts +31 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +113 -8
- package/src/plugins/builtin/alerts/format.ts +80 -4
- package/src/plugins/builtin/alerts/headless.ts +30 -7
- package/src/plugins/builtin/alerts/history-rows.ts +7 -3
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +135 -22
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +134 -0
- package/src/plugins/builtin/alerts/metric-data.ts +111 -0
- package/src/plugins/builtin/alerts/metric-model.ts +324 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +90 -39
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +37 -10
- package/src/plugins/builtin/alerts/types.ts +7 -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/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +10 -5
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- 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 +7 -3
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
- package/src/plugins/builtin/chart-composer/headless.ts +19 -3
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +4 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
- package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- 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 +70 -15
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- 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 +11 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
- package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +36 -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/pane.tsx +7 -5
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +54 -6
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +6 -9
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +42 -12
- package/src/plugins/builtin/options-positioning/client.ts +30 -0
- package/src/plugins/builtin/options-positioning/headless.ts +74 -21
- package/src/plugins/builtin/options-positioning/index.tsx +4 -4
- package/src/plugins/builtin/options-positioning/model.ts +122 -14
- package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
- 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/freshness.ts +12 -10
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +32 -31
- 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 +12 -7
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- 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 +40 -6
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +138 -0
- package/src/plugins/builtin/scanner/live-model.ts +362 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- 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 +10 -3
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +17 -8
- package/src/plugins/builtin/themes/pane.tsx +7 -7
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +9 -17
- package/src/plugins/builtin/vol-surface/headless.ts +63 -14
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +4 -4
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +5 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/remote/resources.ts +2 -1
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +30 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +149 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +70 -19
- package/src/sync/core-contributors.ts +90 -31
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- 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/telemetry/usage-counts.ts +0 -18
- package/src/test-support/preload.ts +14 -0
- package/src/theme/colors.ts +8 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +103 -3
- package/src/tickers/listing-venues.ts +38 -3
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +2 -4
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +198 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +7 -1
- package/src/time-series/resolve.ts +116 -15
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +5 -3
- package/src/time-series/studies.ts +278 -74
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +3 -3
- package/src/time-series/types.ts +27 -1
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +73 -2
- package/src/types/headless.ts +61 -5
- package/src/types/instrument.ts +3 -0
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +10 -1
- package/src/ui/host.tsx +3 -0
- package/src/updater/command-names.ts +12 -7
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +12 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -21,7 +21,8 @@ import type {
|
|
|
21
21
|
} from "../../types/data-provider";
|
|
22
22
|
import type { CapabilityRouteSource } from "../../types/capability-route-source";
|
|
23
23
|
import { routeSourcePriority } from "../../types/capability-route-source";
|
|
24
|
-
import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, QuoteMetadata, TickerFinancials } from "../../types/financials";
|
|
24
|
+
import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, QuoteContributionMap, QuoteMetadata, TickerFinancials } from "../../types/financials";
|
|
25
|
+
import { datedCloseListingKey, PreviousCloseDates, type DatedCloseListing } from "../../market-data/market/dated-previous-close";
|
|
25
26
|
import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
|
|
26
27
|
import type { NewsArticle, NewsQuery } from "../../news/types";
|
|
27
28
|
import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
|
|
@@ -52,6 +53,7 @@ import { ProviderRouterSearchRoutes } from "./search";
|
|
|
52
53
|
import { collectCapabilityRouteSources, normalizeRouteSource } from "./sources";
|
|
53
54
|
import type { ProviderRouterCoreDeps } from "./route-types";
|
|
54
55
|
import { withProviderConnectionHealth } from "./connection-health";
|
|
56
|
+
import { withFiledTrailingEps } from "../../time-series/filed-trailing-eps";
|
|
55
57
|
import {
|
|
56
58
|
contextFromCachedTarget,
|
|
57
59
|
getBrokerCandidates,
|
|
@@ -64,6 +66,14 @@ import {
|
|
|
64
66
|
|
|
65
67
|
const providerLog = debugLog.createLogger("asset-data-router");
|
|
66
68
|
|
|
69
|
+
/**
|
|
70
|
+
* How long a short-lived process (the CLI) waits in all, per request that
|
|
71
|
+
* prints what its moves are from, for the daily closes that date them. The
|
|
72
|
+
* long-lived app never waits: it answers with what was read and dates the
|
|
73
|
+
* copies after.
|
|
74
|
+
*/
|
|
75
|
+
const CHANGE_BASIS_WAIT_MS = 2_500;
|
|
76
|
+
|
|
67
77
|
export class AssetDataRouter implements DataProvider {
|
|
68
78
|
readonly id = "asset-data-router";
|
|
69
79
|
readonly name = "Asset Data Router";
|
|
@@ -86,6 +96,18 @@ export class AssetDataRouter implements DataProvider {
|
|
|
86
96
|
private realtimeCloudAccess: RealtimeCloudAccess = createSignedInRealtimeCloudAccess();
|
|
87
97
|
/** A short-lived process exits before a background refresh can land, so it must await stale entries. */
|
|
88
98
|
private revalidateInBackground = true;
|
|
99
|
+
/**
|
|
100
|
+
* Every quote this router answers names the day its previous close is from,
|
|
101
|
+
* where the venue's calendar cannot. The closes come from the providers
|
|
102
|
+
* alone, the chart's month of them shared: a broker is never asked once
|
|
103
|
+
* per listing. A read that lands dates the latest copy for its listing's
|
|
104
|
+
* stream subscribers.
|
|
105
|
+
*/
|
|
106
|
+
private readonly previousCloseDates = new PreviousCloseDates(
|
|
107
|
+
(listing) => this.historyRoutes.getPriceHistory(listing.symbol, listing.exchange, "1M", listingHistoryContext(listing.context), { askBrokers: false }),
|
|
108
|
+
{ onDated: (listing, quote) => { for (const listener of this.datedQuoteListeners.get(datedCloseListingKey(listing)) ?? []) listener(quote); } },
|
|
109
|
+
);
|
|
110
|
+
private readonly datedQuoteListeners = new Map<string, Set<(quote: Quote) => void>>();
|
|
89
111
|
|
|
90
112
|
constructor(
|
|
91
113
|
fallbackSource: CapabilityRouteSource | DataProvider | null = null,
|
|
@@ -188,7 +210,13 @@ export class AssetDataRouter implements DataProvider {
|
|
|
188
210
|
options: { allowExpired?: boolean; includeStaleQuotes?: boolean } = {},
|
|
189
211
|
): Map<string, TickerFinancials> {
|
|
190
212
|
const { valid } = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, contextFromCachedTarget(target)));
|
|
191
|
-
|
|
213
|
+
const cached = this.financialRoutes.getCachedFinancialsForTargets(valid, options);
|
|
214
|
+
for (const target of valid) {
|
|
215
|
+
const key = target.symbol.toUpperCase();
|
|
216
|
+
const financials = cached.get(key);
|
|
217
|
+
if (financials) cached.set(key, withFiledTrailingEps(this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target)))));
|
|
218
|
+
}
|
|
219
|
+
return cached;
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
getCachedExchangeRates(currencies: string[], options: { allowExpired?: boolean } = {}): Map<string, number> {
|
|
@@ -200,7 +228,18 @@ export class AssetDataRouter implements DataProvider {
|
|
|
200
228
|
options: QuoteBatchOptions = {},
|
|
201
229
|
): Promise<QuoteBatchResult[]> {
|
|
202
230
|
const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, target.context));
|
|
203
|
-
const
|
|
231
|
+
const answered = await this.batchRoutes.getQuotesBatch(mapped.valid, withoutDating(options));
|
|
232
|
+
const quoted = answered.flatMap((result) => result.quote ? [{ quote: result.quote, listing: listingOf(result.target, result.target.context) }] : []);
|
|
233
|
+
// A cache-only answer never reaches the network, so it is dated only by what was read already.
|
|
234
|
+
const dated = options.cacheOnly
|
|
235
|
+
? quoted.map(({ quote, listing }) => this.previousCloseDates.known(quote, listing))
|
|
236
|
+
: await this.datedAnswers(quoted, options.datesChangeBasis, options.signal);
|
|
237
|
+
let next = 0;
|
|
238
|
+
const results = answered.map((result) => {
|
|
239
|
+
if (!result.quote) return result;
|
|
240
|
+
const quote = dated[next++]!;
|
|
241
|
+
return quote === result.quote ? result : { ...result, quote };
|
|
242
|
+
});
|
|
204
243
|
const restored = new Map<QuoteSubscriptionTarget, QuoteBatchResult>(results.map((result) => {
|
|
205
244
|
const target = mapped.original(result.target);
|
|
206
245
|
return [target, { ...result, target }] as const;
|
|
@@ -216,10 +255,20 @@ export class AssetDataRouter implements DataProvider {
|
|
|
216
255
|
|
|
217
256
|
async getTickerFinancialsBatch(
|
|
218
257
|
targets: CachedFinancialsTarget[],
|
|
219
|
-
options: { forceRefresh?: boolean } = {},
|
|
258
|
+
options: { forceRefresh?: boolean; datesChangeBasis?: boolean } = {},
|
|
220
259
|
): Promise<TickerFinancialsBatchResult[]> {
|
|
221
260
|
const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, contextFromCachedTarget(target)));
|
|
222
|
-
const
|
|
261
|
+
const answered = await this.batchRoutes.getTickerFinancialsBatch(mapped.valid, { forceRefresh: options.forceRefresh });
|
|
262
|
+
const listingOfResult = (result: TickerFinancialsBatchResult) => listingOf(result.target, contextFromCachedTarget(result.target));
|
|
263
|
+
const quoted = answered.flatMap((result) => result.financials?.quote ? [{ quote: result.financials.quote, listing: listingOfResult(result) }] : []);
|
|
264
|
+
const dated = await this.datedAnswers(quoted, options.datesChangeBasis);
|
|
265
|
+
let next = 0;
|
|
266
|
+
const results = answered.map((result) => {
|
|
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);
|
|
270
|
+
return financials === result.financials ? result : { ...result, financials };
|
|
271
|
+
});
|
|
223
272
|
const restored = new Map(results.map((result) => {
|
|
224
273
|
const target = mapped.original(result.target);
|
|
225
274
|
return [target, { ...result, target }] as const;
|
|
@@ -228,14 +277,56 @@ export class AssetDataRouter implements DataProvider {
|
|
|
228
277
|
return targets.map((target) => restored.get(target) ?? { target, financials: null });
|
|
229
278
|
}
|
|
230
279
|
|
|
231
|
-
async getTickerFinancials(ticker: string, exchange?: string,
|
|
280
|
+
async getTickerFinancials(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<TickerFinancials> {
|
|
281
|
+
const { context, datesChangeBasis } = splitDating(requestContext);
|
|
232
282
|
exchange = this.resolvePublicExchange(ticker, exchange, context);
|
|
233
|
-
|
|
283
|
+
const financials = await this.financialRoutes.getTickerFinancials(ticker, exchange, context);
|
|
284
|
+
if (!financials.quote) return withFiledTrailingEps(financials);
|
|
285
|
+
const listing = listingOf({ symbol: ticker, exchange }, context);
|
|
286
|
+
const [quote] = await this.datedAnswers([{ quote: financials.quote, listing }], datesChangeBasis);
|
|
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));
|
|
234
289
|
}
|
|
235
290
|
|
|
236
|
-
async getQuote(ticker: string, exchange?: string,
|
|
291
|
+
async getQuote(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<Quote> {
|
|
292
|
+
const { context, datesChangeBasis } = splitDating(requestContext);
|
|
237
293
|
exchange = this.resolvePublicExchange(ticker, exchange, context);
|
|
238
|
-
|
|
294
|
+
const quote = await this.financialRoutes.getQuote(ticker, exchange, context);
|
|
295
|
+
return (await this.datedAnswers([{ quote, listing: listingOf({ symbol: ticker, exchange }, context) }], datesChangeBasis))[0]!;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The quotes as this router answers them, their previous closes dated
|
|
300
|
+
* (PreviousCloseDates). The long-lived app never waits: each is dated by
|
|
301
|
+
* what was read already, and the reads it lacks start for the copies after
|
|
302
|
+
* it. A short-lived process starts reads only for a request that prints
|
|
303
|
+
* what its moves are from (`datesChangeBasis`), and waits for them a few
|
|
304
|
+
* seconds in all.
|
|
305
|
+
*/
|
|
306
|
+
private async datedAnswers(
|
|
307
|
+
entries: Array<{ quote: Quote; listing: DatedCloseListing }>,
|
|
308
|
+
datesChangeBasis?: boolean,
|
|
309
|
+
signal?: AbortSignal,
|
|
310
|
+
): Promise<Quote[]> {
|
|
311
|
+
if (this.revalidateInBackground) return entries.map(({ quote, listing }) => this.previousCloseDates.observe(quote, listing));
|
|
312
|
+
if (datesChangeBasis) await this.previousCloseDates.settle(entries, { waitMs: CHANGE_BASIS_WAIT_MS, signal });
|
|
313
|
+
return entries.map(({ quote, listing }) => this.previousCloseDates.known(quote, listing));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The financials with their dated quote, and the contributions it is resolved from dated alike. */
|
|
317
|
+
private withDatedQuote(financials: TickerFinancials, quote: Quote, listing: DatedCloseListing): TickerFinancials {
|
|
318
|
+
return quote === financials.quote ? financials : { ...this.knownFinancials(financials, listing), quote };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The financials dated by what was read already: their quote and the contributions it is resolved from. */
|
|
322
|
+
private knownFinancials(financials: TickerFinancials, listing: DatedCloseListing): TickerFinancials {
|
|
323
|
+
const quote = this.previousCloseDates.known(financials.quote, listing);
|
|
324
|
+
const contributions = financials.quoteContributions;
|
|
325
|
+
const quoteContributions = contributions && Object.fromEntries(Object.entries(contributions)
|
|
326
|
+
.map(([key, contribution]) => [key, this.previousCloseDates.known(contribution, listing)])) as QuoteContributionMap;
|
|
327
|
+
const contributionsChanged = !!contributions && Object.keys(contributions).some((key) => quoteContributions![key] !== contributions[key]);
|
|
328
|
+
if (quote === financials.quote && !contributionsChanged) return financials;
|
|
329
|
+
return { ...financials, quote, ...(contributionsChanged ? { quoteContributions } : {}) };
|
|
239
330
|
}
|
|
240
331
|
|
|
241
332
|
async getQuoteMetadata(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<QuoteMetadata | null> {
|
|
@@ -569,6 +660,54 @@ export class AssetDataRouter implements DataProvider {
|
|
|
569
660
|
onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
|
|
570
661
|
): () => void {
|
|
571
662
|
const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, target.context));
|
|
572
|
-
|
|
663
|
+
// A read that lands dates the latest copy of a quote for the listing's subscribers.
|
|
664
|
+
const listeners = mapped.valid.map((target) => {
|
|
665
|
+
const key = datedCloseListingKey(listingOf(target));
|
|
666
|
+
const listener = (quote: Quote) => onQuote(mapped.original(target), quote);
|
|
667
|
+
const set = this.datedQuoteListeners.get(key) ?? new Set();
|
|
668
|
+
set.add(listener);
|
|
669
|
+
this.datedQuoteListeners.set(key, set);
|
|
670
|
+
return { key, listener };
|
|
671
|
+
});
|
|
672
|
+
const unsubscribe = this.streamingRoutes.subscribeQuotes(mapped.valid, (target, quote) => {
|
|
673
|
+
// A tick is dated by what was read already; one not yet read is read for the ticks after it.
|
|
674
|
+
onQuote(mapped.original(target), this.previousCloseDates.observe(quote, listingOf(target, target.context)));
|
|
675
|
+
});
|
|
676
|
+
return () => {
|
|
677
|
+
unsubscribe();
|
|
678
|
+
for (const { key, listener } of listeners) {
|
|
679
|
+
const set = this.datedQuoteListeners.get(key);
|
|
680
|
+
set?.delete(listener);
|
|
681
|
+
if (set?.size === 0) this.datedQuoteListeners.delete(key);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
573
684
|
}
|
|
574
685
|
}
|
|
686
|
+
|
|
687
|
+
function listingOf(target: { symbol: string; exchange?: string }, context?: MarketDataRequestContext): DatedCloseListing {
|
|
688
|
+
return { symbol: target.symbol, exchange: target.exchange ?? "", ...(context ? { context } : {}) };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A request's context without the dating flag, which no source reads. */
|
|
692
|
+
function splitDating(requestContext?: MarketDataRequestContext): { context?: MarketDataRequestContext; datesChangeBasis: boolean } {
|
|
693
|
+
if (!requestContext || !("datesChangeBasis" in requestContext)) return { context: requestContext, datesChangeBasis: false };
|
|
694
|
+
const { datesChangeBasis, ...context } = requestContext;
|
|
695
|
+
return { context: Object.keys(context).length ? context : undefined, datesChangeBasis: datesChangeBasis === true };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function withoutDating(options: QuoteBatchOptions): QuoteBatchOptions {
|
|
699
|
+
if (!("datesChangeBasis" in options)) return options;
|
|
700
|
+
const { datesChangeBasis: _dates, ...rest } = options;
|
|
701
|
+
return rest;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* The context a listing's daily closes are read with: its broker contract, so
|
|
706
|
+
* the chart's copy of them is shared, and nothing about the quote request.
|
|
707
|
+
*/
|
|
708
|
+
function listingHistoryContext(context?: MarketDataRequestContext): MarketDataRequestContext | undefined {
|
|
709
|
+
if (!context) return undefined;
|
|
710
|
+
const { brokerId, brokerInstanceId, instrument } = context;
|
|
711
|
+
const history = { ...(brokerId ? { brokerId } : {}), ...(brokerInstanceId ? { brokerInstanceId } : {}), ...(instrument ? { instrument } : {}) };
|
|
712
|
+
return Object.keys(history).length ? history : undefined;
|
|
713
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { BrokerCandidate } from "./brokers";
|
|
|
5
5
|
import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE, shouldLogProviderError } from "../provider-errors";
|
|
6
6
|
import { searchInstrumentKey } from "../../tickers/search/identity";
|
|
7
7
|
import { OperationTimeoutError, settleWithin, withDeadline } from "../../utils/async-deadline";
|
|
8
|
+
import { isNetworkFailure } from "../../utils/network-failure";
|
|
8
9
|
|
|
9
10
|
const SEARCH_CACHE_TTL_MS = 30_000;
|
|
10
11
|
const SEARCH_CACHE_MAX_ENTRIES = 100;
|
|
@@ -142,6 +143,9 @@ export class ProviderRouterSearchRoutes {
|
|
|
142
143
|
* A search nothing answered because a provider was busy (rate limited,
|
|
143
144
|
* overloaded, out of time) rejects with a `ProviderBusyError` instead of
|
|
144
145
|
* reading as an empty one, and is not cached, so asking again asks again.
|
|
146
|
+
* One nothing answered because the data service could not be reached
|
|
147
|
+
* (offline, a refused or hung connection) rejects with that failure: it is
|
|
148
|
+
* no answer, not a search that found nothing.
|
|
145
149
|
*/
|
|
146
150
|
private fetchSearchResults(
|
|
147
151
|
query: string,
|
|
@@ -172,6 +176,7 @@ export class ProviderRouterSearchRoutes {
|
|
|
172
176
|
});
|
|
173
177
|
let answered = false;
|
|
174
178
|
let busy = false;
|
|
179
|
+
let unreachable: unknown = null;
|
|
175
180
|
|
|
176
181
|
const record = (items: InstrumentSearchResult[] | null) => {
|
|
177
182
|
if (!items?.length) return;
|
|
@@ -204,8 +209,10 @@ export class ProviderRouterSearchRoutes {
|
|
|
204
209
|
record(await withDeadline(provider.search(query, context), timeoutMs, `${provider.id} search timed out`));
|
|
205
210
|
} catch (error) {
|
|
206
211
|
const timedOut = error instanceof OperationTimeoutError;
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
if (isNetworkFailure(error)) {
|
|
213
|
+
unreachable ??= error;
|
|
214
|
+
} else if (isProviderBusy(error) || (timedOut && !context?.interactive)) {
|
|
215
|
+
// A keystroke gives up on a slow source by design and asks again on the next one; only a full wait is busy.
|
|
209
216
|
busy = true;
|
|
210
217
|
} else if (!timedOut && shouldLogProviderError(error)) {
|
|
211
218
|
this.deps.logProviderError(`${provider.id} failed: ${error}`);
|
|
@@ -231,6 +238,10 @@ export class ProviderRouterSearchRoutes {
|
|
|
231
238
|
})().then(() => {
|
|
232
239
|
if (answered) return;
|
|
233
240
|
answered = true;
|
|
241
|
+
if (unreachable) {
|
|
242
|
+
rejectFirst(unreachable);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
234
245
|
if (busy) {
|
|
235
246
|
rejectFirst(new ProviderBusyError(SEARCH_BUSY_MESSAGE));
|
|
236
247
|
return;
|
|
@@ -495,6 +495,7 @@ export function AppProvider({
|
|
|
495
495
|
paneState: currentState.paneState,
|
|
496
496
|
focusedPaneId: currentState.focusedPaneId,
|
|
497
497
|
statusBarVisible: currentState.statusBarVisible,
|
|
498
|
+
brokerSyncErrors: currentState.brokerSyncErrors,
|
|
498
499
|
}), []);
|
|
499
500
|
|
|
500
501
|
const serializeDesktopSnapshot = useCallback((snapshot: DesktopSharedStateSnapshot): string => JSON.stringify({
|
|
@@ -502,6 +503,7 @@ export function AppProvider({
|
|
|
502
503
|
paneState: snapshot.paneState,
|
|
503
504
|
focusedPaneId: snapshot.focusedPaneId,
|
|
504
505
|
statusBarVisible: snapshot.statusBarVisible,
|
|
506
|
+
brokerSyncErrors: snapshot.brokerSyncErrors ?? {},
|
|
505
507
|
}), []);
|
|
506
508
|
|
|
507
509
|
const serializePaneState = useCallback((paneState: PaneRuntimeState | undefined): string => JSON.stringify(paneState ?? {}), []);
|
|
@@ -594,6 +596,7 @@ export function AppProvider({
|
|
|
594
596
|
state.focusedPaneId,
|
|
595
597
|
state.paneState,
|
|
596
598
|
state.statusBarVisible,
|
|
599
|
+
state.brokerSyncErrors,
|
|
597
600
|
]);
|
|
598
601
|
|
|
599
602
|
useEffect(() => {
|
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
export interface CloudSessionLifecycle {
|
|
2
|
-
|
|
2
|
+
getCurrentUser(): { id: string } | null;
|
|
3
|
+
isSignedIn(): boolean;
|
|
4
|
+
getVerifiedSessionIdentity(): object | null;
|
|
3
5
|
subscribeCurrentUser(listener: () => void): () => void;
|
|
4
6
|
}
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
* Runs exactly when a signed-in desktop session becomes verified. It deliberately
|
|
8
|
-
* ignores ordinary account and entitlement refreshes so they cannot re-run the
|
|
9
|
-
* initial sync pull and push.
|
|
10
|
-
*/
|
|
8
|
+
/** Starts a first merge for a newly verified account, ignoring ordinary user and entitlement refreshes. */
|
|
11
9
|
export function subscribeToCloudVerification(
|
|
12
10
|
session: CloudSessionLifecycle,
|
|
13
11
|
onVerified: () => void,
|
|
12
|
+
onAccountChange: (accountId: string) => void,
|
|
13
|
+
onSuspended?: () => void,
|
|
14
14
|
): () => void {
|
|
15
|
-
let
|
|
15
|
+
let accountId = session.getCurrentUser()?.id ?? null;
|
|
16
|
+
let wasVerified = session.getVerifiedSessionIdentity() !== null;
|
|
16
17
|
return session.subscribeCurrentUser(() => {
|
|
17
|
-
const isVerified = session.
|
|
18
|
-
const
|
|
18
|
+
const isVerified = session.getVerifiedSessionIdentity() !== null;
|
|
19
|
+
const nextAccountId = isVerified ? session.getCurrentUser()?.id ?? null : null;
|
|
20
|
+
const accountChanged = nextAccountId !== null && nextAccountId !== accountId;
|
|
21
|
+
const becameVerified = isVerified && (!wasVerified || accountChanged);
|
|
22
|
+
const becameSuspended = wasVerified && !isVerified;
|
|
23
|
+
const resetAccount = accountChanged && accountId !== null;
|
|
24
|
+
// Sign-out and failed validation retire pending work, but keep the last
|
|
25
|
+
// account's merge history until a different account is confirmed.
|
|
26
|
+
if (nextAccountId !== null) accountId = nextAccountId;
|
|
19
27
|
wasVerified = isVerified;
|
|
28
|
+
if (becameSuspended) onSuspended?.();
|
|
29
|
+
if (resetAccount) onAccountChange(nextAccountId!);
|
|
20
30
|
if (becameVerified) onVerified();
|
|
21
31
|
});
|
|
22
32
|
}
|
package/src/sync/baseline.ts
CHANGED
|
@@ -4,16 +4,17 @@ import type { SyncBaselineStore } from "./types";
|
|
|
4
4
|
const BASELINE_PLUGIN_ID = "core.sync";
|
|
5
5
|
const BASELINE_KEY = "last-synced-payloads";
|
|
6
6
|
const MEMOS_KEY = "contributor-memos";
|
|
7
|
+
const ACCOUNT_KEY = "account";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Persists what this device last pushed so the next pull can tell "the cloud is
|
|
10
11
|
* newer" from "local changed while the app was closed". Without it every launch
|
|
11
12
|
* treats local state as pristine and the cloud overwrites CLI/offline edits.
|
|
12
13
|
*
|
|
13
|
-
* Storage failures
|
|
14
|
-
*
|
|
14
|
+
* Storage failures do not stop sync, but missing merge history can prevent
|
|
15
|
+
* contributors from recognizing unsynced local edits.
|
|
15
16
|
*/
|
|
16
|
-
export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): SyncBaselineStore {
|
|
17
|
+
export function createSyncBaselineStore(pluginState: Pick<AppPluginStateStorePort, "get" | "set">): SyncBaselineStore {
|
|
17
18
|
const load = (key: string) => {
|
|
18
19
|
try {
|
|
19
20
|
return pluginState.get<Record<string, unknown>>(BASELINE_PLUGIN_ID, key)?.value ?? null;
|
|
@@ -25,10 +26,15 @@ export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): S
|
|
|
25
26
|
try {
|
|
26
27
|
pluginState.set(BASELINE_PLUGIN_ID, key, value);
|
|
27
28
|
} catch {
|
|
28
|
-
//
|
|
29
|
+
// Persistence is best effort; keep the in-memory merge history usable.
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
return {
|
|
33
|
+
loadAccountId: () => {
|
|
34
|
+
const id = load(ACCOUNT_KEY)?.id;
|
|
35
|
+
return typeof id === "string" ? id : null;
|
|
36
|
+
},
|
|
37
|
+
saveAccountId: (id) => save(ACCOUNT_KEY, { id }),
|
|
32
38
|
load: () => load(BASELINE_KEY),
|
|
33
39
|
save: (payloads) => save(BASELINE_KEY, payloads),
|
|
34
40
|
loadMemos: () => load(MEMOS_KEY),
|
package/src/sync/controller.ts
CHANGED
|
@@ -33,11 +33,16 @@ interface SyncRuntime {
|
|
|
33
33
|
getContributors: () => RegisteredSyncContributor[];
|
|
34
34
|
getTransport: () => RegisteredSyncTransport | null;
|
|
35
35
|
baselineStore?: SyncBaselineStore;
|
|
36
|
+
/** Retires pending operations when the Cloud session changes; transports decide availability. */
|
|
37
|
+
getSessionIdentity?: () => object | null;
|
|
38
|
+
/** Confirmed Cloud account, used to keep persisted merge history scoped across restarts. */
|
|
39
|
+
getAccountId?: () => string | null;
|
|
36
40
|
/** Sources whose quotes and bars never leave this device (`BrokerAdapter.localMarketData`); none when absent. */
|
|
37
41
|
isLocalMarketData?: LocalMarketDataLookup;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
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";
|
|
41
46
|
const PUSH_DEBOUNCE_MS = 2500;
|
|
42
47
|
/** How soon after a change contributors date it, so an edit made just before quitting still is. */
|
|
43
48
|
const OBSERVE_DEBOUNCE_MS = 250;
|
|
@@ -56,10 +61,10 @@ function nowIso(): string {
|
|
|
56
61
|
|
|
57
62
|
function resolveClientId(): string {
|
|
58
63
|
const storage = globalThis.localStorage;
|
|
59
|
-
const existing = storage?.getItem(
|
|
64
|
+
const existing = storage?.getItem(SYNC_CLIENT_ID_STORAGE_KEY);
|
|
60
65
|
if (existing) return existing;
|
|
61
66
|
const id = `client_${crypto.randomUUID()}`;
|
|
62
|
-
storage?.setItem(
|
|
67
|
+
storage?.setItem(SYNC_CLIENT_ID_STORAGE_KEY, id);
|
|
63
68
|
return id;
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -88,7 +93,9 @@ export class CloudSyncController {
|
|
|
88
93
|
private memosLoaded = false;
|
|
89
94
|
private inFlight: Promise<void> | null = null;
|
|
90
95
|
private syncQueued = false;
|
|
96
|
+
private generation = 0;
|
|
91
97
|
private clientId: string | null = null;
|
|
98
|
+
private accountId: string | null = null;
|
|
92
99
|
private lastSignature: string | null = null;
|
|
93
100
|
/**
|
|
94
101
|
* The content signature of the cloud's copy, with the revision and the
|
|
@@ -146,6 +153,7 @@ export class CloudSyncController {
|
|
|
146
153
|
if (this.runtime !== runtime) {
|
|
147
154
|
this.resetOperations();
|
|
148
155
|
this.runtime = runtime;
|
|
156
|
+
this.accountId = runtime.baselineStore?.loadAccountId?.() ?? null;
|
|
149
157
|
this.updateAvailability();
|
|
150
158
|
}
|
|
151
159
|
return () => this.clearRuntime(runtime);
|
|
@@ -158,6 +166,31 @@ export class CloudSyncController {
|
|
|
158
166
|
this.setStatus({ phase: "disabled", transportId: null, error: null });
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
/** Retires pending session work without losing unsynced edits for the same account. */
|
|
170
|
+
retireSession(): void {
|
|
171
|
+
this.generation += 1;
|
|
172
|
+
if (this.pushTimer) clearTimeout(this.pushTimer);
|
|
173
|
+
this.pushTimer = null;
|
|
174
|
+
this.inFlight = null;
|
|
175
|
+
this.syncQueued = false;
|
|
176
|
+
this.updateAvailability();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Account revisions, merge baselines and edit provenance never carry into another account. */
|
|
180
|
+
resetAccount(accountId?: string): void {
|
|
181
|
+
this.resetOperations();
|
|
182
|
+
this.accountId = accountId ?? null;
|
|
183
|
+
this.baseline = {};
|
|
184
|
+
this.baselineLoaded = true;
|
|
185
|
+
this.memos = {};
|
|
186
|
+
this.memosLoaded = true;
|
|
187
|
+
this.runtime?.baselineStore?.save({});
|
|
188
|
+
this.runtime?.baselineStore?.saveMemos?.({});
|
|
189
|
+
if (accountId) this.runtime?.baselineStore?.saveAccountId?.(accountId);
|
|
190
|
+
this.setStatus({ phase: "idle", transportId: null, lastSyncAt: null, lastPullAt: null, revision: null, error: null });
|
|
191
|
+
this.updateAvailability();
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
subscribe(listener: () => void): () => void {
|
|
162
195
|
this.listeners.add(listener);
|
|
163
196
|
return () => this.listeners.delete(listener);
|
|
@@ -206,6 +239,17 @@ export class CloudSyncController {
|
|
|
206
239
|
private async syncOnce(options: { reason?: string; force?: boolean }): Promise<void> {
|
|
207
240
|
const runtime = this.runtime;
|
|
208
241
|
if (!runtime) return;
|
|
242
|
+
const accountId = runtime.getAccountId?.();
|
|
243
|
+
if (accountId && accountId !== this.accountId) {
|
|
244
|
+
if (this.accountId !== null) this.resetAccount(accountId);
|
|
245
|
+
else {
|
|
246
|
+
// Legacy stores have no owner; retain their offline edits on first validation.
|
|
247
|
+
this.accountId = accountId;
|
|
248
|
+
runtime.baselineStore?.saveAccountId?.(accountId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const generation = this.generation;
|
|
252
|
+
const sessionIdentity = runtime.getSessionIdentity?.();
|
|
209
253
|
this.observe(runtime);
|
|
210
254
|
const registration = runtime.getTransport();
|
|
211
255
|
if (!registration?.transport.isAvailable()) {
|
|
@@ -222,11 +266,11 @@ export class CloudSyncController {
|
|
|
222
266
|
// Always pull before push. Another device (TUI, browser, phone) may have
|
|
223
267
|
// written since this process last synced, and contributors already merge
|
|
224
268
|
// so local-only edits survive the apply.
|
|
225
|
-
const pulled = await this.runPull(runtime, transport);
|
|
226
|
-
if (!pulled || !this.isCurrent(runtime, transport)) return;
|
|
269
|
+
const pulled = await this.runPull(runtime, transport, generation, sessionIdentity);
|
|
270
|
+
if (!pulled || !this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
|
|
227
271
|
|
|
228
|
-
const snapshot = await this.assembleSnapshot(runtime);
|
|
229
|
-
if (!this.isCurrent(runtime, transport)) return;
|
|
272
|
+
const snapshot = await this.assembleSnapshot(runtime, generation, sessionIdentity);
|
|
273
|
+
if (!snapshot || !this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
|
|
230
274
|
const signature = snapshotContentSignature(snapshot);
|
|
231
275
|
if (!options.force && signature === this.lastSignature) return;
|
|
232
276
|
if (!options.force && signature === pulled.signature) {
|
|
@@ -242,7 +286,7 @@ export class CloudSyncController {
|
|
|
242
286
|
// The revision this snapshot was merged with. The cloud refuses the
|
|
243
287
|
// write if another device wrote since, instead of losing that write.
|
|
244
288
|
const result = await transport.pushSnapshot(snapshot, { baseRevision: pulled.revision });
|
|
245
|
-
if (!this.isCurrent(runtime, transport)) return;
|
|
289
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
|
|
246
290
|
this.lastSignature = signature;
|
|
247
291
|
this.appliedRevision = result.revision;
|
|
248
292
|
this.saveBaseline(runtime, snapshot);
|
|
@@ -255,10 +299,10 @@ export class CloudSyncController {
|
|
|
255
299
|
});
|
|
256
300
|
return;
|
|
257
301
|
} catch (error) {
|
|
258
|
-
if (!this.isCurrent(runtime, transport)) return;
|
|
302
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
|
|
259
303
|
if (error instanceof SyncRevisionConflictError && attempt < MAX_CONFLICT_RETRIES) {
|
|
260
304
|
// Pull whatever the other device wrote, even an older revision than
|
|
261
|
-
// this process last saw
|
|
305
|
+
// this process last saw, such as a restored snapshot.
|
|
262
306
|
this.appliedRevision = null;
|
|
263
307
|
continue;
|
|
264
308
|
}
|
|
@@ -272,18 +316,18 @@ export class CloudSyncController {
|
|
|
272
316
|
}
|
|
273
317
|
}
|
|
274
318
|
|
|
275
|
-
private async runPull(runtime: SyncRuntime, transport: SyncTransport): Promise<PullResult | null> {
|
|
319
|
+
private async runPull(runtime: SyncRuntime, transport: SyncTransport, generation: number, sessionIdentity: object | null | undefined): Promise<PullResult | null> {
|
|
276
320
|
const baselineState = runtime.getState();
|
|
277
321
|
this.setStatus({ phase: "syncing", transportId: transport.id, error: null });
|
|
278
322
|
try {
|
|
279
323
|
const response = await transport.pullSnapshot();
|
|
280
|
-
if (!this.isCurrent(runtime, transport)) return null;
|
|
324
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
|
|
281
325
|
|
|
282
326
|
if (response.snapshot && this.shouldApplyPulledRevision(response.revision)) {
|
|
283
327
|
this.assertSnapshotCompatible(response.snapshot, runtime.getContributors());
|
|
284
328
|
this.pulledContributors = response.snapshot.contributors;
|
|
285
329
|
for (const entry of runtime.getContributors()) {
|
|
286
|
-
if (!this.isCurrent(runtime, transport)) return null;
|
|
330
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
|
|
287
331
|
const contributorPayload = response.snapshot.contributors[entry.contributor.id];
|
|
288
332
|
if (!contributorPayload || !entry.contributor.apply) continue;
|
|
289
333
|
await entry.contributor.apply(contributorPayload.payload, {
|
|
@@ -292,16 +336,17 @@ export class CloudSyncController {
|
|
|
292
336
|
baselinePayload: this.loadBaseline(runtime)?.[entry.contributor.id] ?? null,
|
|
293
337
|
state: runtime.getState(),
|
|
294
338
|
getState: runtime.getState,
|
|
295
|
-
isCurrent: () => this.isCurrent(runtime, transport),
|
|
339
|
+
isCurrent: () => this.isCurrent(runtime, transport, generation, sessionIdentity),
|
|
296
340
|
dispatch: runtime.dispatch,
|
|
297
341
|
tickerRepository: runtime.tickerRepository,
|
|
298
342
|
memo: this.memoFor(runtime, entry.contributor.id),
|
|
299
343
|
});
|
|
344
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
|
|
300
345
|
}
|
|
301
346
|
if (response.revision != null) this.appliedRevision = response.revision;
|
|
302
347
|
}
|
|
303
348
|
|
|
304
|
-
if (!this.isCurrent(runtime, transport)) return null;
|
|
349
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
|
|
305
350
|
this.setStatus({
|
|
306
351
|
phase: response.snapshot ? "synced" : "idle",
|
|
307
352
|
transportId: transport.id,
|
|
@@ -315,7 +360,7 @@ export class CloudSyncController {
|
|
|
315
360
|
signature: response.snapshot ? this.pulledSignature(transport, response.revision, response.snapshot) : null,
|
|
316
361
|
};
|
|
317
362
|
} catch (error) {
|
|
318
|
-
if (!this.isCurrent(runtime, transport)) return null;
|
|
363
|
+
if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
|
|
319
364
|
this.setStatus({
|
|
320
365
|
phase: "error",
|
|
321
366
|
transportId: transport.id,
|
|
@@ -348,7 +393,7 @@ export class CloudSyncController {
|
|
|
348
393
|
}
|
|
349
394
|
}
|
|
350
395
|
|
|
351
|
-
private async assembleSnapshot(runtime: SyncRuntime): Promise<SyncSnapshot> {
|
|
396
|
+
private async assembleSnapshot(runtime: SyncRuntime, generation: number, sessionIdentity: object | null | undefined): Promise<SyncSnapshot | null> {
|
|
352
397
|
const contributors = runtime.getContributors();
|
|
353
398
|
const createdAt = nowIso();
|
|
354
399
|
// The snapshot replaces the stored one wholesale, so data owned by a
|
|
@@ -356,6 +401,7 @@ export class CloudSyncController {
|
|
|
356
401
|
// carried over instead of being deleted for every other device.
|
|
357
402
|
const payloads: SyncSnapshot["contributors"] = { ...this.pulledContributors };
|
|
358
403
|
for (const entry of contributors) {
|
|
404
|
+
if (this.runtime !== runtime || this.generation !== generation || runtime.getSessionIdentity?.() !== sessionIdentity) return null;
|
|
359
405
|
const payload = await entry.contributor.collect({
|
|
360
406
|
state: runtime.getState(),
|
|
361
407
|
baselinePayload: this.loadBaseline(runtime)?.[entry.contributor.id] ?? null,
|
|
@@ -415,9 +461,12 @@ export class CloudSyncController {
|
|
|
415
461
|
}
|
|
416
462
|
|
|
417
463
|
private memoFor(runtime: SyncRuntime, contributorId: string) {
|
|
464
|
+
const generation = this.generation;
|
|
465
|
+
const sessionIdentity = runtime.getSessionIdentity?.();
|
|
418
466
|
return {
|
|
419
467
|
get: () => this.loadMemos(runtime)[contributorId] ?? null,
|
|
420
468
|
set: (value: unknown) => {
|
|
469
|
+
if (this.runtime !== runtime || this.generation !== generation || runtime.getSessionIdentity?.() !== sessionIdentity) return;
|
|
421
470
|
const memos = { ...this.loadMemos(runtime), [contributorId]: value };
|
|
422
471
|
this.memos = memos;
|
|
423
472
|
runtime.baselineStore?.saveMemos?.(memos);
|
|
@@ -442,8 +491,9 @@ export class CloudSyncController {
|
|
|
442
491
|
runtime.baselineStore?.save(payloads);
|
|
443
492
|
}
|
|
444
493
|
|
|
445
|
-
private isCurrent(runtime: SyncRuntime, transport: SyncTransport): boolean {
|
|
446
|
-
return this.runtime === runtime && runtime.getTransport()?.transport === transport
|
|
494
|
+
private isCurrent(runtime: SyncRuntime, transport: SyncTransport, generation: number, sessionIdentity: object | null | undefined): boolean {
|
|
495
|
+
return this.generation === generation && this.runtime === runtime && runtime.getTransport()?.transport === transport
|
|
496
|
+
&& runtime.getSessionIdentity?.() === sessionIdentity;
|
|
447
497
|
}
|
|
448
498
|
|
|
449
499
|
private pulledSignature(transport: SyncTransport, revision: number | null, snapshot: SyncSnapshot): string {
|
|
@@ -470,6 +520,7 @@ export class CloudSyncController {
|
|
|
470
520
|
}
|
|
471
521
|
|
|
472
522
|
private resetOperations(): void {
|
|
523
|
+
this.generation += 1;
|
|
473
524
|
if (this.pushTimer) clearTimeout(this.pushTimer);
|
|
474
525
|
this.pushTimer = null;
|
|
475
526
|
if (this.observeTimer) clearTimeout(this.observeTimer);
|