gloomberb 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -11
- package/package.json +13 -5
- package/src/app.tsx +414 -191
- package/src/architecture/import-boundaries.test.ts +118 -0
- package/src/assets/gloomberb-logo.png +0 -0
- package/src/assets/gloomberb-logo.svg +73 -0
- package/src/brokers/account-cache.ts +157 -0
- package/src/brokers/profile-form.test.ts +73 -0
- package/src/brokers/profile-form.ts +109 -0
- package/src/brokers/remote-broker-adapter.ts +91 -0
- package/src/brokers/sync-broker-instance.test.ts +124 -1
- package/src/brokers/sync-broker-instance.ts +113 -17
- package/src/capabilities/factories.ts +100 -0
- package/src/capabilities/index.ts +4 -0
- package/src/capabilities/registry.test.ts +137 -0
- package/src/capabilities/registry.ts +131 -0
- package/src/capabilities/service-capabilities.ts +27 -0
- package/src/capabilities/types.ts +84 -0
- package/src/cli/commands/ticker.ts +11 -3
- package/src/cli/context.ts +19 -13
- package/src/cli/helpers.ts +1 -0
- package/src/cli/types.ts +2 -2
- package/src/components/chart/chart-controller.ts +4 -24
- package/src/components/chart/chart-data.ts +3 -14
- package/src/components/chart/chart-indicator-selector.tsx +28 -6
- package/src/components/chart/chart-pane-settings.ts +15 -28
- package/src/components/chart/chart-renderer.ts +74 -99
- package/src/components/chart/chart-resolution.ts +10 -10
- package/src/components/chart/chart-scroll.test.ts +6 -5
- package/src/components/chart/chart-scroll.ts +2 -2
- package/src/components/chart/chart-types.ts +0 -1
- package/src/components/chart/chart-viewport.ts +2 -1
- package/src/components/chart/chart.test.ts +37 -0
- package/src/components/chart/comparison-chart-data.ts +3 -17
- package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
- package/src/components/chart/comparison-chart-renderer.ts +1 -1
- package/src/components/chart/comparison-stock-chart.tsx +318 -158
- package/src/components/chart/cursor-motion.ts +3 -2
- package/src/components/chart/indicators/bands.test.ts +0 -14
- package/src/components/chart/indicators/moving-averages.test.ts +0 -15
- package/src/components/chart/indicators/options.ts +3 -3
- package/src/components/chart/indicators/oscillators.test.ts +0 -21
- package/src/components/chart/indicators/types.ts +1 -2
- package/src/components/chart/native/chart-rasterizer.ts +37 -44
- package/src/components/chart/native/kitty-adapter.ts +2 -5
- package/src/components/chart/native/kitty-manager.ts +1 -1
- package/src/components/chart/native/kitty-protocol.ts +0 -1
- package/src/components/chart/native/kitty-support.ts +1 -1
- package/src/components/chart/native/kitty.test.ts +53 -13
- package/src/components/chart/native/renderer-selection.ts +1 -1
- package/src/components/chart/native/surface-manager.ts +1 -1
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.test.tsx +51 -0
- package/src/components/chart/static-chart-surface.tsx +191 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
- package/src/components/chart/stock-chart.tsx +452 -191
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +269 -20
- package/src/components/command-bar/command-bar.tsx +830 -353
- package/src/components/command-bar/command-registry.ts +10 -20
- package/src/components/command-bar/helpers.test.ts +1 -1
- package/src/components/command-bar/helpers.ts +9 -11
- package/src/components/command-bar/pane-template-display.ts +4 -8
- package/src/components/command-bar/root-shortcuts.ts +12 -11
- package/src/components/command-bar/view-model.test.ts +8 -42
- package/src/components/command-bar/view-model.ts +8 -36
- package/src/components/command-bar/workflow-ops.test.ts +76 -2
- package/src/components/command-bar/workflow-ops.ts +40 -9
- package/src/components/command-bar/workflow-types.ts +2 -1
- package/src/components/data-table-stack-view.test.tsx +206 -0
- package/src/components/data-table-stack-view.tsx +152 -0
- package/src/components/data-table-view.test.tsx +188 -0
- package/src/components/data-table-view.tsx +279 -0
- package/src/components/feed-data-table-stack-view.test.tsx +225 -0
- package/src/components/feed-data-table-stack-view.tsx +365 -0
- package/src/components/index.ts +26 -31
- package/src/components/layout/detached-pane-shell.tsx +185 -0
- package/src/components/layout/floating-pane.tsx +87 -14
- package/src/components/layout/header.tsx +139 -49
- package/src/components/layout/pane-content.tsx +42 -0
- package/src/components/layout/pane-footer.test.tsx +195 -0
- package/src/components/layout/pane-footer.tsx +425 -0
- package/src/components/layout/pane-header.test.tsx +39 -0
- package/src/components/layout/pane-header.tsx +192 -14
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/pane-sizing.ts +9 -10
- package/src/components/layout/pane-title.ts +35 -0
- package/src/components/layout/pane.tsx +52 -11
- package/src/components/layout/shell.test.tsx +315 -107
- package/src/components/layout/shell.tsx +1010 -368
- package/src/components/layout/status-bar.test.tsx +17 -10
- package/src/components/layout/status-bar.tsx +367 -100
- package/src/components/layout/titlebar-overlay.ts +2 -0
- package/src/components/markdown-editor.tsx +29 -14
- package/src/components/markdown-text.tsx +25 -24
- package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
- package/src/components/onboarding/onboarding-wizard.tsx +347 -473
- package/src/components/pane-settings-dialog.tsx +521 -89
- package/src/components/pane-template-wizard.test.tsx +1 -1
- package/src/components/pane-template-wizard.tsx +38 -37
- package/src/components/price-selector-dialog.tsx +22 -21
- package/src/components/ticker-badge-list.tsx +53 -0
- package/src/components/ticker-badge-text.test.tsx +44 -1
- package/src/components/ticker-badge-text.tsx +78 -38
- package/src/components/ticker-badge.tsx +28 -7
- package/src/components/ticker-list-table-view.tsx +248 -0
- package/src/components/ticker-list-table.test.tsx +47 -1
- package/src/components/ticker-list-table.tsx +212 -41
- package/src/components/toggle-list.tsx +114 -8
- package/src/components/ui/button.tsx +24 -16
- package/src/components/ui/choice-dialog.tsx +89 -0
- package/src/components/ui/data-table.tsx +313 -114
- package/src/components/ui/external-link.tsx +52 -13
- package/src/components/ui/fields.tsx +44 -30
- package/src/components/ui/frame.tsx +24 -57
- package/src/components/ui/index.ts +15 -50
- package/src/components/ui/list-view.tsx +65 -25
- package/src/components/ui/loading.tsx +5 -60
- package/src/components/ui/message-composer.test.tsx +81 -0
- package/src/components/ui/message-composer.tsx +139 -0
- package/src/components/ui/multi-select-dialog.tsx +100 -32
- package/src/components/ui/multi-select.ts +112 -0
- package/src/components/ui/native-select.tsx +120 -0
- package/src/components/ui/page-stack-view.tsx +49 -19
- package/src/components/ui/remote-image.tsx +50 -0
- package/src/components/ui/row-value-cache.test.ts +17 -0
- package/src/components/ui/row-value-cache.ts +41 -0
- package/src/components/ui/shortcut-hint.tsx +55 -0
- package/src/components/ui/status.tsx +12 -67
- package/src/components/ui/table-layout.ts +59 -0
- package/src/components/ui/tabs.tsx +249 -27
- package/src/components/ui/toggle.tsx +19 -127
- package/src/components/ui/ui.test.tsx +266 -53
- package/src/components/use-double-click-activation.ts +7 -2
- package/src/components/use-external-link-footer.ts +79 -0
- package/src/core/app-services.ts +95 -0
- package/src/core/state/app-state.ts +886 -0
- package/src/{state → core/state}/session-persistence.ts +7 -6
- package/src/data/config-layout.ts +225 -0
- package/src/data/config-store-node.ts +390 -0
- package/src/data/config-store.test.ts +72 -0
- package/src/data/config-store.ts +35 -478
- package/src/data/sqlite-json.ts +2 -5
- package/src/data/sqlite-retry.ts +1 -0
- package/src/index.tsx +2 -68
- package/src/market-data/coordinator-subscriptions.test.ts +125 -0
- package/src/market-data/coordinator.test.ts +73 -4
- package/src/market-data/coordinator.ts +190 -24
- package/src/market-data/hooks.test.tsx +1 -1
- package/src/market-data/hooks.tsx +109 -87
- package/src/market-data/query-store.ts +3 -3
- package/src/market-data/request-types.test.ts +0 -14
- package/src/market-data/result-types.ts +1 -0
- package/src/news/aggregator.test.ts +267 -0
- package/src/news/aggregator.ts +452 -0
- package/src/news/hooks.ts +39 -0
- package/src/news/ticker-symbols.ts +21 -0
- package/src/news/types.ts +77 -0
- package/src/notifications/app-notifier.ts +21 -10
- package/src/pane-settings.test.ts +0 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
- package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +41 -5
- package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
- package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
- package/src/plugins/builtin/ai/settings.ts +0 -1
- package/src/plugins/builtin/ai/utils.ts +1 -71
- package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
- package/src/plugins/builtin/alerts/index.test.tsx +61 -82
- package/src/plugins/builtin/alerts/index.tsx +263 -155
- package/src/plugins/builtin/alerts/types.ts +7 -0
- package/src/plugins/builtin/analytics/index.test.tsx +13 -8
- package/src/plugins/builtin/analytics/index.tsx +78 -120
- package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
- package/src/plugins/builtin/ask-ai.test.tsx +55 -54
- package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
- package/src/plugins/builtin/broker-manager/index.tsx +759 -0
- package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
- package/src/plugins/builtin/broker-manager/model.ts +142 -0
- package/src/plugins/builtin/chat-controller.test.ts +129 -0
- package/src/plugins/builtin/chat-controller.ts +85 -14
- package/src/plugins/builtin/chat.test.tsx +283 -36
- package/src/plugins/builtin/chat.tsx +363 -170
- package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
- package/src/plugins/builtin/cloud-tweets.tsx +896 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
- package/src/plugins/builtin/comparison-chart.tsx +11 -11
- package/src/plugins/builtin/correlation/compute.test.ts +0 -17
- package/src/plugins/builtin/correlation/index.tsx +51 -46
- package/src/plugins/builtin/correlation/settings.ts +1 -2
- package/src/plugins/builtin/debug.tsx +99 -70
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
- package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
- package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
- package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
- package/src/plugins/builtin/earnings/index.tsx +187 -94
- package/src/plugins/builtin/econ/calendar-source.ts +9 -116
- package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
- package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
- package/src/plugins/builtin/econ/index.tsx +236 -421
- package/src/plugins/builtin/econ/types.ts +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
- package/src/plugins/builtin/help.test.tsx +120 -60
- package/src/plugins/builtin/help.tsx +266 -82
- package/src/plugins/builtin/holders/index.test.ts +45 -0
- package/src/plugins/builtin/holders/index.tsx +949 -0
- package/src/plugins/builtin/insider/index.tsx +207 -253
- package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
- package/src/plugins/builtin/insider/insider-data.ts +1 -0
- package/src/plugins/builtin/layout-manager.tsx +12 -40
- package/src/plugins/builtin/market-movers/index.tsx +70 -144
- package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
- package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
- package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
- package/src/plugins/builtin/news-wire/categories.ts +114 -0
- package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
- package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
- package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
- package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
- package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
- package/src/plugins/builtin/news-wire/hash.ts +9 -0
- package/src/plugins/builtin/news-wire/index.tsx +92 -0
- package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
- package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
- package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
- package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
- package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
- package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
- package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
- package/src/plugins/builtin/news-wire/read-state.ts +76 -0
- package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
- package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
- package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
- package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
- package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
- package/src/plugins/builtin/news.tsx +98 -25
- package/src/plugins/builtin/notes-files.ts +51 -10
- package/src/plugins/builtin/notes.tsx +332 -131
- package/src/plugins/builtin/options.test.tsx +185 -0
- package/src/plugins/builtin/options.tsx +346 -206
- package/src/plugins/builtin/plugin-groups.ts +92 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
- package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
- package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
- package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
- package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
- package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
- package/src/plugins/builtin/portfolio-list.test.ts +60 -1
- package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
- package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
- package/src/plugins/builtin/research/index.tsx +724 -0
- package/src/plugins/builtin/sec.tsx +67 -19
- package/src/plugins/builtin/sectors/index.tsx +42 -80
- package/src/plugins/builtin/sectors/sector-data.ts +1 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
- package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
- package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
- package/src/plugins/builtin/ticker-surface.ts +68 -0
- package/src/plugins/builtin/world-indices/index.tsx +44 -82
- package/src/plugins/builtin/world-indices/indices.ts +1 -0
- package/src/plugins/builtin/yahoo.ts +55 -1
- package/src/plugins/builtin/yield-curve/index.tsx +87 -145
- package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
- package/src/plugins/catalog-ui.test.ts +24 -0
- package/src/plugins/catalog-ui.ts +44 -0
- package/src/plugins/catalog.ts +2 -50
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/account-cache.test.ts +15 -14
- package/src/plugins/ibkr/account-cache.ts +15 -120
- package/src/plugins/ibkr/broker-adapter.ts +67 -0
- package/src/plugins/ibkr/config.test.ts +0 -18
- package/src/plugins/ibkr/dialogs.tsx +14 -56
- package/src/plugins/ibkr/flex.test.ts +57 -2
- package/src/plugins/ibkr/flex.ts +116 -12
- package/src/plugins/ibkr/gateway-helpers.ts +8 -3
- package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
- package/src/plugins/ibkr/gateway-service.test.ts +47 -9
- package/src/plugins/ibkr/gateway-service.ts +264 -2235
- package/src/plugins/ibkr/gateway-types.ts +27 -0
- package/src/plugins/ibkr/index.test.tsx +3 -11
- package/src/plugins/ibkr/index.tsx +1 -0
- package/src/plugins/ibkr/price-normalization.test.ts +1 -6
- package/src/plugins/ibkr/price-normalization.ts +1 -0
- package/src/plugins/ibkr/trade-badge.tsx +6 -5
- package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
- package/src/plugins/ibkr/trade-tab.tsx +166 -117
- package/src/plugins/ibkr/trading-pane.tsx +117 -83
- package/src/plugins/pane-manager.test.ts +3 -1
- package/src/plugins/pane-manager.ts +93 -19
- package/src/plugins/plugin-runtime.test.tsx +176 -74
- package/src/plugins/plugin-runtime.tsx +228 -10
- package/src/plugins/prediction-markets/cache.ts +49 -0
- package/src/plugins/prediction-markets/categories.ts +0 -10
- package/src/plugins/prediction-markets/chart.test.tsx +1 -1
- package/src/plugins/prediction-markets/chart.tsx +21 -20
- package/src/plugins/prediction-markets/cli.ts +1 -0
- package/src/plugins/prediction-markets/controller-data.ts +135 -89
- package/src/plugins/prediction-markets/controller.ts +37 -108
- package/src/plugins/prediction-markets/detail/book.tsx +23 -22
- package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
- package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
- package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
- package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
- package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
- package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
- package/src/plugins/prediction-markets/detail.test.tsx +1 -1
- package/src/plugins/prediction-markets/keyboard.ts +1 -0
- package/src/plugins/prediction-markets/launch.test.ts +1 -1
- package/src/plugins/prediction-markets/launch.ts +1 -1
- package/src/plugins/prediction-markets/metrics.ts +25 -12
- package/src/plugins/prediction-markets/pane.test.tsx +16 -7
- package/src/plugins/prediction-markets/pane.tsx +180 -82
- package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
- package/src/plugins/prediction-markets/services/fetch.ts +27 -10
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/settings.ts +0 -5
- package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
- package/src/plugins/registry.test.ts +307 -0
- package/src/plugins/registry.ts +293 -82
- package/src/react/input.ts +61 -0
- package/src/react/plugins/plugin-slot.tsx +21 -0
- package/src/react/use-raf-callback.ts +44 -0
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
- package/src/renderers/electrobun/bun/application-menu.ts +118 -0
- package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
- package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
- package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
- package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
- package/src/renderers/electrobun/bun/index.ts +1149 -0
- package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
- package/src/renderers/electrobun/bun/window-frame.ts +46 -0
- package/src/renderers/electrobun/shared/protocol.ts +72 -0
- package/src/renderers/electrobun/view/ai-host.ts +93 -0
- package/src/renderers/electrobun/view/app-services.ts +348 -0
- package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
- package/src/renderers/electrobun/view/config-host.ts +35 -0
- package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
- package/src/renderers/electrobun/view/context-menu.ts +164 -0
- package/src/renderers/electrobun/view/data-table.tsx +773 -0
- package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
- package/src/renderers/electrobun/view/http-fetch.ts +113 -0
- package/src/renderers/electrobun/view/input-host.tsx +112 -0
- package/src/renderers/electrobun/view/key-event.test.ts +34 -0
- package/src/renderers/electrobun/view/key-event.ts +134 -0
- package/src/renderers/electrobun/view/main.tsx +104 -0
- package/src/renderers/electrobun/view/native-renderer.ts +110 -0
- package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
- package/src/renderers/electrobun/view/notes-files.ts +56 -0
- package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
- package/src/renderers/electrobun/view/resource-store.ts +106 -0
- package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
- package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
- package/src/renderers/electrobun/view/toast-host.tsx +65 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
- package/src/renderers/electrobun/view/update-host.ts +38 -0
- package/src/renderers/opentui/dialog-host.test.tsx +44 -0
- package/src/renderers/opentui/dialog-host.tsx +56 -0
- package/src/renderers/opentui/host.tsx +140 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +63 -0
- package/src/renderers/opentui/start.tsx +71 -0
- package/src/renderers/opentui/test-utils.tsx +76 -0
- package/src/renderers/opentui/toast-host.tsx +10 -0
- package/src/renderers/opentui/ui-host.tsx +98 -0
- package/src/sources/gloomberb-cloud.test.ts +288 -17
- package/src/sources/gloomberb-cloud.ts +176 -9
- package/src/sources/provider-errors.ts +1 -0
- package/src/sources/provider-router.test.ts +220 -81
- package/src/sources/provider-router.ts +294 -111
- package/src/sources/sec-edgar.ts +19 -6
- package/src/sources/yahoo-finance.test.ts +0 -5
- package/src/sources/yahoo-finance.ts +420 -11
- package/src/state/app-activity.ts +7 -7
- package/src/state/app-bootstrap.ts +114 -40
- package/src/state/app-context-selector.test.tsx +70 -1
- package/src/state/app-context.test.ts +71 -3
- package/src/state/app-context.test.tsx +14 -7
- package/src/state/app-context.tsx +209 -869
- package/src/state/config-save-scheduler.ts +35 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/persist-scheduler.test.ts +67 -0
- package/src/state/persist-scheduler.ts +69 -0
- package/src/state/selectors-ui.ts +55 -0
- package/src/state/selectors.ts +15 -71
- package/src/state/session-persistence.test.ts +1 -1
- package/src/state/ticker-refresh-queue.ts +52 -3
- package/src/state/use-inline-tickers.ts +18 -16
- package/src/state/use-quote-streaming.test.ts +0 -6
- package/src/state/use-quote-streaming.test.tsx +1 -1
- package/src/state/use-quote-streaming.ts +3 -2
- package/src/state/use-session-snapshot.ts +32 -17
- package/src/test-support/plugin-runtime.ts +149 -0
- package/src/theme/color-utils.ts +1 -0
- package/src/theme/colors.ts +16 -0
- package/src/types/broker.ts +16 -1
- package/src/types/capability-route-source.ts +23 -0
- package/src/types/config.test.ts +23 -0
- package/src/types/config.ts +145 -10
- package/src/types/context-menu.ts +95 -0
- package/src/types/data-provider.ts +34 -3
- package/src/types/desktop-menu.ts +13 -0
- package/src/types/desktop-window.ts +29 -0
- package/src/types/financials.ts +123 -2
- package/src/types/instrument.ts +1 -0
- package/src/types/news-source.ts +9 -0
- package/src/types/persistence.ts +1 -0
- package/src/types/plugin.ts +32 -8
- package/src/ui/ascii-font.ts +85 -0
- package/src/ui/context-menu.test.ts +94 -0
- package/src/ui/context-menu.tsx +433 -0
- package/src/ui/dialog.tsx +55 -0
- package/src/ui/host.tsx +364 -0
- package/src/ui/index.tsx +134 -0
- package/src/ui/toast.tsx +40 -0
- package/src/updater.test.ts +69 -0
- package/src/updater.ts +61 -18
- package/src/utils/api-client.test.ts +99 -9
- package/src/utils/api-client.ts +378 -18
- package/src/utils/back-navigation.ts +6 -0
- package/src/utils/cli-output.ts +1 -0
- package/src/utils/currency-units.test.ts +1 -6
- package/src/utils/currency-units.ts +1 -0
- package/src/utils/debug-log.ts +68 -0
- package/src/utils/dom-screenshot.ts +326 -0
- package/src/utils/exchange-rate-map.ts +1 -0
- package/src/utils/exchanges.ts +214 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +1 -0
- package/src/utils/fuzzy-search.ts +1 -0
- package/src/utils/html-entities.test.ts +8 -0
- package/src/utils/html-entities.ts +32 -0
- package/src/utils/http-transport.ts +11 -0
- package/src/utils/main-thread-monitor.ts +127 -0
- package/src/utils/perf-marks.ts +54 -0
- package/src/utils/quote-freshness.ts +27 -50
- package/src/utils/quote-resolution.test.ts +5 -5
- package/src/utils/quote-resolution.ts +0 -1
- package/src/utils/quote-time.ts +1 -5
- package/src/utils/sec.test.ts +0 -12
- package/src/utils/selection-clipboard.ts +14 -6
- package/src/utils/shortcut-labels.test.ts +20 -0
- package/src/utils/shortcut-labels.ts +67 -0
- package/src/utils/slugify.ts +1 -0
- package/src/utils/terminal-input-reset.ts +1 -0
- package/src/utils/text-wrap.ts +82 -0
- package/src/utils/throttled-fetch.test.ts +19 -0
- package/src/utils/throttled-fetch.ts +11 -9
- package/src/utils/ticker-list.test.ts +1 -7
- package/src/utils/ticker-list.ts +1 -0
- package/src/utils/timestamp.ts +29 -0
- package/src/utils/tweet-text.test.ts +13 -0
- package/src/utils/tweet-text.ts +18 -0
- package/src/utils/url.ts +11 -0
- package/src/version.ts +1 -1
- package/src/components/chart/chart-control-hint.tsx +0 -39
- package/src/components/detail-feed-view.test.tsx +0 -180
- package/src/components/detail-feed-view.tsx +0 -531
- package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
- package/src/components/layout/pixel-mouse.test.ts +0 -40
- package/src/components/spinner.tsx +0 -2
- package/src/components/tab-bar.tsx +0 -17
- package/src/components/ui/multi-select-chips.tsx +0 -69
- package/src/plugins/builtin/ai/index.test.tsx +0 -139
- package/src/plugins/builtin/ask-ai.tsx +0 -9
- package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
- package/src/plugins/builtin/correlation/settings.test.ts +0 -34
- package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
- package/src/plugins/builtin/econ/fred-client.ts +0 -79
- package/src/plugins/builtin/fred-settings.ts +0 -10
- package/src/plugins/builtin/options-availability.test.tsx +0 -179
- package/src/plugins/builtin/options-availability.ts +0 -158
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
- package/src/plugins/prediction-markets/status.ts +0 -24
- package/src/plugins/prediction-markets/table.tsx +0 -87
- package/src/utils/quote-time.test.ts +0 -22
- package/src/utils/selection-clipboard.test.ts +0 -43
- package/src/utils/terminal-input-reset.test.ts +0 -50
package/src/index.tsx
CHANGED
|
@@ -1,72 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createRoot } from "@opentui/react";
|
|
3
|
-
import { App } from "./app";
|
|
4
|
-
import { getDataDir, initDataDir } from "./data/config-store";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { existsSync, mkdirSync } from "fs";
|
|
7
|
-
import { dispatchCli } from "./cli/index";
|
|
8
|
-
import { loadExternalPlugins } from "./plugins/loader";
|
|
9
|
-
import { debugLog } from "./utils/debug-log";
|
|
10
|
-
import { resetTerminalInputState } from "./utils/terminal-input-reset";
|
|
1
|
+
import { startOpenTuiApp } from "./renderers/opentui/start";
|
|
11
2
|
|
|
12
|
-
|
|
13
|
-
// Intercept console.log/warn/error to capture in debug log
|
|
14
|
-
debugLog.interceptConsole();
|
|
15
|
-
|
|
16
|
-
const appLog = debugLog.createLogger("app");
|
|
17
|
-
appLog.info("Gloomberb starting");
|
|
18
|
-
|
|
19
|
-
// Handle CLI subcommands (install, remove, update, plugins)
|
|
20
|
-
const cliArgs = process.argv.slice(2);
|
|
21
|
-
const externalPlugins = await loadExternalPlugins();
|
|
22
|
-
let cliLaunchRequest = null;
|
|
23
|
-
if (cliArgs.length > 0) {
|
|
24
|
-
const dispatchResult = await dispatchCli(cliArgs, { externalPlugins });
|
|
25
|
-
if (dispatchResult.kind === "handled") return;
|
|
26
|
-
if (dispatchResult.kind === "launch-ui") {
|
|
27
|
-
cliLaunchRequest = dispatchResult.request;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Determine data directory
|
|
32
|
-
let dataDir = await getDataDir();
|
|
33
|
-
|
|
34
|
-
if (!dataDir) {
|
|
35
|
-
// First run - use default location
|
|
36
|
-
dataDir = join(process.env.HOME || "~", ".gloomberb");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Ensure data dir exists
|
|
40
|
-
if (!existsSync(dataDir)) {
|
|
41
|
-
mkdirSync(dataDir, { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Load or create config
|
|
45
|
-
const config = await initDataDir(dataDir);
|
|
46
|
-
|
|
47
|
-
// `bun run --watch` does not guarantee the previous run's renderer can cleanly
|
|
48
|
-
// restore mouse/raw terminal modes before the next run starts.
|
|
49
|
-
resetTerminalInputState();
|
|
50
|
-
|
|
51
|
-
// Create renderer
|
|
52
|
-
const renderer = await createCliRenderer({
|
|
53
|
-
exitOnCtrlC: true,
|
|
54
|
-
backgroundColor: "#000000",
|
|
55
|
-
enableMouseMovement: true,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Render app
|
|
59
|
-
createRoot(renderer).render(
|
|
60
|
-
<App
|
|
61
|
-
config={config}
|
|
62
|
-
renderer={renderer}
|
|
63
|
-
externalPlugins={externalPlugins}
|
|
64
|
-
cliLaunchRequest={cliLaunchRequest}
|
|
65
|
-
/>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
main().catch((err) => {
|
|
3
|
+
startOpenTuiApp().catch((err) => {
|
|
70
4
|
console.error("Fatal error:", err);
|
|
71
5
|
process.exitCode = 1;
|
|
72
6
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { MarketDataCoordinator } from "./coordinator";
|
|
3
|
+
import { buildQuoteKey } from "./selectors";
|
|
4
|
+
import { createTestDataProvider } from "../test-support/data-provider";
|
|
5
|
+
import type { Quote } from "../types/financials";
|
|
6
|
+
import type { DataProvider, QuoteSubscriptionTarget } from "../types/data-provider";
|
|
7
|
+
|
|
8
|
+
function createProvider(): {
|
|
9
|
+
provider: DataProvider;
|
|
10
|
+
emitQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void;
|
|
11
|
+
} {
|
|
12
|
+
let onQuote: ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null = null;
|
|
13
|
+
const provider = createTestDataProvider({
|
|
14
|
+
id: "test-provider",
|
|
15
|
+
subscribeQuotes: (_targets, handler) => {
|
|
16
|
+
onQuote = handler;
|
|
17
|
+
return () => {};
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
provider,
|
|
22
|
+
emitQuote(target, quote) {
|
|
23
|
+
if (!onQuote) throw new Error("subscription was not registered");
|
|
24
|
+
onQuote(target, quote);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function quote(symbol: string, price: number, overrides: Partial<Quote> = {}): Quote {
|
|
30
|
+
return {
|
|
31
|
+
symbol,
|
|
32
|
+
price,
|
|
33
|
+
currency: "USD",
|
|
34
|
+
change: 0,
|
|
35
|
+
changePercent: 0,
|
|
36
|
+
lastUpdated: Date.now(),
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function flushCoordinator(): Promise<void> {
|
|
42
|
+
await Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("MarketDataCoordinator key subscriptions", () => {
|
|
46
|
+
test("notifies listeners for changed keys only", async () => {
|
|
47
|
+
const { provider, emitQuote } = createProvider();
|
|
48
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
49
|
+
const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
50
|
+
const msft = { symbol: "MSFT", exchange: "NASDAQ" };
|
|
51
|
+
let aaplCalls = 0;
|
|
52
|
+
let msftCalls = 0;
|
|
53
|
+
|
|
54
|
+
coordinator.subscribeKeys([buildQuoteKey(aapl)], () => { aaplCalls += 1; });
|
|
55
|
+
coordinator.subscribeKeys([buildQuoteKey(msft)], () => { msftCalls += 1; });
|
|
56
|
+
coordinator.subscribeQuotes([{ instrument: aapl }, { instrument: msft }]);
|
|
57
|
+
|
|
58
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 100));
|
|
59
|
+
await flushCoordinator();
|
|
60
|
+
|
|
61
|
+
expect(aaplCalls).toBe(1);
|
|
62
|
+
expect(msftCalls).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("dedupes listeners subscribed to multiple changed keys", async () => {
|
|
66
|
+
const { provider, emitQuote } = createProvider();
|
|
67
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
68
|
+
const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
69
|
+
const msft = { symbol: "MSFT", exchange: "NASDAQ" };
|
|
70
|
+
let calls = 0;
|
|
71
|
+
|
|
72
|
+
coordinator.subscribeKeys([buildQuoteKey(aapl), buildQuoteKey(msft)], () => { calls += 1; });
|
|
73
|
+
coordinator.subscribeQuotes([{ instrument: aapl }, { instrument: msft }]);
|
|
74
|
+
|
|
75
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 100));
|
|
76
|
+
emitQuote({ symbol: "MSFT", exchange: "NASDAQ" }, quote("MSFT", 200));
|
|
77
|
+
await flushCoordinator();
|
|
78
|
+
|
|
79
|
+
expect(calls).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("coalesces global notifications per microtask", async () => {
|
|
83
|
+
const { provider, emitQuote } = createProvider();
|
|
84
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
85
|
+
const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
86
|
+
const msft = { symbol: "MSFT", exchange: "NASDAQ" };
|
|
87
|
+
let calls = 0;
|
|
88
|
+
|
|
89
|
+
coordinator.subscribe(() => { calls += 1; });
|
|
90
|
+
coordinator.subscribeQuotes([{ instrument: aapl }, { instrument: msft }]);
|
|
91
|
+
|
|
92
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 100));
|
|
93
|
+
emitQuote({ symbol: "MSFT", exchange: "NASDAQ" }, quote("MSFT", 200));
|
|
94
|
+
await flushCoordinator();
|
|
95
|
+
|
|
96
|
+
expect(calls).toBe(1);
|
|
97
|
+
expect(coordinator.getVersion()).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ignores repeated stream quotes that only refresh timestamp noise", async () => {
|
|
101
|
+
const { provider, emitQuote } = createProvider();
|
|
102
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
103
|
+
const aapl = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
104
|
+
let calls = 0;
|
|
105
|
+
|
|
106
|
+
coordinator.subscribeKeys([buildQuoteKey(aapl)], () => { calls += 1; });
|
|
107
|
+
coordinator.subscribeQuotes([{ instrument: aapl }]);
|
|
108
|
+
|
|
109
|
+
const firstTimestamp = 1_700_000_000_000;
|
|
110
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 100, { lastUpdated: firstTimestamp }));
|
|
111
|
+
await flushCoordinator();
|
|
112
|
+
|
|
113
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 100, { lastUpdated: firstTimestamp + 10_000 }));
|
|
114
|
+
await flushCoordinator();
|
|
115
|
+
|
|
116
|
+
expect(calls).toBe(1);
|
|
117
|
+
expect(coordinator.getQuoteEntry(aapl).data?.lastUpdated).toBe(firstTimestamp);
|
|
118
|
+
|
|
119
|
+
emitQuote({ symbol: "AAPL", exchange: "NASDAQ" }, quote("AAPL", 101, { lastUpdated: firstTimestamp + 20_000 }));
|
|
120
|
+
await flushCoordinator();
|
|
121
|
+
|
|
122
|
+
expect(calls).toBe(2);
|
|
123
|
+
expect(coordinator.getQuoteEntry(aapl).data?.price).toBe(101);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -87,11 +87,80 @@ describe("MarketDataCoordinator", () => {
|
|
|
87
87
|
const first = await coordinator.loadChart(request);
|
|
88
88
|
expect(first.data?.length).toBe(2);
|
|
89
89
|
|
|
90
|
-
const second = await coordinator.loadChart(request);
|
|
90
|
+
const second = await coordinator.loadChart(request, { forceRefresh: true });
|
|
91
91
|
expect(second.data).toBeNull();
|
|
92
92
|
expect(second.lastGoodData?.length).toBe(2);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it("reuses fresh snapshots instead of refetching on every pane rerender", async () => {
|
|
96
|
+
let calls = 0;
|
|
97
|
+
const provider = createProvider({
|
|
98
|
+
getTickerFinancials: async () => {
|
|
99
|
+
calls += 1;
|
|
100
|
+
return {
|
|
101
|
+
quote: {
|
|
102
|
+
symbol: "AAPL",
|
|
103
|
+
price: 100 + calls,
|
|
104
|
+
currency: "USD",
|
|
105
|
+
change: 1,
|
|
106
|
+
changePercent: 1,
|
|
107
|
+
lastUpdated: Date.now(),
|
|
108
|
+
},
|
|
109
|
+
fundamentals: { marketCap: calls },
|
|
110
|
+
profile: null,
|
|
111
|
+
annualStatements: [],
|
|
112
|
+
quarterlyStatements: [],
|
|
113
|
+
priceHistory: [],
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
118
|
+
const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
119
|
+
|
|
120
|
+
const first = await coordinator.loadSnapshot(instrument);
|
|
121
|
+
const second = await coordinator.loadSnapshot(instrument);
|
|
122
|
+
const refreshed = await coordinator.loadSnapshot(instrument, { forceRefresh: true });
|
|
123
|
+
|
|
124
|
+
expect(first.data?.quote?.price).toBe(101);
|
|
125
|
+
expect(second.data?.quote?.price).toBe(101);
|
|
126
|
+
expect(refreshed.data?.quote?.price).toBe(102);
|
|
127
|
+
expect(calls).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("reuses fresh empty tab query results instead of refetching on reopen", async () => {
|
|
131
|
+
let newsCalls = 0;
|
|
132
|
+
let optionsCalls = 0;
|
|
133
|
+
const provider = createProvider({
|
|
134
|
+
getNews: async () => {
|
|
135
|
+
newsCalls += 1;
|
|
136
|
+
return [];
|
|
137
|
+
},
|
|
138
|
+
getOptionsChain: async () => {
|
|
139
|
+
optionsCalls += 1;
|
|
140
|
+
return {
|
|
141
|
+
underlyingSymbol: "AAPL",
|
|
142
|
+
expirationDates: [],
|
|
143
|
+
calls: [],
|
|
144
|
+
puts: [],
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
149
|
+
const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
150
|
+
|
|
151
|
+
const firstNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
152
|
+
const secondNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
153
|
+
const firstOptions = await coordinator.loadOptions({ instrument });
|
|
154
|
+
const secondOptions = await coordinator.loadOptions({ instrument });
|
|
155
|
+
|
|
156
|
+
expect(firstNews.error?.reasonCode).toBe("NO_DATA");
|
|
157
|
+
expect(secondNews.error?.reasonCode).toBe("NO_DATA");
|
|
158
|
+
expect(firstOptions.error?.reasonCode).toBe("NO_DATA");
|
|
159
|
+
expect(secondOptions.error?.reasonCode).toBe("NO_DATA");
|
|
160
|
+
expect(newsCalls).toBe(1);
|
|
161
|
+
expect(optionsCalls).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
95
164
|
it("uses cached chart data while a wider range request is loading", async () => {
|
|
96
165
|
const oneYearHistory = [
|
|
97
166
|
{ date: new Date("2024-01-01"), close: 100 },
|
|
@@ -346,7 +415,7 @@ describe("MarketDataCoordinator", () => {
|
|
|
346
415
|
change: -0.021,
|
|
347
416
|
changePercent: -7.89,
|
|
348
417
|
lastUpdated: Date.now() - 1000,
|
|
349
|
-
dataSource: "
|
|
418
|
+
dataSource: "delayed",
|
|
350
419
|
},
|
|
351
420
|
annualStatements: [],
|
|
352
421
|
quarterlyStatements: [],
|
|
@@ -385,7 +454,7 @@ describe("MarketDataCoordinator", () => {
|
|
|
385
454
|
change: -0.021,
|
|
386
455
|
changePercent: -7.89,
|
|
387
456
|
lastUpdated: Date.now() - 1000,
|
|
388
|
-
dataSource: "
|
|
457
|
+
dataSource: "delayed",
|
|
389
458
|
},
|
|
390
459
|
annualStatements: [],
|
|
391
460
|
quarterlyStatements: [],
|
|
@@ -432,7 +501,7 @@ describe("MarketDataCoordinator", () => {
|
|
|
432
501
|
quote: {
|
|
433
502
|
symbol: "HY9H",
|
|
434
503
|
providerId: "yahoo",
|
|
435
|
-
dataSource: "
|
|
504
|
+
dataSource: "delayed",
|
|
436
505
|
price: 598,
|
|
437
506
|
currency: "EUR",
|
|
438
507
|
change: 6,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { DataProvider,
|
|
1
|
+
import type { DataProvider, SecFilingItem } from "../types/data-provider";
|
|
2
|
+
import type { NewsArticle } from "../news/types";
|
|
2
3
|
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
3
4
|
import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "./request-types";
|
|
4
5
|
import { QueryStore } from "./query-store";
|
|
@@ -26,10 +27,19 @@ import { traceMarketData } from "./trace";
|
|
|
26
27
|
import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../utils/price-history";
|
|
27
28
|
import { hasFreshQuoteForCurrentSession, isQuoteStaleForCurrentSession } from "../utils/quote-freshness";
|
|
28
29
|
import { TIME_RANGE_ORDER } from "../components/chart/chart-resolution";
|
|
30
|
+
import { measurePerf } from "../utils/perf-marks";
|
|
29
31
|
|
|
30
32
|
const EMPTY_MESSAGE = "No data available";
|
|
31
33
|
const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
|
|
32
34
|
const TIME_RANGE_INDEX = new Map(TIME_RANGE_ORDER.map((range, index) => [range, index]));
|
|
35
|
+
const SNAPSHOT_CACHE_TTL_MS = 5 * 60_000;
|
|
36
|
+
const CHART_CACHE_TTL_MS = 10 * 60_000;
|
|
37
|
+
const NEWS_CACHE_TTL_MS = 2 * 60_000;
|
|
38
|
+
const OPTIONS_CACHE_TTL_MS = 10 * 60_000;
|
|
39
|
+
const SEC_FILINGS_CACHE_TTL_MS = 10 * 60_000;
|
|
40
|
+
const SEC_CONTENT_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
41
|
+
const ARTICLE_SUMMARY_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
42
|
+
const FX_CACHE_TTL_MS = 30 * 60_000;
|
|
33
43
|
|
|
34
44
|
function createBaselineChartRequest(instrument: InstrumentRef): ChartRequest {
|
|
35
45
|
return {
|
|
@@ -69,6 +79,15 @@ function classifyError(error: unknown): { reasonCode: ProviderReasonCode; messag
|
|
|
69
79
|
return { reasonCode: "UPSTREAM_ERROR", message };
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
function hasFreshEntryData<T>(entry: QueryEntry<T>, ttlMs: number, now = Date.now()): boolean {
|
|
83
|
+
if (resolveEntryData(entry) == null) return false;
|
|
84
|
+
return entry.fetchedAt != null && now - entry.fetchedAt < ttlMs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasFreshReadyEntry<T>(entry: QueryEntry<T>, ttlMs: number, now = Date.now()): boolean {
|
|
88
|
+
return entry.phase === "ready" && entry.fetchedAt != null && now - entry.fetchedAt < ttlMs;
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
function createAttempt(
|
|
73
92
|
providerId: string,
|
|
74
93
|
startedAt: number,
|
|
@@ -132,6 +151,57 @@ function readyQuoteEntry(
|
|
|
132
151
|
return readyEntry(current, quote, source, attempts, { keepLastGoodOnEmpty: true });
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
const STREAM_QUOTE_FIELDS: Array<keyof Quote> = [
|
|
155
|
+
"symbol",
|
|
156
|
+
"providerId",
|
|
157
|
+
"price",
|
|
158
|
+
"currency",
|
|
159
|
+
"change",
|
|
160
|
+
"changePercent",
|
|
161
|
+
"previousClose",
|
|
162
|
+
"high52w",
|
|
163
|
+
"low52w",
|
|
164
|
+
"marketCap",
|
|
165
|
+
"volume",
|
|
166
|
+
"name",
|
|
167
|
+
"exchangeName",
|
|
168
|
+
"fullExchangeName",
|
|
169
|
+
"listingExchangeName",
|
|
170
|
+
"listingExchangeFullName",
|
|
171
|
+
"routingExchangeName",
|
|
172
|
+
"routingExchangeFullName",
|
|
173
|
+
"marketState",
|
|
174
|
+
"sessionConfidence",
|
|
175
|
+
"preMarketPrice",
|
|
176
|
+
"preMarketChange",
|
|
177
|
+
"preMarketChangePercent",
|
|
178
|
+
"postMarketPrice",
|
|
179
|
+
"postMarketChange",
|
|
180
|
+
"postMarketChangePercent",
|
|
181
|
+
"bid",
|
|
182
|
+
"ask",
|
|
183
|
+
"bidSize",
|
|
184
|
+
"askSize",
|
|
185
|
+
"open",
|
|
186
|
+
"high",
|
|
187
|
+
"low",
|
|
188
|
+
"mark",
|
|
189
|
+
"dataSource",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
function quoteTimestampMinute(quote: Quote): number | null {
|
|
193
|
+
return Number.isFinite(quote.lastUpdated) ? Math.floor(quote.lastUpdated / 60_000) : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function areStreamQuotesEquivalent(current: Quote | null | undefined, next: Quote): boolean {
|
|
197
|
+
if (!current) return false;
|
|
198
|
+
for (const field of STREAM_QUOTE_FIELDS) {
|
|
199
|
+
if (current[field] !== next[field]) return false;
|
|
200
|
+
}
|
|
201
|
+
return quoteTimestampMinute(current) === quoteTimestampMinute(next)
|
|
202
|
+
&& JSON.stringify(current.provenance ?? null) === JSON.stringify(next.provenance ?? null);
|
|
203
|
+
}
|
|
204
|
+
|
|
135
205
|
function errorEntry<T>(current: QueryEntry<T>, attempt: ProviderAttempt): QueryEntry<T> {
|
|
136
206
|
return {
|
|
137
207
|
...current,
|
|
@@ -147,28 +217,55 @@ function errorEntry<T>(current: QueryEntry<T>, attempt: ProviderAttempt): QueryE
|
|
|
147
217
|
|
|
148
218
|
export class MarketDataCoordinator {
|
|
149
219
|
private version = 0;
|
|
220
|
+
private pendingVersionBump = false;
|
|
221
|
+
private pendingChangedKeys = new Set<string>();
|
|
150
222
|
private readonly listeners = new Set<() => void>();
|
|
223
|
+
private readonly keyListeners = new Map<string, Set<() => void>>();
|
|
224
|
+
private readonly keyVersions = new Map<string, number>();
|
|
151
225
|
private readonly inFlight = new Map<string, Promise<unknown>>();
|
|
152
226
|
private readonly chartRequests = new Map<string, ChartRequest>();
|
|
153
227
|
|
|
154
|
-
private readonly quoteStore = new QueryStore<Quote>(() => this.bump());
|
|
155
|
-
private readonly snapshotStore = new QueryStore<TickerFinancials>(() => this.bump());
|
|
156
|
-
private readonly profileStore = new QueryStore<TickerFinancials["profile"]>(() => this.bump());
|
|
157
|
-
private readonly fundamentalsStore = new QueryStore<TickerFinancials["fundamentals"]>(() => this.bump());
|
|
158
|
-
private readonly statementsStore = new QueryStore<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>(() => this.bump());
|
|
159
|
-
private readonly chartStore = new QueryStore<PricePoint[]>(() => this.bump());
|
|
160
|
-
private readonly newsStore = new QueryStore<
|
|
161
|
-
private readonly optionsStore = new QueryStore<OptionsChain>(() => this.bump());
|
|
162
|
-
private readonly secFilingsStore = new QueryStore<SecFilingItem[]>(() => this.bump());
|
|
163
|
-
private readonly secContentStore = new QueryStore<string | null>(() => this.bump());
|
|
164
|
-
private readonly articleSummaryStore = new QueryStore<string | null>(() => this.bump());
|
|
165
|
-
private readonly fxStore = new QueryStore<number>(() => this.bump());
|
|
228
|
+
private readonly quoteStore = new QueryStore<Quote>((key) => this.bump(key));
|
|
229
|
+
private readonly snapshotStore = new QueryStore<TickerFinancials>((key) => this.bump(key));
|
|
230
|
+
private readonly profileStore = new QueryStore<TickerFinancials["profile"]>((key) => this.bump(key));
|
|
231
|
+
private readonly fundamentalsStore = new QueryStore<TickerFinancials["fundamentals"]>((key) => this.bump(key));
|
|
232
|
+
private readonly statementsStore = new QueryStore<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>((key) => this.bump(key));
|
|
233
|
+
private readonly chartStore = new QueryStore<PricePoint[]>((key) => this.bump(key));
|
|
234
|
+
private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.bump(key));
|
|
235
|
+
private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.bump(key));
|
|
236
|
+
private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.bump(key));
|
|
237
|
+
private readonly secContentStore = new QueryStore<string | null>((key) => this.bump(key));
|
|
238
|
+
private readonly articleSummaryStore = new QueryStore<string | null>((key) => this.bump(key));
|
|
239
|
+
private readonly fxStore = new QueryStore<number>((key) => this.bump(key));
|
|
166
240
|
|
|
167
241
|
constructor(private readonly dataProvider: DataProvider) {}
|
|
168
242
|
|
|
169
|
-
private bump(): void {
|
|
170
|
-
this.
|
|
171
|
-
|
|
243
|
+
private bump(changeKey?: string): void {
|
|
244
|
+
if (changeKey) this.pendingChangedKeys.add(changeKey);
|
|
245
|
+
if (this.pendingVersionBump) return;
|
|
246
|
+
this.pendingVersionBump = true;
|
|
247
|
+
queueMicrotask(() => this.flushBump());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private flushBump(): void {
|
|
251
|
+
this.pendingVersionBump = false;
|
|
252
|
+
const changedKeys = this.pendingChangedKeys;
|
|
253
|
+
this.pendingChangedKeys = new Set();
|
|
254
|
+
measurePerf("market-data.bump", () => {
|
|
255
|
+
this.version += 1;
|
|
256
|
+
for (const key of changedKeys) {
|
|
257
|
+
this.keyVersions.set(key, (this.keyVersions.get(key) ?? 0) + 1);
|
|
258
|
+
}
|
|
259
|
+
const keyListeners = new Set<() => void>();
|
|
260
|
+
for (const key of changedKeys) {
|
|
261
|
+
for (const listener of this.keyListeners.get(key) ?? []) {
|
|
262
|
+
keyListeners.add(listener);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const listener of this.listeners) listener();
|
|
267
|
+
for (const listener of keyListeners) listener();
|
|
268
|
+
}, { changedKeyCount: changedKeys.size });
|
|
172
269
|
}
|
|
173
270
|
|
|
174
271
|
subscribe(listener: () => void): () => void {
|
|
@@ -178,10 +275,35 @@ export class MarketDataCoordinator {
|
|
|
178
275
|
};
|
|
179
276
|
}
|
|
180
277
|
|
|
278
|
+
subscribeKeys(keys: readonly string[], listener: () => void): () => void {
|
|
279
|
+
const uniqueKeys = [...new Set(keys)];
|
|
280
|
+
for (const key of uniqueKeys) {
|
|
281
|
+
if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set());
|
|
282
|
+
this.keyListeners.get(key)!.add(listener);
|
|
283
|
+
}
|
|
284
|
+
return () => {
|
|
285
|
+
for (const key of uniqueKeys) {
|
|
286
|
+
const listeners = this.keyListeners.get(key);
|
|
287
|
+
listeners?.delete(listener);
|
|
288
|
+
if (listeners?.size === 0) {
|
|
289
|
+
this.keyListeners.delete(key);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
181
295
|
getVersion(): number {
|
|
182
296
|
return this.version;
|
|
183
297
|
}
|
|
184
298
|
|
|
299
|
+
getKeysVersion(keys: readonly string[]): number {
|
|
300
|
+
let version = 0;
|
|
301
|
+
for (const key of new Set(keys)) {
|
|
302
|
+
version += this.keyVersions.get(key) ?? 0;
|
|
303
|
+
}
|
|
304
|
+
return version;
|
|
305
|
+
}
|
|
306
|
+
|
|
185
307
|
getQuoteEntry(instrument: InstrumentRef): QueryEntry<Quote> {
|
|
186
308
|
return this.quoteStore.get(buildQuoteKey(instrument));
|
|
187
309
|
}
|
|
@@ -194,7 +316,7 @@ export class MarketDataCoordinator {
|
|
|
194
316
|
return this.chartStore.get(buildChartKey(request));
|
|
195
317
|
}
|
|
196
318
|
|
|
197
|
-
getNewsEntry(request: NewsRequest): QueryEntry<
|
|
319
|
+
getNewsEntry(request: NewsRequest): QueryEntry<NewsArticle[]> {
|
|
198
320
|
return this.newsStore.get(buildNewsKey(request));
|
|
199
321
|
}
|
|
200
322
|
|
|
@@ -354,6 +476,10 @@ export class MarketDataCoordinator {
|
|
|
354
476
|
options: { forceRefresh?: boolean } = {},
|
|
355
477
|
): Promise<QueryEntry<TickerFinancials>> {
|
|
356
478
|
const key = buildSnapshotKey(instrument);
|
|
479
|
+
const current = this.snapshotStore.get(key);
|
|
480
|
+
if (!options.forceRefresh && hasFreshEntryData(current, SNAPSHOT_CACHE_TTL_MS)) {
|
|
481
|
+
return current;
|
|
482
|
+
}
|
|
357
483
|
const flightKey = options.forceRefresh ? `${key}|refresh` : key;
|
|
358
484
|
return this.runSingleFlight(flightKey, async () => {
|
|
359
485
|
this.snapshotStore.update(key, loadingEntry);
|
|
@@ -448,6 +574,10 @@ export class MarketDataCoordinator {
|
|
|
448
574
|
): Promise<QueryEntry<PricePoint[]>> {
|
|
449
575
|
const key = buildChartKey(request);
|
|
450
576
|
this.chartRequests.set(key, request);
|
|
577
|
+
const current = this.chartStore.get(key);
|
|
578
|
+
if (!options.forceRefresh && hasFreshEntryData(current, CHART_CACHE_TTL_MS)) {
|
|
579
|
+
return current;
|
|
580
|
+
}
|
|
451
581
|
const flightKey = options.forceRefresh ? `${key}|refresh` : key;
|
|
452
582
|
return this.runSingleFlight(flightKey, async () => {
|
|
453
583
|
this.chartStore.update(key, (current) => this.createChartLoadingEntry(key, request, current));
|
|
@@ -498,18 +628,33 @@ export class MarketDataCoordinator {
|
|
|
498
628
|
});
|
|
499
629
|
}
|
|
500
630
|
|
|
501
|
-
async loadNews(request: NewsRequest): Promise<QueryEntry<
|
|
631
|
+
async loadNews(request: NewsRequest): Promise<QueryEntry<NewsArticle[]>> {
|
|
502
632
|
const key = buildNewsKey(request);
|
|
633
|
+
const current = this.newsStore.get(key);
|
|
634
|
+
if (hasFreshReadyEntry(current, NEWS_CACHE_TTL_MS)) {
|
|
635
|
+
return current;
|
|
636
|
+
}
|
|
503
637
|
return this.runSingleFlight(key, async () => {
|
|
504
638
|
this.newsStore.update(key, loadingEntry);
|
|
505
639
|
const startedAt = Date.now();
|
|
506
640
|
try {
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
641
|
+
const newsProvider = this.dataProvider as DataProvider & {
|
|
642
|
+
getNews?: (query: {
|
|
643
|
+
feed: "ticker";
|
|
644
|
+
ticker: string;
|
|
645
|
+
exchange?: string;
|
|
646
|
+
tickerTier: "primary";
|
|
647
|
+
limit?: number;
|
|
648
|
+
}) => Promise<NewsArticle[]>;
|
|
649
|
+
};
|
|
650
|
+
if (!newsProvider.getNews) throw new Error("No news provider available");
|
|
651
|
+
const data = await newsProvider.getNews({
|
|
652
|
+
feed: "ticker",
|
|
653
|
+
ticker: request.instrument.symbol,
|
|
654
|
+
exchange: request.instrument.exchange ?? "",
|
|
655
|
+
tickerTier: "primary",
|
|
656
|
+
limit: request.count ?? 50,
|
|
657
|
+
});
|
|
513
658
|
const attempts = [createAttempt(this.dataProvider.id, startedAt, data.length > 0 ? "success" : "empty", data.length === 0 ? "NO_DATA" : undefined)];
|
|
514
659
|
return this.newsStore.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, this.dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
|
|
515
660
|
} catch (error) {
|
|
@@ -522,6 +667,10 @@ export class MarketDataCoordinator {
|
|
|
522
667
|
|
|
523
668
|
async loadOptions(request: OptionsRequest): Promise<QueryEntry<OptionsChain>> {
|
|
524
669
|
const key = buildOptionsKey(request);
|
|
670
|
+
const current = this.optionsStore.get(key);
|
|
671
|
+
if (hasFreshReadyEntry(current, OPTIONS_CACHE_TTL_MS)) {
|
|
672
|
+
return current;
|
|
673
|
+
}
|
|
525
674
|
return this.runSingleFlight(key, async () => {
|
|
526
675
|
this.optionsStore.update(key, loadingEntry);
|
|
527
676
|
const startedAt = Date.now();
|
|
@@ -548,6 +697,10 @@ export class MarketDataCoordinator {
|
|
|
548
697
|
|
|
549
698
|
async loadSecFilings(request: SecFilingsRequest): Promise<QueryEntry<SecFilingItem[]>> {
|
|
550
699
|
const key = buildSecFilingsKey(request);
|
|
700
|
+
const current = this.secFilingsStore.get(key);
|
|
701
|
+
if (hasFreshReadyEntry(current, SEC_FILINGS_CACHE_TTL_MS)) {
|
|
702
|
+
return current;
|
|
703
|
+
}
|
|
551
704
|
return this.runSingleFlight(key, async () => {
|
|
552
705
|
this.secFilingsStore.update(key, loadingEntry);
|
|
553
706
|
const startedAt = Date.now();
|
|
@@ -574,6 +727,10 @@ export class MarketDataCoordinator {
|
|
|
574
727
|
|
|
575
728
|
async loadSecFilingContent(filing: SecFilingItem): Promise<QueryEntry<string | null>> {
|
|
576
729
|
const key = buildSecContentKey(filing.accessionNumber);
|
|
730
|
+
const current = this.secContentStore.get(key);
|
|
731
|
+
if (hasFreshReadyEntry(current, SEC_CONTENT_CACHE_TTL_MS)) {
|
|
732
|
+
return current;
|
|
733
|
+
}
|
|
577
734
|
return this.runSingleFlight(key, async () => {
|
|
578
735
|
this.secContentStore.update(key, loadingEntry);
|
|
579
736
|
const startedAt = Date.now();
|
|
@@ -596,6 +753,10 @@ export class MarketDataCoordinator {
|
|
|
596
753
|
|
|
597
754
|
async loadArticleSummary(url: string): Promise<QueryEntry<string | null>> {
|
|
598
755
|
const key = buildArticleSummaryKey(url);
|
|
756
|
+
const current = this.articleSummaryStore.get(key);
|
|
757
|
+
if (hasFreshReadyEntry(current, ARTICLE_SUMMARY_CACHE_TTL_MS)) {
|
|
758
|
+
return current;
|
|
759
|
+
}
|
|
599
760
|
return this.runSingleFlight(key, async () => {
|
|
600
761
|
this.articleSummaryStore.update(key, loadingEntry);
|
|
601
762
|
const startedAt = Date.now();
|
|
@@ -614,6 +775,10 @@ export class MarketDataCoordinator {
|
|
|
614
775
|
|
|
615
776
|
async loadFxRate(currency: string): Promise<QueryEntry<number>> {
|
|
616
777
|
const key = buildFxKey(currency);
|
|
778
|
+
const current = this.fxStore.get(key);
|
|
779
|
+
if (hasFreshEntryData(current, FX_CACHE_TTL_MS)) {
|
|
780
|
+
return current;
|
|
781
|
+
}
|
|
617
782
|
return this.runSingleFlight(key, async () => {
|
|
618
783
|
this.fxStore.update(key, loadingEntry);
|
|
619
784
|
const startedAt = Date.now();
|
|
@@ -650,6 +815,7 @@ export class MarketDataCoordinator {
|
|
|
650
815
|
};
|
|
651
816
|
const key = buildQuoteKey(instrument);
|
|
652
817
|
const current = this.quoteStore.get(key);
|
|
818
|
+
if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, quote)) return;
|
|
653
819
|
const attempts = [createAttempt(quote.providerId ?? this.dataProvider.id, Date.now(), "success")];
|
|
654
820
|
this.quoteStore.set(key, readyQuoteEntry(current, quote, quote.providerId ?? this.dataProvider.id, attempts));
|
|
655
821
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useState } from "react";
|
|
3
|
-
import { testRender } from "
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
4
|
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
5
5
|
import type { TickerRecord } from "../types/ticker";
|
|
6
6
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "./coordinator";
|