gloomberb 0.11.2 → 0.12.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 +7 -4
- package/README.zh-CN.md +4 -2
- package/package.json +28 -1
- package/src/api-client/auth.ts +10 -0
- package/src/api-client/data.ts +108 -1
- package/src/api-client/index.test.ts +127 -0
- package/src/api-client/index.ts +134 -1
- package/src/api-client/normalizers.ts +39 -0
- package/src/api-client/paths.ts +85 -1
- package/src/api-client/types.ts +220 -0
- package/src/app/pane-runtime/index.tsx +15 -12
- package/src/app/pane-runtime/layout-placement.test.ts +27 -0
- package/src/app/pane-runtime/layout-placement.ts +13 -0
- package/src/app/pane-runtime/plugin-bindings.ts +62 -0
- package/src/app.tsx +60 -18
- package/src/architecture/browser-entry-graph.test.ts +96 -0
- package/src/brokers/profile-form.test.ts +25 -27
- package/src/brokers/test-broker.ts +68 -0
- package/src/capabilities/chart-series.ts +3 -2
- package/src/cli/commands/market.ts +2 -2
- package/src/cli/commands/plugins.ts +39 -11
- package/src/cli/desktop-pane-shot.ts +285 -16
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/capabilities.ts +61 -31
- package/src/cli/pane-functions/catalog.ts +16 -4
- package/src/cli/pane-functions/discovery.ts +1 -0
- package/src/cli/pane-functions/headless.test.ts +247 -0
- package/src/cli/pane-functions/headless.ts +423 -0
- package/src/cli/pane-functions/index.test.ts +9 -0
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +27 -2
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +26 -8
- package/src/cli/pane-functions/screenshot.test.ts +76 -0
- package/src/cli/pane-functions/screenshot.ts +236 -20
- package/src/cli/registry.ts +0 -22
- package/src/cli/restore-plugins.ts +34 -0
- package/src/cli/result.ts +7 -10
- package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
- package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
- package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -20
- package/src/components/chart/composite/composite-chart.tsx +301 -204
- package/src/components/chart/composite/format.ts +8 -2
- package/src/components/chart/composite/interactions.test.ts +68 -102
- package/src/components/chart/composite/interactions.ts +214 -264
- package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
- package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
- package/src/components/chart/composite/scene.ts +57 -3
- package/src/components/chart/composite/text-renderer.ts +3 -2
- package/src/components/chart/composite/time-scale.ts +138 -57
- package/src/components/chart/composite/types.ts +25 -1
- package/src/components/chart/core/data.ts +9 -13
- package/src/components/chart/core/date-window.ts +7 -1
- package/src/components/chart/core/pointer.ts +3 -0
- package/src/components/chart/core/types.ts +0 -45
- package/src/components/chart/native/chart-rasterizer.ts +0 -1
- package/src/components/chart/native/kitty/index.test.ts +0 -188
- package/src/components/chart/native/raster/crosshair.ts +6 -5
- package/src/components/chart/native/raster/primitives.ts +0 -23
- package/src/components/chart/native/renderer-selection.ts +81 -31
- package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
- package/src/components/chart/static/chart-surface.tsx +150 -0
- package/src/components/chart/static/index.ts +8 -0
- package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
- package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
- package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
- package/src/components/chart/static/series.ts +49 -0
- package/src/components/command-bar/action-runtime.ts +1 -3
- package/src/components/command-bar/assist/model.test.ts +17 -2
- package/src/components/command-bar/assist/model.ts +40 -9
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/commands/direct/runtime.ts +1 -1
- package/src/components/command-bar/commands/registry.ts +0 -11
- package/src/components/command-bar/helpers.test.ts +2 -2
- package/src/components/command-bar/helpers.ts +2 -6
- package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
- package/src/components/command-bar/list/badge.test.ts +54 -0
- package/src/components/command-bar/list/badge.ts +62 -0
- package/src/components/command-bar/list/model.test.ts +103 -0
- package/src/components/command-bar/list/model.ts +61 -4
- package/src/components/command-bar/list/view.test.tsx +113 -0
- package/src/components/command-bar/list/view.tsx +147 -131
- package/src/components/command-bar/pane-templates/workflow.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +51 -65
- package/src/components/command-bar/panel/index.tsx +48 -13
- package/src/components/command-bar/panel/layout.test.ts +139 -0
- package/src/components/command-bar/panel/layout.ts +65 -28
- package/src/components/command-bar/panel/native-surface.ts +24 -0
- package/src/components/command-bar/panel/prompt-binding.ts +54 -0
- package/src/components/command-bar/panel/runtime.ts +6 -3
- package/src/components/command-bar/panel/state.ts +41 -7
- package/src/components/command-bar/panel/types.ts +7 -1
- package/src/components/command-bar/routes/root/provider-search.ts +34 -15
- package/src/components/command-bar/routes/root/results.test.ts +161 -0
- package/src/components/command-bar/routes/root/results.ts +30 -28
- package/src/components/command-bar/routes/root/runtime.ts +37 -20
- package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
- package/src/components/command-bar/routes/root/search-providers.ts +205 -0
- package/src/components/command-bar/routes/root/selection.ts +1 -12
- package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
- package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
- package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
- package/src/components/command-bar/routing/actions.ts +1 -81
- package/src/components/command-bar/routing/launch-request.ts +1 -1
- package/src/components/command-bar/routing/list-state.ts +6 -21
- package/src/components/command-bar/selection-runtime.ts +1 -1
- package/src/components/command-bar/surface/assist.test.tsx +14 -10
- package/src/components/command-bar/surface/environment.ts +8 -1
- package/src/components/command-bar/surface/index.test.tsx +84 -68
- package/src/components/command-bar/surface/index.tsx +33 -36
- package/src/components/command-bar/surface/test-harness.tsx +15 -3
- package/src/components/command-bar/theme-picker.tsx +43 -15
- package/src/components/command-bar/view-model.test.ts +53 -8
- package/src/components/command-bar/view-model.ts +43 -14
- package/src/components/command-bar/workflow/tickers.ts +1 -0
- package/src/components/command-bar/workflow/types.ts +1 -1
- package/src/components/data-table/export.test.ts +21 -0
- package/src/components/data-table/export.ts +21 -0
- package/src/components/data-table/view.test.tsx +45 -6
- package/src/components/data-table/view.tsx +55 -9
- package/src/components/index.ts +10 -2
- package/src/components/input-search-bar.test.tsx +36 -2
- package/src/components/input-search-bar.tsx +18 -1
- package/src/components/layout/header.test.tsx +57 -10
- package/src/components/layout/header.tsx +292 -149
- package/src/components/layout/market-summary.test.ts +81 -0
- package/src/components/layout/market-summary.ts +109 -0
- package/src/components/layout/shell/chrome.ts +96 -1
- package/src/components/layout/shell/index.tsx +11 -3
- package/src/components/layout/shell/menu.ts +9 -0
- package/src/components/layout/shell/pane/actions.ts +29 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/shortcuts.ts +3 -0
- package/src/components/layout/status-bar.test.tsx +9 -8
- package/src/components/layout/status-bar.tsx +85 -54
- package/src/components/onboarding/onboarding-frame.tsx +2 -26
- package/src/components/price-sparkline/index.ts +61 -34
- package/src/components/price-sparkline/view.tsx +1 -1
- package/src/components/sign-in-gate.test.tsx +128 -0
- package/src/components/sign-in-gate.tsx +149 -0
- package/src/components/speedometer-gauge/desktop.tsx +2 -12
- package/src/components/ticker/badge/list.tsx +17 -3
- package/src/components/ticker/list-table-view.tsx +6 -3
- package/src/components/ui/data-table/index.tsx +22 -4
- package/src/components/ui/data-table/opentui/index.tsx +165 -120
- package/src/components/ui/frame.tsx +28 -0
- package/src/components/ui/index.ts +3 -0
- package/src/components/ui/native-select.tsx +20 -10
- package/src/components/ui/select-button.tsx +138 -0
- package/src/components/ui/status.tsx +2 -2
- package/src/components/ui/table-layout.ts +1 -1
- package/src/components/use-double-click-activation.ts +7 -3
- package/src/core/app-services.ts +3 -3
- package/src/data/config/chart-settings.ts +4 -4
- package/src/data/config/layout.ts +12 -6
- package/src/data/config/store/index.test.ts +97 -0
- package/src/data/config/store/migrations.ts +41 -0
- package/src/data/config/store/normalize.ts +5 -3
- package/src/i18n/es.ts +6 -2
- package/src/i18n/ja.ts +6 -2
- package/src/i18n/ko.ts +6 -2
- package/src/i18n/zh-cn.ts +6 -2
- package/src/i18n/zh-tw.ts +6 -2
- package/src/layout-marketplace/gallery-terminal.tsx +126 -96
- package/src/layout-marketplace/gallery.test.tsx +65 -1
- package/src/layout-marketplace/pane-imagery.ts +1 -0
- package/src/layout-marketplace/payload.test.ts +17 -0
- package/src/layout-marketplace/payload.ts +40 -5
- package/src/market-data/coordinator/index.ts +3 -2
- package/src/market-data/coordinator/quotes.ts +173 -66
- package/src/market-data/coordinator/subscriptions.test.ts +215 -37
- package/src/pane-settings.ts +1 -1
- package/src/plugins/builtin/analytics/pane-model.ts +1 -1
- package/src/plugins/builtin/analytics/view.tsx +1 -1
- package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
- package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
- package/src/plugins/builtin/chart-composer/settings.ts +2 -13
- package/src/plugins/builtin/chat/content/composer.tsx +8 -4
- package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
- package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
- package/src/plugins/builtin/cloud/plugin.tsx +1 -0
- package/src/plugins/builtin/composite-plugins.ts +16 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
- package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
- package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings/index.tsx +1 -0
- package/src/plugins/builtin/earnings-calls/data.ts +223 -0
- package/src/plugins/builtin/earnings-calls/format.ts +49 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
- package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
- package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
- package/src/plugins/builtin/econ/detail-view.tsx +1 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
- package/src/plugins/builtin/econ/index.tsx +3 -1
- package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
- package/src/plugins/builtin/econ-statistics/client.ts +92 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
- package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
- package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
- package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
- package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
- package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
- package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
- package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
- package/src/plugins/builtin/econ-statistics/view.ts +90 -0
- package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
- package/src/plugins/builtin/fear-greed/data.ts +6 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
- package/src/plugins/builtin/help/index.tsx +4 -0
- package/src/plugins/builtin/holders/index.tsx +1 -0
- package/src/plugins/builtin/insider/index.tsx +1 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
- package/src/plugins/builtin/market-halts/index.tsx +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +1 -0
- package/src/plugins/builtin/market-valuation/align.ts +122 -0
- package/src/plugins/builtin/market-valuation/cache.ts +32 -0
- package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
- package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
- package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
- package/src/plugins/builtin/market-valuation/client.ts +116 -0
- package/src/plugins/builtin/market-valuation/defs.ts +251 -0
- package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
- package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
- package/src/plugins/builtin/market-valuation/headless.ts +170 -0
- package/src/plugins/builtin/market-valuation/index.tsx +64 -0
- package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
- package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
- package/src/plugins/builtin/market-valuation/model.ts +8 -0
- package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
- package/src/plugins/builtin/market-valuation/series.ts +12 -0
- package/src/plugins/builtin/market-valuation/settings.ts +38 -0
- package/src/plugins/builtin/market-valuation/sources.ts +130 -0
- package/src/plugins/builtin/market-valuation/trend.ts +89 -0
- package/src/plugins/builtin/market-valuation/view.ts +137 -0
- package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
- package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
- package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
- package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
- package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
- package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
- package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
- package/src/plugins/builtin/news/wire/index.tsx +31 -44
- package/src/plugins/builtin/options/analytics.test.ts +83 -0
- package/src/plugins/builtin/options/analytics.ts +110 -0
- package/src/plugins/builtin/options/index.tsx +18 -2
- package/src/plugins/builtin/options/table.ts +121 -51
- package/src/plugins/builtin/options/types.ts +22 -15
- package/src/plugins/builtin/options/view.test.tsx +53 -1
- package/src/plugins/builtin/options/view.tsx +92 -41
- package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
- package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
- package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
- package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
- package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
- package/src/plugins/builtin/plugin-module.test.ts +2 -4
- package/src/plugins/builtin/plugin-module.ts +9 -8
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/research/index.tsx +4 -0
- package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
- package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
- package/src/plugins/builtin/research-search/data.ts +109 -0
- package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
- package/src/plugins/builtin/research-search/document-view.tsx +254 -0
- package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
- package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
- package/src/plugins/builtin/research-search/index.tsx +85 -0
- package/src/plugins/builtin/research-search/model.test.ts +40 -0
- package/src/plugins/builtin/research-search/model.ts +291 -0
- package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
- package/src/plugins/builtin/research-search/pane.tsx +678 -0
- package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
- package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
- package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
- package/src/plugins/builtin/research-search/snippet.ts +134 -0
- package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
- package/src/plugins/builtin/scanner/flow-model.ts +29 -1
- package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
- package/src/plugins/builtin/scanner/index.ts +2 -0
- package/src/plugins/builtin/sec/index.tsx +1 -0
- package/src/plugins/builtin/sectors/index.tsx +1 -0
- package/src/plugins/builtin/shared/series-cache.ts +93 -0
- package/src/plugins/builtin/short-interest/index.tsx +1 -0
- package/src/plugins/builtin/short-interest/pane.tsx +1 -1
- package/src/plugins/builtin/thirteenf/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
- package/src/plugins/builtin/volatility/index.tsx +1 -1
- package/src/plugins/builtin/world-indices/index.tsx +1 -0
- package/src/plugins/builtin/world-venue-map/index.ts +27 -0
- package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
- package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
- package/src/plugins/builtin/world-venue-map/model.ts +268 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
- package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/bundle.test.ts +109 -0
- package/src/plugins/bundle.ts +139 -0
- package/src/plugins/catalog-backend.ts +6 -44
- package/src/plugins/catalog-browser.test.ts +1 -0
- package/src/plugins/catalog-browser.ts +12 -0
- package/src/plugins/catalog-manifest.test.ts +43 -0
- package/src/plugins/catalog-manifest.ts +92 -0
- package/src/plugins/catalog-ui.ts +20 -4
- package/src/plugins/catalog.ts +9 -2
- package/src/plugins/current-target.ts +28 -0
- package/src/plugins/host-contract.ts +37 -0
- package/src/plugins/host-link.ts +158 -0
- package/src/plugins/host-modules.ts +61 -0
- package/src/plugins/loader.ts +41 -22
- package/src/plugins/ownership.ts +1 -0
- package/src/plugins/prediction-markets/index.tsx +1 -0
- package/src/plugins/registry/context.ts +3 -0
- package/src/plugins/registry/contributions.ts +27 -0
- package/src/plugins/registry/index.test.ts +25 -0
- package/src/plugins/registry/index.ts +8 -0
- package/src/plugins/registry/pane-settings.test.ts +53 -0
- package/src/plugins/registry/pane-settings.ts +29 -3
- package/src/plugins/seed.test.ts +83 -0
- package/src/plugins/seed.ts +80 -0
- package/src/public/broker.ts +31 -0
- package/src/public/market-data.ts +9 -0
- package/src/public/pane-hooks.ts +57 -0
- package/src/public/public-api.test.ts +285 -0
- package/src/public/react.ts +62 -0
- package/src/public/test-support.ts +23 -0
- package/src/public/time-series.ts +8 -0
- package/src/public/utils.ts +77 -0
- package/src/remote/layout-helpers.ts +6 -8
- package/src/remote/resources.ts +2 -0
- package/src/remote/schema.ts +1 -0
- package/src/remote/semantic-tree.test.tsx +105 -0
- package/src/remote/semantic-tree.tsx +53 -35
- package/src/renderers/browser/main.tsx +6 -0
- package/src/renderers/browser/ui-host.tsx +11 -0
- package/src/renderers/cloudflare/worker.test.ts +19 -0
- package/src/renderers/cloudflare/worker.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
- package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
- package/src/renderers/electrobun/bun/index.ts +6 -2
- package/src/renderers/electrobun/shared/protocol.ts +25 -0
- package/src/renderers/electrobun/view/build-assets.ts +0 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
- package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
- package/src/renderers/electrobun/view/external-plugins.ts +63 -0
- package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
- package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
- package/src/renderers/electrobun/view/host/mouse.ts +16 -2
- package/src/renderers/electrobun/view/main.tsx +27 -2
- package/src/renderers/electrobun/view/styles.css +5 -0
- package/src/renderers/electrobun/view/toast-host.tsx +18 -0
- package/src/renderers/electrobun/view/ui-host.tsx +3 -0
- package/src/renderers/opentui/chart-surface.tsx +5 -1
- package/src/renderers/opentui/host.tsx +20 -2
- package/src/renderers/opentui/interaction-performance.test.ts +79 -0
- package/src/renderers/opentui/interaction-performance.ts +188 -0
- package/src/renderers/opentui/start.tsx +26 -1
- package/src/renderers/opentui/test-utils.tsx +13 -0
- package/src/renderers/opentui/toast-host.tsx +26 -2
- package/src/renderers/opentui/ui-host.tsx +1 -0
- package/src/sources/provider-router/search.test.ts +191 -0
- package/src/sources/provider-router/search.ts +92 -39
- package/src/state/app/context/index.tsx +2 -0
- package/src/state/app/context/selector.test.tsx +33 -1
- package/src/state/hooks/quote-streaming.test.tsx +93 -0
- package/src/state/hooks/quote-streaming.ts +106 -30
- package/src/state/pane-table-export-registry.ts +50 -0
- package/src/theme/color-utils.ts +1 -1
- package/src/theme/colors.ts +50 -0
- package/src/theme/themes.test.ts +15 -2
- package/src/theme/themes.ts +19 -1
- package/src/tickers/search/index.test.ts +26 -0
- package/src/tickers/search/index.ts +42 -17
- package/src/tickers/search/ranking.ts +14 -0
- package/src/time-series/chart-data.test.ts +216 -0
- package/src/time-series/range.ts +12 -0
- package/src/time-series/resolution.test.ts +25 -26
- package/src/time-series/resolution.ts +44 -25
- package/src/time-series/resolve.ts +7 -0
- package/src/time-series/spec.ts +5 -5
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.ts +5 -0
- package/src/types/config.ts +123 -3
- package/src/types/data-provider.ts +11 -0
- package/src/types/headless.ts +161 -0
- package/src/types/plugin.ts +104 -20
- package/src/ui/host.tsx +19 -1
- package/src/ui/index.tsx +1 -0
- package/src/ui/toast.tsx +8 -4
- package/src/utils/csv.test.ts +13 -0
- package/src/utils/csv.ts +37 -0
- package/src/utils/format.ts +35 -0
- package/src/utils/fuzzy-search.test.ts +94 -0
- package/src/utils/fuzzy-search.ts +81 -22
- package/src/utils/save-text-file.test.ts +24 -0
- package/src/utils/save-text-file.ts +36 -0
- package/src/utils/shortcut-labels.ts +14 -0
- package/src/components/chart/core/axis-format.ts +0 -157
- package/src/components/chart/core/index.test.ts +0 -1075
- package/src/components/chart/core/projection.ts +0 -357
- package/src/components/chart/core/render-utils.ts +0 -4
- package/src/components/chart/core/renderer.ts +0 -244
- package/src/components/chart/core/scene.ts +0 -180
- package/src/components/chart/core/time-axis.ts +0 -330
- package/src/components/chart/cursor-motion.ts +0 -47
- package/src/components/chart/indicators/types.ts +0 -26
- package/src/components/chart/market-session.ts +0 -176
- package/src/components/chart/multi-line/renderer.test.ts +0 -83
- package/src/components/chart/multi-line/renderer.ts +0 -161
- package/src/components/chart/multi-line/scene.ts +0 -188
- package/src/components/chart/native/raster/layout.ts +0 -129
- package/src/components/chart/native/raster/price-chart.ts +0 -244
- package/src/components/chart/rendering/chart-draw.ts +0 -270
- package/src/components/chart/rendering/geometry.ts +0 -95
- package/src/components/chart/rendering/pixel-buffer.ts +0 -267
- package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
- package/src/components/chart/scatter-chart-renderer.ts +0 -223
- package/src/components/chart/static/chart/surface.tsx +0 -358
- package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
- package/src/components/command-bar/routes/root/article-results.ts +0 -55
- package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
- package/src/plugins/builtin/news/wire/article-search.ts +0 -140
- package/src/plugins/builtin/substack/api/auth.ts +0 -185
- package/src/plugins/builtin/substack/api/cache.ts +0 -82
- package/src/plugins/builtin/substack/api/loaders.ts +0 -170
- package/src/plugins/builtin/substack/api/store.ts +0 -186
- package/src/plugins/builtin/substack/api/types.ts +0 -42
- package/src/plugins/builtin/substack/api.test.ts +0 -298
- package/src/plugins/builtin/substack/article-detail.tsx +0 -397
- package/src/plugins/builtin/substack/article-stack.tsx +0 -133
- package/src/plugins/builtin/substack/content.ts +0 -478
- package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
- package/src/plugins/builtin/substack/index.tsx +0 -49
- package/src/plugins/builtin/substack/login-view.tsx +0 -126
- package/src/plugins/builtin/substack/model.test.ts +0 -155
- package/src/plugins/builtin/substack/normalize.ts +0 -343
- package/src/plugins/builtin/substack/pane-footer.ts +0 -52
- package/src/plugins/builtin/substack/pane-state.ts +0 -170
- package/src/plugins/builtin/substack/pane.tsx +0 -547
- package/src/plugins/builtin/substack/read-state.ts +0 -30
- package/src/plugins/builtin/substack/table.ts +0 -111
- package/src/plugins/builtin/substack/types.ts +0 -71
- package/src/plugins/builtin/substack/utils.ts +0 -131
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
- package/src/plugins/ibkr/account-cache.test.ts +0 -102
- package/src/plugins/ibkr/account-cache.ts +0 -24
- package/src/plugins/ibkr/broker-adapter.ts +0 -286
- package/src/plugins/ibkr/config.test.ts +0 -79
- package/src/plugins/ibkr/config.ts +0 -250
- package/src/plugins/ibkr/dialogs.tsx +0 -40
- package/src/plugins/ibkr/flex/index.test.ts +0 -264
- package/src/plugins/ibkr/flex/index.ts +0 -367
- package/src/plugins/ibkr/flex/statement-client.ts +0 -235
- package/src/plugins/ibkr/fundamental-parser.ts +0 -149
- package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
- package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
- package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
- package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
- package/src/plugins/ibkr/gateway/connection.ts +0 -152
- package/src/plugins/ibkr/gateway/contracts.ts +0 -132
- package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
- package/src/plugins/ibkr/gateway/helpers.ts +0 -33
- package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
- package/src/plugins/ibkr/gateway/history.ts +0 -120
- package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
- package/src/plugins/ibkr/gateway/market-data.ts +0 -345
- package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
- package/src/plugins/ibkr/gateway/orders.ts +0 -63
- package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
- package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
- package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
- package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
- package/src/plugins/ibkr/gateway/selection.ts +0 -51
- package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
- package/src/plugins/ibkr/gateway/service/index.ts +0 -429
- package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
- package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
- package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
- package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
- package/src/plugins/ibkr/gateway/service/native.ts +0 -543
- package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
- package/src/plugins/ibkr/gateway/types.ts +0 -27
- package/src/plugins/ibkr/index.test.tsx +0 -162
- package/src/plugins/ibkr/index.tsx +0 -159
- package/src/plugins/ibkr/instance-selection.test.ts +0 -120
- package/src/plugins/ibkr/instance-selection.ts +0 -36
- package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
- package/src/plugins/ibkr/portfolio-performance.ts +0 -129
- package/src/plugins/ibkr/trade/badge.tsx +0 -28
- package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
- package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
- package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
- package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
- package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
- package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
- package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
- package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
- package/src/plugins/ibkr/trade/tab/model.ts +0 -159
- package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
- package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
- package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
- package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
- package/src/plugins/ibkr/trade/utils.test.ts +0 -175
- package/src/plugins/ibkr/trade/utils.ts +0 -144
- package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
- package/src/plugins/ibkr/trading/pane.tsx +0 -332
- package/src/plugins/ibkr/trading/state.test.ts +0 -109
- package/src/plugins/ibkr/trading/state.ts +0 -335
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DataProvider, SearchRequestContext } from "../../types/data-provider";
|
|
3
|
+
import type { InstrumentSearchResult } from "../../types/instrument";
|
|
4
|
+
import { ProviderRouterSearchRoutes } from "./search";
|
|
5
|
+
|
|
6
|
+
function result(symbol: string, extra: Partial<InstrumentSearchResult> = {}): InstrumentSearchResult {
|
|
7
|
+
return { symbol, name: symbol, exchange: "NASDAQ", type: "EQUITY", ...extra };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function provider(
|
|
11
|
+
id: string,
|
|
12
|
+
items: InstrumentSearchResult[],
|
|
13
|
+
delayMs = 0,
|
|
14
|
+
): DataProvider {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
search: async () => {
|
|
18
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
19
|
+
return items;
|
|
20
|
+
},
|
|
21
|
+
} as unknown as DataProvider;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function broker(
|
|
25
|
+
id: string,
|
|
26
|
+
items: InstrumentSearchResult[] | null,
|
|
27
|
+
delayMs = 0,
|
|
28
|
+
) {
|
|
29
|
+
return {
|
|
30
|
+
brokerId: id,
|
|
31
|
+
brokerInstanceId: `${id}-1`,
|
|
32
|
+
brokerLabel: id,
|
|
33
|
+
instance: {},
|
|
34
|
+
broker: {
|
|
35
|
+
searchInstruments: async () => {
|
|
36
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
37
|
+
if (!items) throw new Error("broker down");
|
|
38
|
+
return items;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeRoutes(options: {
|
|
45
|
+
brokers?: ReturnType<typeof broker>[];
|
|
46
|
+
providers?: DataProvider[];
|
|
47
|
+
}) {
|
|
48
|
+
return new ProviderRouterSearchRoutes({
|
|
49
|
+
getBrokerCandidates: () => (options.brokers ?? []) as never,
|
|
50
|
+
providersInPriorityOrder: () => options.providers ?? [],
|
|
51
|
+
logProviderError: () => {},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("provider search racing", () => {
|
|
56
|
+
/**
|
|
57
|
+
* The reason this exists: brokers used to be awaited one at a time before the
|
|
58
|
+
* cloud was asked at all, so a single unreachable broker delayed every ticker
|
|
59
|
+
* lookup in the app by its full timeout.
|
|
60
|
+
*/
|
|
61
|
+
test("a hanging broker does not delay a provider that can answer", async () => {
|
|
62
|
+
const routes = makeRoutes({
|
|
63
|
+
brokers: [broker("dead", [result("NVDA")], 5_000)],
|
|
64
|
+
providers: [provider("cloud", [result("NVDA")])],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
const results = await routes.search("nvidia", { preferBroker: true, interactive: true });
|
|
69
|
+
|
|
70
|
+
expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
|
|
71
|
+
// Sequentially this waited on the broker's timeout first.
|
|
72
|
+
expect(Date.now() - started).toBeLessThan(400);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("interactive lookups abandon a stalled source in well under a second", async () => {
|
|
76
|
+
const routes = makeRoutes({ providers: [provider("slow", [result("AAPL")], 5_000)] });
|
|
77
|
+
|
|
78
|
+
const started = Date.now();
|
|
79
|
+
const results = await routes.search("apple", { interactive: true });
|
|
80
|
+
|
|
81
|
+
expect(results).toEqual([]);
|
|
82
|
+
expect(Date.now() - started).toBeLessThan(1_500);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a broker that answers after the cloud upgrades the row in place", async () => {
|
|
86
|
+
const partials: InstrumentSearchResult[][] = [];
|
|
87
|
+
const routes = makeRoutes({
|
|
88
|
+
brokers: [broker("ibkr", [result("NVDA", { brokerContract: { brokerId: "ibkr" } as never })], 60)],
|
|
89
|
+
providers: [provider("cloud", [result("NVDA")])],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const first = await routes.search("nvidia", {
|
|
93
|
+
preferBroker: true,
|
|
94
|
+
interactive: true,
|
|
95
|
+
onPartial: (items) => partials.push(items),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// The cloud answers immediately and that is what the caller renders.
|
|
99
|
+
expect(first.map((item) => item.symbol)).toEqual(["NVDA"]);
|
|
100
|
+
expect(first[0]?.brokerContract).toBeUndefined();
|
|
101
|
+
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
103
|
+
|
|
104
|
+
// The broker's richer copy replaces it without duplicating the symbol.
|
|
105
|
+
expect(partials).toHaveLength(1);
|
|
106
|
+
expect(partials[0]).toHaveLength(1);
|
|
107
|
+
expect(partials[0]?.[0]?.brokerContract).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Racing everything would bill a metered fallback on every keystroke for an
|
|
112
|
+
* answer already in hand, so the second tier only runs when the first found
|
|
113
|
+
* nothing.
|
|
114
|
+
*/
|
|
115
|
+
test("a fallback provider is not asked when the preferred one answers", async () => {
|
|
116
|
+
let fallbackCalls = 0;
|
|
117
|
+
const fallback = {
|
|
118
|
+
id: "fallback",
|
|
119
|
+
search: async () => {
|
|
120
|
+
fallbackCalls += 1;
|
|
121
|
+
return [result("WRONG")];
|
|
122
|
+
},
|
|
123
|
+
} as unknown as DataProvider;
|
|
124
|
+
const routes = makeRoutes({ providers: [provider("preferred", [result("NVDA")]), fallback] });
|
|
125
|
+
|
|
126
|
+
const results = await routes.search("nvidia", { interactive: true });
|
|
127
|
+
|
|
128
|
+
expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
|
|
129
|
+
expect(fallbackCalls).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a fallback provider is asked when the preferred one finds nothing", async () => {
|
|
133
|
+
let fallbackCalls = 0;
|
|
134
|
+
const fallback = {
|
|
135
|
+
id: "fallback",
|
|
136
|
+
search: async () => {
|
|
137
|
+
fallbackCalls += 1;
|
|
138
|
+
return [result("NVDA")];
|
|
139
|
+
},
|
|
140
|
+
} as unknown as DataProvider;
|
|
141
|
+
const routes = makeRoutes({ providers: [provider("preferred", []), fallback] });
|
|
142
|
+
|
|
143
|
+
const results = await routes.search("nvidia", { interactive: true });
|
|
144
|
+
|
|
145
|
+
expect(results.map((item) => item.symbol)).toEqual(["NVDA"]);
|
|
146
|
+
expect(fallbackCalls).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a failing broker leaves the provider result intact", async () => {
|
|
150
|
+
const routes = makeRoutes({
|
|
151
|
+
brokers: [broker("dead", null)],
|
|
152
|
+
providers: [provider("cloud", [result("MSFT")])],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const results = await routes.search("microsoft", { preferBroker: true });
|
|
156
|
+
expect(results.map((item) => item.symbol)).toEqual(["MSFT"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("concurrent identical queries share one run", async () => {
|
|
160
|
+
let calls = 0;
|
|
161
|
+
const counting = {
|
|
162
|
+
id: "cloud",
|
|
163
|
+
search: async () => {
|
|
164
|
+
calls += 1;
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
166
|
+
return [result("TSLA")];
|
|
167
|
+
},
|
|
168
|
+
} as unknown as DataProvider;
|
|
169
|
+
const routes = makeRoutes({ providers: [counting] });
|
|
170
|
+
|
|
171
|
+
const [a, b] = await Promise.all([routes.search("tesla"), routes.search("tesla")]);
|
|
172
|
+
|
|
173
|
+
expect(a.map((item) => item.symbol)).toEqual(["TSLA"]);
|
|
174
|
+
expect(b.map((item) => item.symbol)).toEqual(["TSLA"]);
|
|
175
|
+
expect(calls).toBe(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("results returned to one caller are not mutated by a later source", async () => {
|
|
179
|
+
const routes = makeRoutes({
|
|
180
|
+
brokers: [broker("late", [result("AMD")], 50)],
|
|
181
|
+
providers: [provider("cloud", [result("NVDA")])],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const first = await routes.search("chips", { preferBroker: true } as SearchRequestContext);
|
|
185
|
+
expect(first).toHaveLength(1);
|
|
186
|
+
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
188
|
+
// The array handed back stays the answer that was handed back.
|
|
189
|
+
expect(first).toHaveLength(1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -7,6 +7,12 @@ const SEARCH_CACHE_TTL_MS = 30_000;
|
|
|
7
7
|
const SEARCH_CACHE_MAX_ENTRIES = 100;
|
|
8
8
|
|
|
9
9
|
const SEARCH_PROVIDER_TIMEOUT_MS = 5_000;
|
|
10
|
+
/**
|
|
11
|
+
* A keystroke path cannot wait the batch budget on a source that is failing
|
|
12
|
+
* slowly. A broker whose gateway is down is the common case, and five seconds
|
|
13
|
+
* of it is indistinguishable from the feature being broken.
|
|
14
|
+
*/
|
|
15
|
+
const INTERACTIVE_SEARCH_TIMEOUT_MS = 800;
|
|
10
16
|
|
|
11
17
|
interface BrokerSearchCandidate {
|
|
12
18
|
brokerId: string;
|
|
@@ -14,9 +20,9 @@ interface BrokerSearchCandidate {
|
|
|
14
20
|
brokerLabel: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
function withSearchTimeout<T>(promise: Promise<T
|
|
23
|
+
function withSearchTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null> {
|
|
18
24
|
return new Promise<T | null>((resolve) => {
|
|
19
|
-
const timer = setTimeout(() => resolve(null),
|
|
25
|
+
const timer = setTimeout(() => resolve(null), timeoutMs);
|
|
20
26
|
promise.then(
|
|
21
27
|
(value) => { clearTimeout(timer); resolve(value); },
|
|
22
28
|
() => { clearTimeout(timer); resolve(null); },
|
|
@@ -93,72 +99,119 @@ export class ProviderRouterSearchRoutes {
|
|
|
93
99
|
const inFlight = this.searchInFlight.get(cacheKey);
|
|
94
100
|
if (inFlight) return inFlight;
|
|
95
101
|
|
|
96
|
-
const
|
|
102
|
+
const { first, settled } = this.fetchSearchResults(query, context, cacheKey);
|
|
103
|
+
// Held until every source settles, not until the first answer, so a repeat
|
|
104
|
+
// of the same query joins this run instead of starting a second one.
|
|
105
|
+
this.searchInFlight.set(cacheKey, first);
|
|
106
|
+
void settled.finally(() => {
|
|
97
107
|
this.searchInFlight.delete(cacheKey);
|
|
98
108
|
});
|
|
99
|
-
|
|
100
|
-
this.searchInFlight.set(cacheKey, task);
|
|
101
|
-
return task;
|
|
109
|
+
return first;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Brokers and the preferred provider run at once, rather than each being
|
|
114
|
+
* awaited in turn. The old order tried brokers one at a time before the
|
|
115
|
+
* preferred provider was asked at all, so one unreachable broker delayed
|
|
116
|
+
* every lookup by its full timeout.
|
|
117
|
+
*
|
|
118
|
+
* Fallback providers stay a fallback: they are only asked when that first
|
|
119
|
+
* round found nothing, because they are metered upstreams and racing them on
|
|
120
|
+
* every keystroke would bill for answers already in hand.
|
|
121
|
+
*
|
|
122
|
+
* Ordering still matters, it just no longer costs latency: `mergeSearchResults`
|
|
123
|
+
* keeps the richest entry per symbol, so a broker arriving after the cloud
|
|
124
|
+
* still upgrades the row, and `onPartial` hands that upgrade to the caller.
|
|
125
|
+
*/
|
|
126
|
+
private fetchSearchResults(
|
|
105
127
|
query: string,
|
|
106
128
|
context: SearchRequestContext | undefined,
|
|
107
129
|
cacheKey: string,
|
|
108
|
-
): Promise<InstrumentSearchResult[]> {
|
|
130
|
+
): { first: Promise<InstrumentSearchResult[]>; settled: Promise<void> } {
|
|
109
131
|
const results: InstrumentSearchResult[] = [];
|
|
110
132
|
const resultIndexByKey = new Map<string, number>();
|
|
133
|
+
const timeoutMs = context?.interactive
|
|
134
|
+
? INTERACTIVE_SEARCH_TIMEOUT_MS
|
|
135
|
+
: SEARCH_PROVIDER_TIMEOUT_MS;
|
|
136
|
+
|
|
111
137
|
const cacheResults = (ttl = SEARCH_CACHE_TTL_MS) => {
|
|
112
138
|
this.searchCache.set(cacheKey, {
|
|
113
139
|
expiresAt: Date.now() + ttl,
|
|
114
|
-
results,
|
|
140
|
+
results: [...results],
|
|
115
141
|
});
|
|
116
142
|
if (this.searchCache.size > SEARCH_CACHE_MAX_ENTRIES) {
|
|
117
143
|
this.searchCache.delete(this.searchCache.keys().next().value!);
|
|
118
144
|
}
|
|
119
145
|
};
|
|
120
|
-
|
|
146
|
+
|
|
147
|
+
let resolveFirst!: (value: InstrumentSearchResult[]) => void;
|
|
148
|
+
const first = new Promise<InstrumentSearchResult[]>((resolve) => {
|
|
149
|
+
resolveFirst = resolve;
|
|
150
|
+
});
|
|
151
|
+
let answered = false;
|
|
152
|
+
|
|
153
|
+
const record = (items: InstrumentSearchResult[] | null) => {
|
|
154
|
+
if (!items?.length) return;
|
|
121
155
|
mergeSearchResults(results, resultIndexByKey, items, context);
|
|
156
|
+
cacheResults();
|
|
157
|
+
if (!answered) {
|
|
158
|
+
answered = true;
|
|
159
|
+
resolveFirst([...results]);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// The caller already has an answer, so a later, richer source is handed
|
|
163
|
+
// over rather than silently dropped into the cache.
|
|
164
|
+
context?.onPartial?.([...results]);
|
|
122
165
|
};
|
|
123
166
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
push(annotateBrokerSearchResults(items, candidate));
|
|
134
|
-
if (results.length > 0) {
|
|
135
|
-
cacheResults();
|
|
136
|
-
return results;
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// continue through broker candidates
|
|
140
|
-
}
|
|
167
|
+
const searchBroker = (candidate: BrokerCandidate) => (async () => {
|
|
168
|
+
try {
|
|
169
|
+
const items = await withSearchTimeout(
|
|
170
|
+
candidate.broker.searchInstruments!(query, candidate.instance),
|
|
171
|
+
timeoutMs,
|
|
172
|
+
);
|
|
173
|
+
record(items && annotateBrokerSearchResults(items, candidate));
|
|
174
|
+
} catch {
|
|
175
|
+
// A broker that cannot answer must not hold up the ones that can.
|
|
141
176
|
}
|
|
142
|
-
}
|
|
177
|
+
})();
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
const searchProvider = (provider: DataProvider) => (async () => {
|
|
145
180
|
try {
|
|
146
|
-
|
|
147
|
-
if (!items) continue;
|
|
148
|
-
push(items);
|
|
149
|
-
if (results.length > 0) {
|
|
150
|
-
cacheResults();
|
|
151
|
-
return results;
|
|
152
|
-
}
|
|
181
|
+
record(await withSearchTimeout(provider.search(query, context), timeoutMs));
|
|
153
182
|
} catch (error) {
|
|
154
183
|
if (shouldLogProviderError(error)) {
|
|
155
184
|
this.deps.logProviderError(`${provider.id} failed: ${error}`);
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
|
-
}
|
|
187
|
+
})();
|
|
188
|
+
|
|
189
|
+
const brokers = context?.preferBroker === false
|
|
190
|
+
? []
|
|
191
|
+
: this.deps
|
|
192
|
+
.getBrokerCandidates(context?.brokerInstanceId, context?.brokerId)
|
|
193
|
+
.filter((candidate) => candidate.broker.searchInstruments);
|
|
194
|
+
const [preferred, ...fallbacks] = this.deps.providersInPriorityOrder();
|
|
195
|
+
|
|
196
|
+
const settled = (async () => {
|
|
197
|
+
await Promise.allSettled([
|
|
198
|
+
...brokers.map(searchBroker),
|
|
199
|
+
...(preferred ? [searchProvider(preferred)] : []),
|
|
200
|
+
]);
|
|
201
|
+
if (results.length === 0) {
|
|
202
|
+
await Promise.allSettled(fallbacks.map(searchProvider));
|
|
203
|
+
}
|
|
204
|
+
})().then(() => {
|
|
205
|
+
if (!answered) {
|
|
206
|
+
answered = true;
|
|
207
|
+
// Nothing matched anywhere, so hold the empty answer briefly rather
|
|
208
|
+
// than asking every source again on the next keystroke.
|
|
209
|
+
cacheResults(Math.min(SEARCH_CACHE_TTL_MS, 5_000));
|
|
210
|
+
resolveFirst([]);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
159
213
|
|
|
160
|
-
|
|
161
|
-
return results;
|
|
214
|
+
return { first, settled };
|
|
162
215
|
}
|
|
163
216
|
}
|
|
164
217
|
|
|
@@ -287,6 +287,7 @@ export function usePaneStateValue<T>(key: string, fallback: T, paneId?: string):
|
|
|
287
287
|
const resolved = typeof nextValue === "function"
|
|
288
288
|
? (nextValue as (previousValue: T) => T)(currentValue)
|
|
289
289
|
: nextValue;
|
|
290
|
+
if (Object.is(currentValue, resolved)) return;
|
|
290
291
|
dispatch({ type: "UPDATE_PANE_STATE", paneId: scopedPaneId, patch: { [key]: resolved } });
|
|
291
292
|
}, [dispatch, key, scopedPaneId, stateRef]);
|
|
292
293
|
return [value, setValue];
|
|
@@ -309,6 +310,7 @@ export function usePaneSettingValue<T>(
|
|
|
309
310
|
const resolved = typeof nextValue === "function"
|
|
310
311
|
? (nextValue as (previousValue: T) => T)(currentValue)
|
|
311
312
|
: nextValue;
|
|
313
|
+
if (Object.is(currentValue, resolved)) return;
|
|
312
314
|
const layout = setPaneSetting(currentState.config.layout, scopedPaneId, key, resolved);
|
|
313
315
|
const nextConfig = syncConfigActiveLayoutState(
|
|
314
316
|
{ ...currentState.config, layout },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useRef, type Dispatch } from "react";
|
|
3
3
|
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
|
-
import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneTicker, type AppAction } from "./index";
|
|
4
|
+
import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneSettingValue, usePaneTicker, type AppAction } from "./index";
|
|
5
5
|
import { cloneLayout, createDefaultConfig, type AppConfig } from "../../../types/config";
|
|
6
6
|
import { applyTheme } from "../../../theme/colors";
|
|
7
7
|
import { useThemeId } from "../../../theme/theme-context";
|
|
@@ -11,6 +11,7 @@ const TEST_PANE_ID = "ticker-detail:test";
|
|
|
11
11
|
|
|
12
12
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
13
13
|
let capturedDispatch: Dispatch<AppAction> | null = null;
|
|
14
|
+
let capturedPaneSetting: ((value: string) => void) | null = null;
|
|
14
15
|
|
|
15
16
|
function createTickerDetailConfig(symbol: string): AppConfig {
|
|
16
17
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
@@ -44,6 +45,14 @@ function PaneTickerHarness() {
|
|
|
44
45
|
return <text>{`${symbol ?? "none"}:${renderCountRef.current}`}</text>;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function PaneSettingHarness() {
|
|
49
|
+
const renderCountRef = useRef(0);
|
|
50
|
+
renderCountRef.current += 1;
|
|
51
|
+
const [value, setValue] = usePaneSettingValue("mode", "table");
|
|
52
|
+
capturedPaneSetting = setValue;
|
|
53
|
+
return <text>{`${value}:${renderCountRef.current}`}</text>;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
function ThemeSelectorHarness() {
|
|
48
57
|
const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
|
|
49
58
|
const themeId = useThemeId();
|
|
@@ -99,6 +108,7 @@ describe("pane selectors", () => {
|
|
|
99
108
|
}
|
|
100
109
|
testSetup = undefined;
|
|
101
110
|
capturedDispatch = null;
|
|
111
|
+
capturedPaneSetting = null;
|
|
102
112
|
applyTheme("amber");
|
|
103
113
|
});
|
|
104
114
|
|
|
@@ -125,6 +135,28 @@ describe("pane selectors", () => {
|
|
|
125
135
|
expect(testSetup.captureCharFrame()).toContain("AAPL:1");
|
|
126
136
|
});
|
|
127
137
|
|
|
138
|
+
test("does not dispatch or rerender when a pane setting keeps its effective value", async () => {
|
|
139
|
+
testSetup = await testRender(
|
|
140
|
+
<AppProvider config={createTickerDetailConfig("AAPL")}>
|
|
141
|
+
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
142
|
+
<PaneSettingHarness />
|
|
143
|
+
</PaneInstanceProvider>
|
|
144
|
+
</AppProvider>,
|
|
145
|
+
{ width: 24, height: 4 },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await testSetup.renderOnce();
|
|
149
|
+
expect(testSetup.captureCharFrame()).toContain("table:1");
|
|
150
|
+
|
|
151
|
+
await act(() => {
|
|
152
|
+
capturedPaneSetting?.("table");
|
|
153
|
+
});
|
|
154
|
+
await testSetup.renderOnce();
|
|
155
|
+
await testSetup.renderOnce();
|
|
156
|
+
|
|
157
|
+
expect(testSetup.captureCharFrame()).toContain("table:1");
|
|
158
|
+
});
|
|
159
|
+
|
|
128
160
|
test("rerenders theme hook consumers when the theme changes", async () => {
|
|
129
161
|
testSetup = await testRender(
|
|
130
162
|
<AppProvider config={createTickerDetailConfig("AAPL")}>
|
|
@@ -10,6 +10,7 @@ import { useLiveQuoteEntries, useQuoteStreaming, useQuoteUpdates } from "./quote
|
|
|
10
10
|
|
|
11
11
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
12
12
|
let bumpHarness: (() => void) | null = null;
|
|
13
|
+
let toggleStreamingPriority: (() => void) | null = null;
|
|
13
14
|
let togglePollingPriority: (() => void) | null = null;
|
|
14
15
|
let updateLiveTargets: ((symbol: string) => void) | null = null;
|
|
15
16
|
let updateFreshnessScope: ((scope: string) => void) | null = null;
|
|
@@ -27,6 +28,31 @@ function QuoteStreamingHarness() {
|
|
|
27
28
|
return <text>{String(tick)}</text>;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function QuoteStreamingPriorityHarness() {
|
|
32
|
+
const [selected, setSelected] = useState(false);
|
|
33
|
+
toggleStreamingPriority = () => setSelected((current) => !current);
|
|
34
|
+
useQuoteStreaming([{
|
|
35
|
+
symbol: "AAPL",
|
|
36
|
+
exchange: "NASDAQ",
|
|
37
|
+
context: {
|
|
38
|
+
brokerId: "ibkr",
|
|
39
|
+
brokerInstanceId: "ibkr-work",
|
|
40
|
+
instrument: {
|
|
41
|
+
brokerId: "ibkr",
|
|
42
|
+
brokerInstanceId: "ibkr-work",
|
|
43
|
+
conId: 1001,
|
|
44
|
+
symbol: "AAPL",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
route: "provider",
|
|
48
|
+
surface: selected ? "detail" : "portfolio",
|
|
49
|
+
visible: true,
|
|
50
|
+
selected,
|
|
51
|
+
weight: selected ? 100 : 10,
|
|
52
|
+
}]);
|
|
53
|
+
return <text>{selected ? "selected" : "idle"}</text>;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
function QuotePollingHarness() {
|
|
31
57
|
useQuoteUpdates([{
|
|
32
58
|
symbol: "AAPL",
|
|
@@ -82,6 +108,7 @@ afterEach(async () => {
|
|
|
82
108
|
testSetup = undefined;
|
|
83
109
|
}
|
|
84
110
|
bumpHarness = null;
|
|
111
|
+
toggleStreamingPriority = null;
|
|
85
112
|
togglePollingPriority = null;
|
|
86
113
|
updateLiveTargets = null;
|
|
87
114
|
updateFreshnessScope = null;
|
|
@@ -127,6 +154,72 @@ describe("useQuoteStreaming", () => {
|
|
|
127
154
|
expect(unsubscribeCalls).toBe(0);
|
|
128
155
|
});
|
|
129
156
|
|
|
157
|
+
test("updates priorities without replacing the hook subscription", async () => {
|
|
158
|
+
type CoordinatorTargets = Parameters<MarketDataCoordinator["subscribeQuotes"]>[0];
|
|
159
|
+
const initialTargets: CoordinatorTargets[] = [];
|
|
160
|
+
const updates: CoordinatorTargets[] = [];
|
|
161
|
+
let unsubscribeCalls = 0;
|
|
162
|
+
const coordinator = {
|
|
163
|
+
subscribeQuotes: (targets: CoordinatorTargets) => {
|
|
164
|
+
initialTargets.push(targets);
|
|
165
|
+
return Object.assign(
|
|
166
|
+
() => { unsubscribeCalls += 1; },
|
|
167
|
+
{ update: (nextTargets: CoordinatorTargets) => updates.push(nextTargets) },
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator);
|
|
172
|
+
|
|
173
|
+
testSetup = await testRender(<QuoteStreamingPriorityHarness />, {
|
|
174
|
+
width: 20,
|
|
175
|
+
height: 1,
|
|
176
|
+
});
|
|
177
|
+
await act(async () => testSetup!.renderOnce());
|
|
178
|
+
|
|
179
|
+
expect(initialTargets).toHaveLength(1);
|
|
180
|
+
expect(initialTargets[0]?.[0]).toMatchObject({
|
|
181
|
+
instrument: {
|
|
182
|
+
symbol: "AAPL",
|
|
183
|
+
exchange: "NASDAQ",
|
|
184
|
+
brokerId: "ibkr",
|
|
185
|
+
brokerInstanceId: "ibkr-work",
|
|
186
|
+
instrument: { conId: 1001, symbol: "AAPL" },
|
|
187
|
+
},
|
|
188
|
+
priority: {
|
|
189
|
+
route: "provider",
|
|
190
|
+
surface: "portfolio",
|
|
191
|
+
visible: true,
|
|
192
|
+
selected: false,
|
|
193
|
+
weight: 10,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await act(async () => {
|
|
198
|
+
toggleStreamingPriority?.();
|
|
199
|
+
await Promise.resolve();
|
|
200
|
+
await testSetup!.renderOnce();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(initialTargets).toHaveLength(1);
|
|
204
|
+
expect(unsubscribeCalls).toBe(0);
|
|
205
|
+
expect(updates).toHaveLength(1);
|
|
206
|
+
expect(updates[0]?.[0]).toMatchObject({
|
|
207
|
+
instrument: {
|
|
208
|
+
symbol: "AAPL",
|
|
209
|
+
brokerId: "ibkr",
|
|
210
|
+
brokerInstanceId: "ibkr-work",
|
|
211
|
+
instrument: { conId: 1001 },
|
|
212
|
+
},
|
|
213
|
+
priority: {
|
|
214
|
+
route: "provider",
|
|
215
|
+
surface: "detail",
|
|
216
|
+
visible: true,
|
|
217
|
+
selected: true,
|
|
218
|
+
weight: 100,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
130
223
|
test("uses forced polling instead of a subscription when live streaming is disabled", async () => {
|
|
131
224
|
let subscribeCalls = 0;
|
|
132
225
|
let loadCalls = 0;
|