gloomberb 0.11.0 → 0.11.2
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 +38 -15
- package/README.zh-CN.md +12 -12
- package/package.json +10 -3
- package/src/api-client/auth.ts +14 -10
- package/src/api-client/data.ts +54 -0
- package/src/api-client/index.test.ts +124 -0
- package/src/api-client/index.ts +139 -4
- package/src/api-client/paths.ts +89 -0
- package/src/api-client/request.ts +15 -2
- package/src/api-client/socket-health.test.ts +1 -0
- package/src/api-client/socket.ts +2 -1
- package/src/api-client/types.ts +163 -0
- package/src/app/pane-runtime/index.tsx +4 -1
- package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
- package/src/app/pane-runtime/plugin-bindings.ts +25 -4
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
- package/src/app/runtime/desktop-deeplink.test.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +66 -2
- package/src/app/runtime/desktop-menu.ts +3 -0
- package/src/app/runtime/plugin-bindings.ts +30 -28
- package/src/app/runtime/update.ts +8 -6
- package/src/app.tsx +21 -7
- package/src/architecture/import-boundaries.test.ts +5 -1
- package/src/capabilities/factories.ts +4 -0
- package/src/cli/commands/automation.test.ts +9 -0
- package/src/cli/commands/automation.ts +22 -73
- package/src/cli/index.ts +10 -6
- package/src/cli/registry.ts +1 -1
- package/src/components/chart/composite/composite-chart.test.tsx +45 -2
- package/src/components/chart/composite/composite-chart.tsx +16 -12
- package/src/components/chart/composite/format.test.ts +34 -0
- package/src/components/chart/composite/format.ts +33 -10
- package/src/components/chart/composite/scene.ts +0 -21
- package/src/components/chart/composite/tools.ts +49 -0
- package/src/components/chart/core/axis-format.ts +13 -3
- package/src/components/chart/core/index.test.ts +7 -0
- package/src/components/chart/core/renderer.ts +3 -4
- package/src/components/command-bar/commands/direct/index.ts +4 -0
- package/src/components/command-bar/commands/registry.ts +8 -1
- package/src/components/command-bar/layout-items/current-layout.ts +1 -1
- package/src/components/command-bar/pane-templates/workflow.ts +4 -0
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
- package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
- package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
- package/src/components/command-bar/surface/index.test.tsx +6 -6
- package/src/components/command-bar/surface/index.tsx +4 -1
- package/src/components/command-bar/surface/pane.test.tsx +66 -2
- package/src/components/command-bar/view-model.test.ts +2 -1
- package/src/components/command-bar/workflow/ops.test.ts +54 -0
- package/src/components/command-bar/workflow/ops.ts +1 -1
- package/src/components/company-logo.test.ts +19 -0
- package/src/components/company-logo.tsx +60 -0
- package/src/components/feed-data-table/stack-view.tsx +7 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +85 -15
- package/src/components/layout/header.tsx +5 -5
- package/src/components/layout/pane/title.ts +31 -3
- package/src/components/layout/shell/active-drag.ts +4 -11
- package/src/components/layout/shell/drag/index.ts +4 -20
- package/src/components/layout/shell/drag/runtime.ts +3 -4
- package/src/components/layout/shell/index.test.tsx +17 -3
- package/src/components/layout/shell/index.tsx +89 -31
- package/src/components/layout/shell/menu.test.ts +37 -1
- package/src/components/layout/shell/menu.ts +14 -0
- package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
- package/src/components/layout/shell/pane/actions.ts +0 -5
- package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
- package/src/components/layout/shell/shortcuts.ts +6 -3
- package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
- package/src/components/layout/shell/ticker-link-menu.ts +78 -0
- package/src/components/layout/status-bar.test.tsx +62 -61
- package/src/components/layout/status-bar.tsx +53 -75
- package/src/components/onboarding/account-step/form-panel.tsx +4 -2
- package/src/components/onboarding/account-step/index.tsx +1 -1
- package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
- package/src/components/onboarding/onboarding-wizard.tsx +13 -5
- package/src/components/onboarding/wizard-account.ts +4 -23
- package/src/components/table-view-shared.test.ts +11 -0
- package/src/components/table-view-shared.tsx +26 -0
- package/src/components/ui/data-table/index.tsx +19 -1
- package/src/components/ui/fields.tsx +5 -1
- package/src/core/state/app/layout-reducer.ts +36 -0
- package/src/core/state/app/layout.ts +10 -5
- package/src/core/state/app/state.ts +0 -13
- package/src/core/state/app/types.ts +6 -4
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/config/layout.ts +2 -0
- package/src/data/config/store/index.test.ts +27 -6
- package/src/data/config/store/normalize.ts +3 -5
- package/src/data/plugin-state-store.ts +11 -0
- package/src/data/sqlite/cache.test.ts +10 -0
- package/src/i18n/es.ts +13 -5
- package/src/i18n/ja.ts +13 -5
- package/src/i18n/ko.ts +13 -5
- package/src/i18n/zh-cn.ts +13 -5
- package/src/i18n/zh-tw.ts +13 -5
- package/src/layout-marketplace/api.test.ts +54 -0
- package/src/layout-marketplace/api.ts +65 -0
- package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
- package/src/layout-marketplace/gallery-desktop.tsx +375 -0
- package/src/layout-marketplace/gallery-terminal.tsx +376 -0
- package/src/layout-marketplace/gallery.test.tsx +84 -0
- package/src/layout-marketplace/gallery.tsx +323 -0
- package/src/layout-marketplace/geometry.test.ts +47 -0
- package/src/layout-marketplace/geometry.ts +80 -0
- package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
- package/src/layout-marketplace/mini-workspace.tsx +428 -0
- package/src/layout-marketplace/model.test.ts +118 -0
- package/src/layout-marketplace/model.ts +171 -0
- package/src/layout-marketplace/name-dialog.tsx +60 -0
- package/src/layout-marketplace/pane-imagery.ts +85 -0
- package/src/layout-marketplace/pane.tsx +20 -0
- package/src/layout-marketplace/payload.test.ts +246 -0
- package/src/layout-marketplace/payload.ts +624 -0
- package/src/layout-marketplace/use-marketplace.ts +63 -0
- package/src/market-data/coordinator/index.test.ts +2 -2
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/hooks.tsx +0 -41
- package/src/market-data/market/format.test.ts +7 -0
- package/src/market-data/market/freshness.ts +5 -2
- package/src/market-data/quotes/freshness.test.ts +16 -0
- package/src/news/aggregator.test.ts +92 -3
- package/src/news/aggregator.ts +98 -18
- package/src/news/hooks.test.tsx +2 -0
- package/src/news/hooks.ts +27 -1
- package/src/news/news-model.ts +5 -2
- package/src/news/types.ts +7 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
- package/src/plugins/broker-sync/normalize.ts +42 -5
- package/src/plugins/builtin/account-management/pane.tsx +5 -5
- package/src/plugins/builtin/ai/index.tsx +18 -2
- package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
- package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
- package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
- package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
- package/src/plugins/builtin/alerts/index.test.tsx +49 -6
- package/src/plugins/builtin/alerts/index.tsx +11 -6
- package/src/plugins/builtin/alerts/pane.tsx +51 -9
- package/src/plugins/builtin/alerts/quotes.ts +11 -3
- package/src/plugins/builtin/alerts/types.ts +1 -0
- package/src/plugins/builtin/analytics/index.tsx +11 -1
- package/src/plugins/builtin/analytics/pane-model.ts +6 -4
- package/src/plugins/builtin/broker-manager/index.tsx +3 -0
- package/src/plugins/builtin/buildout/model/index.ts +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
- package/src/plugins/builtin/cds/client.test.ts +83 -0
- package/src/plugins/builtin/cds/client.ts +67 -0
- package/src/plugins/builtin/cds/index.tsx +41 -0
- package/src/plugins/builtin/cds/model.test.ts +232 -0
- package/src/plugins/builtin/cds/model.ts +401 -0
- package/src/plugins/builtin/cds/pane.test.tsx +132 -0
- package/src/plugins/builtin/cds/pane.tsx +358 -0
- package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
- package/src/plugins/builtin/chart-composer/index.tsx +88 -2
- package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
- package/src/plugins/builtin/chat/content/index.tsx +4 -1
- package/src/plugins/builtin/chat/controller/channels.ts +7 -1
- package/src/plugins/builtin/chat/controller/index.ts +14 -9
- package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
- package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
- package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
- package/src/plugins/builtin/chat/controller/send.ts +1 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
- package/src/plugins/builtin/chat/controller/state.ts +1 -3
- package/src/plugins/builtin/chat/controller/storage.ts +0 -2
- package/src/plugins/builtin/chat/controller/utils.ts +4 -6
- package/src/plugins/builtin/chat/controller/view.ts +2 -2
- package/src/plugins/builtin/chat/controller.test.ts +142 -14
- package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
- package/src/plugins/builtin/chat/profile-popover.ts +53 -22
- package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
- package/src/plugins/builtin/chat/sidebar.tsx +63 -0
- package/src/plugins/builtin/chat/test-harness.tsx +29 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
- package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
- package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
- package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
- package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
- package/src/plugins/builtin/cloud/browser.tsx +9 -0
- package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
- package/src/plugins/builtin/cloud/device-signin.ts +2 -8
- package/src/plugins/builtin/cloud/index.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +14 -24
- package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
- package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
- package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
- package/src/plugins/builtin/composite-plugins.ts +3 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
- package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
- package/src/plugins/builtin/congress-trades/model.ts +69 -1
- package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
- package/src/plugins/builtin/connections/index.ts +3 -0
- package/src/plugins/builtin/dividend-yield/client.ts +18 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
- package/src/plugins/builtin/earnings/index.tsx +5 -3
- package/src/plugins/builtin/earnings/model.test.ts +19 -1
- package/src/plugins/builtin/earnings/model.ts +17 -15
- package/src/plugins/builtin/econ/calendar-model.ts +0 -5
- package/src/plugins/builtin/econ/index.test.tsx +6 -1
- package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
- package/src/plugins/builtin/insider/index.tsx +27 -11
- package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
- package/src/plugins/builtin/layout-manager/index.tsx +17 -4
- package/src/plugins/builtin/news/index.tsx +11 -6
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
- package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
- package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
- package/src/plugins/builtin/news/wire/index.tsx +3 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
- package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
- package/src/plugins/builtin/notes/files.ts +80 -0
- package/src/plugins/builtin/notes/index.tsx +3 -0
- package/src/plugins/builtin/notes/sync.test.ts +49 -0
- package/src/plugins/builtin/notes/sync.ts +98 -0
- package/src/plugins/builtin/options/index.tsx +1 -0
- package/src/plugins/builtin/options/view.test.tsx +25 -3
- package/src/plugins/builtin/options/view.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
- package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
- package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
- package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
- package/src/plugins/builtin/research/index.test.ts +14 -0
- package/src/plugins/builtin/research/index.tsx +29 -0
- package/src/plugins/builtin/sec/index.tsx +24 -9
- package/src/plugins/builtin/sectors/index.tsx +14 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
- package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
- package/src/plugins/builtin/shared/public-pane.ts +9 -0
- package/src/plugins/builtin/shared/public-share.ts +23 -0
- package/src/plugins/builtin/shared/ticker-source.ts +32 -0
- package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
- package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
- package/src/plugins/builtin/substack/api/cache.ts +7 -3
- package/src/plugins/builtin/substack/api/loaders.ts +36 -12
- package/src/plugins/builtin/substack/api/types.ts +2 -0
- package/src/plugins/builtin/substack/pane-state.ts +1 -1
- package/src/plugins/builtin/substack/pane.tsx +55 -12
- package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +40 -18
- package/src/plugins/builtin/thirteenf/data.ts +15 -7
- package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
- package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
- package/src/plugins/builtin/world-indices/index.tsx +2 -0
- package/src/plugins/catalog-browser.test.ts +78 -0
- package/src/plugins/catalog-browser.ts +136 -0
- package/src/plugins/gridlock-notification.ts +2 -1
- package/src/plugins/ibkr/flex/index.test.ts +19 -0
- package/src/plugins/ibkr/flex/index.ts +1 -1
- package/src/plugins/pane-manager/gridlock.ts +87 -3
- package/src/plugins/pane-manager.test.ts +30 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +2 -2
- package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
- package/src/plugins/prediction-markets/controller/data.ts +7 -1
- package/src/plugins/prediction-markets/controller/index.ts +9 -2
- package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
- package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
- package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
- package/src/plugins/prediction-markets/detail.test.tsx +29 -2
- package/src/plugins/prediction-markets/index.tsx +8 -4
- package/src/plugins/prediction-markets/navigation.ts +0 -1
- package/src/plugins/prediction-markets/pane.test.tsx +77 -0
- package/src/plugins/prediction-markets/pane.tsx +12 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
- package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
- package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
- package/src/plugins/registry/index.ts +2 -0
- package/src/plugins/ticker-navigation.test.ts +24 -0
- package/src/plugins/ticker-navigation.ts +40 -0
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/app-services.ts +61 -0
- package/src/renderers/browser/cloud-transport.test.ts +68 -0
- package/src/renderers/browser/cloud-transport.ts +48 -0
- package/src/renderers/browser/config-host.ts +58 -0
- package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
- package/src/renderers/browser/deeplink-bridge.ts +22 -0
- package/src/renderers/browser/error-boundary.tsx +23 -0
- package/src/renderers/browser/main.tsx +64 -0
- package/src/renderers/browser/persistence.ts +74 -0
- package/src/renderers/browser/storage.test.ts +66 -0
- package/src/renderers/browser/storage.ts +50 -0
- package/src/renderers/browser/ticker-repository.ts +59 -0
- package/src/renderers/browser/ui-host.tsx +40 -0
- package/src/renderers/cloudflare/worker.test.ts +136 -0
- package/src/renderers/cloudflare/worker.ts +116 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +14 -3
- package/src/renderers/electrobun/view/build-assets.ts +12 -10
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
- package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
- package/src/renderers/electrobun/view/host/box.tsx +11 -6
- package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
- package/src/renderers/electrobun/view/host/input.tsx +11 -7
- package/src/renderers/electrobun/view/host/native.ts +0 -5
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
- package/src/renderers/electrobun/view/styles.css +107 -7
- package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
- package/src/renderers/electrobun/view/toast-host.tsx +69 -43
- package/src/renderers/electrobun/view/ui-host.tsx +6 -137
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/renderers/share/layout-view.test.tsx +58 -0
- package/src/renderers/share/layout-view.tsx +133 -0
- package/src/renderers/share/main.tsx +102 -0
- package/src/renderers/share/styles.css +54 -0
- package/src/renderers/share/view.test.tsx +100 -0
- package/src/renderers/share/view.tsx +150 -0
- package/src/shares/api.test.ts +105 -0
- package/src/shares/api.ts +127 -0
- package/src/shares/chart-snapshot.test.ts +34 -0
- package/src/shares/chart-snapshot.ts +28 -0
- package/src/shares/location.test.ts +19 -0
- package/src/shares/location.ts +11 -0
- package/src/shares/pane.test.ts +110 -0
- package/src/shares/pane.ts +54 -0
- package/src/shares/payload.ts +177 -0
- package/src/sources/gloomberb-cloud/index.test.ts +61 -1
- package/src/sources/gloomberb-cloud/index.ts +86 -14
- package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
- package/src/sources/provider-router/history.test.ts +2 -2
- package/src/sources/provider-router/news.test.ts +34 -0
- package/src/sources/provider-router/news.ts +15 -1
- package/src/sources/sec-edgar.test.ts +59 -0
- package/src/sources/sec-edgar.ts +63 -23
- package/src/sources/yahoo-finance/mappers.ts +3 -0
- package/src/sources/yahoo-finance/snapshots.ts +10 -2
- package/src/sources/yahoo-finance/symbols.test.ts +10 -0
- package/src/sources/yahoo-finance/symbols.ts +13 -8
- package/src/sources/yahoo-finance.test.ts +1 -1
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/app/bootstrap.ts +6 -7
- package/src/state/app/context/index.test.ts +41 -1
- package/src/state/app/context/index.test.tsx +34 -16
- package/src/state/selectors-ui.ts +0 -8
- package/src/sync/baseline.ts +32 -0
- package/src/sync/controller.test.ts +124 -1
- package/src/sync/controller.ts +54 -12
- package/src/sync/core-contributors.test.ts +44 -0
- package/src/sync/core-contributors.ts +59 -10
- package/src/sync/react.ts +22 -2
- package/src/sync/types.ts +12 -0
- package/src/theme/font-scale.ts +0 -11
- package/src/tickers/search/index.test.ts +27 -0
- package/src/tickers/search/upsert.ts +10 -0
- package/src/types/capability-route-source.ts +2 -1
- package/src/types/config.ts +35 -5
- package/src/types/desktop-menu.ts +1 -0
- package/src/types/plugin.ts +36 -0
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/host.tsx +4 -0
- package/src/ui/toast.tsx +2 -0
- package/src/utils/back-navigation.ts +0 -4
- package/src/utils/price-history.ts +11 -10
- package/src/utils/sec.test.ts +6 -0
- package/src/utils/sec.ts +1 -1
|
@@ -386,7 +386,7 @@ export class MarketDataCoordinator {
|
|
|
386
386
|
&& typeof quote.lastUpdated === "number"
|
|
387
387
|
&& quote.lastUpdated < snapshot.quote.lastUpdated
|
|
388
388
|
) {
|
|
389
|
-
return quote;
|
|
389
|
+
return snapshot.quote;
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
const cachedFinancials = snapshot ?? this.readCachedFinancialsForInstrument(instrument);
|
|
@@ -213,47 +213,6 @@ export function useQuoteEntries(
|
|
|
213
213
|
}, [coordinator, instrumentKey, keysVersion]);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
export function useChartQuery(
|
|
217
|
-
request: ChartRequest | null | undefined,
|
|
218
|
-
options: ChartQueryOptions = {},
|
|
219
|
-
): QueryEntry<PricePoint[]> | null {
|
|
220
|
-
const key = request ? buildChartKey(request) : null;
|
|
221
|
-
useCoordinatorKeysVersion(key ? [key] : []);
|
|
222
|
-
const coordinator = getSharedMarketDataCoordinator();
|
|
223
|
-
const entry = coordinator && request ? coordinator.getChartEntry(request) : null;
|
|
224
|
-
const appActive = useAppActive();
|
|
225
|
-
const wasActiveRef = useRef(appActive);
|
|
226
|
-
const refreshIntervalMs = Math.max(0, options.refreshIntervalMs ?? 0);
|
|
227
|
-
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
const coordinator = getSharedMarketDataCoordinator();
|
|
230
|
-
if (!coordinator || !request) {
|
|
231
|
-
wasActiveRef.current = appActive;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (!appActive) {
|
|
235
|
-
wasActiveRef.current = false;
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const forceRefresh = refreshIntervalMs > 0 && !wasActiveRef.current;
|
|
240
|
-
wasActiveRef.current = true;
|
|
241
|
-
void coordinator.loadChart(request, { forceRefresh }).catch(() => {});
|
|
242
|
-
}, [appActive, key, refreshIntervalMs]);
|
|
243
|
-
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
const coordinator = getSharedMarketDataCoordinator();
|
|
246
|
-
if (!coordinator || !request || !appActive || refreshIntervalMs <= 0) return;
|
|
247
|
-
|
|
248
|
-
const interval = setInterval(() => {
|
|
249
|
-
void coordinator.loadChart(request, { forceRefresh: true }).catch(() => {});
|
|
250
|
-
}, refreshIntervalMs);
|
|
251
|
-
return () => clearInterval(interval);
|
|
252
|
-
}, [appActive, key, refreshIntervalMs]);
|
|
253
|
-
|
|
254
|
-
return entry;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
216
|
export function useChartQueries(
|
|
258
217
|
requests: readonly ChartRequest[],
|
|
259
218
|
options: ChartQueryOptions = {},
|
|
@@ -100,6 +100,13 @@ describe("formatMarketPriceWithCurrency", () => {
|
|
|
100
100
|
test("preserves chart-style compact notation for large values", () => {
|
|
101
101
|
expect(formatCompactMarketPriceWithCurrency(21_970, "JPY")).toBe("¥22.0K");
|
|
102
102
|
expect(formatCompactMarketPriceWithCurrency(12_340, "HKD")).toBe("HK$12.3K");
|
|
103
|
+
expect(formatCompactMarketPriceWithCurrency(79_432.18, "USD", { assetCategory: "CRYPTO" })).toBe("$79.4K");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("keeps full price precision for legend-style values", () => {
|
|
107
|
+
expect(formatMarketPriceWithCurrency(79_432.18, "USD", { assetCategory: "CRYPTO" })).toBe("$79,432.18");
|
|
108
|
+
expect(formatMarketPriceWithCurrency(79_432.18, "USD", { assetCategory: "STK" })).toBe("$79,432.18");
|
|
109
|
+
expect(formatMarketPriceWithCurrency(1.084567, "USD", { isCashBalance: true })).toBe("$1.084567");
|
|
103
110
|
});
|
|
104
111
|
|
|
105
112
|
test("formats position cost values with tighter equity precision", () => {
|
|
@@ -5,6 +5,7 @@ const US_EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "
|
|
|
5
5
|
const ALWAYS_OPEN_EXCHANGES = new Set(["CCC"]);
|
|
6
6
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
7
7
|
const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
|
|
8
|
+
const ALWAYS_OPEN_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
8
9
|
const REGULAR_OPEN_MINUTES: Record<string, number> = {
|
|
9
10
|
NASDAQ: 9 * 60 + 30,
|
|
10
11
|
NYSE: 9 * 60 + 30,
|
|
@@ -174,11 +175,13 @@ function isTimestampStaleForExchangeSessionUnsafe(
|
|
|
174
175
|
const canonical = canonicalExchange(exchange);
|
|
175
176
|
if (!canonical || !Number.isFinite(timestampMs) || !Number.isFinite(now)) return false;
|
|
176
177
|
|
|
178
|
+
if (ALWAYS_OPEN_EXCHANGES.has(canonical)) {
|
|
179
|
+
return now - timestampMs > ALWAYS_OPEN_MAX_AGE_MS;
|
|
180
|
+
}
|
|
181
|
+
|
|
177
182
|
const timestampDate = exchangeLocalDate(canonical, timestampMs);
|
|
178
183
|
const currentDate = exchangeLocalDate(canonical, now);
|
|
179
184
|
if (!timestampDate || !currentDate || timestampDate === currentDate) return false;
|
|
180
|
-
|
|
181
|
-
if (ALWAYS_OPEN_EXCHANGES.has(canonical)) return true;
|
|
182
185
|
if (marketState === "REGULAR" && !isBeforeKnownRegularOpen(canonical, now)) return true;
|
|
183
186
|
|
|
184
187
|
if (isUsExtendedHoursExchange(canonical)) {
|
|
@@ -58,6 +58,22 @@ describe("quote freshness", () => {
|
|
|
58
58
|
).toBe(true);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
test("treats same-day crypto quotes older than two hours as stale", () => {
|
|
62
|
+
expect(
|
|
63
|
+
isQuoteStaleForCurrentSession(
|
|
64
|
+
quote({
|
|
65
|
+
symbol: "BTC-USD",
|
|
66
|
+
currency: "USD",
|
|
67
|
+
lastUpdated: Date.parse("2026-05-13T12:00:00Z"),
|
|
68
|
+
listingExchangeName: "CCC",
|
|
69
|
+
exchangeName: "CCC",
|
|
70
|
+
marketState: "REGULAR",
|
|
71
|
+
}),
|
|
72
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
73
|
+
),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
61
77
|
test("treats old crypto quotes as stale on the 24/7 venue", () => {
|
|
62
78
|
expect(
|
|
63
79
|
isQuoteStaleForCurrentSession(
|
|
@@ -214,8 +214,8 @@ describe("NewsService", () => {
|
|
|
214
214
|
expect(breaking.every((i) => i.url !== "https://f.com/old-high")).toBe(true);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
it("retains at most
|
|
218
|
-
const items = Array.from({ length:
|
|
217
|
+
it("retains at most 10000 articles", async () => {
|
|
218
|
+
const items = Array.from({ length: 10_100 }, (_, i) =>
|
|
219
219
|
makeItem({
|
|
220
220
|
url: `https://g.com/${i}`,
|
|
221
221
|
publishedAt: new Date(Date.now() - i * 1000),
|
|
@@ -224,7 +224,7 @@ describe("NewsService", () => {
|
|
|
224
224
|
agg.register(makeSource("g", items));
|
|
225
225
|
await agg.poll();
|
|
226
226
|
|
|
227
|
-
expect(agg.getFirehose(undefined,
|
|
227
|
+
expect(agg.getFirehose(undefined, 20_000)).toHaveLength(10_000);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
it("subscribe callback fires on poll and getVersion increments", async () => {
|
|
@@ -488,4 +488,93 @@ describe("NewsService", () => {
|
|
|
488
488
|
|
|
489
489
|
expect(state.articles[0]?.items?.map((item) => item.id)).toEqual(["item-1"]);
|
|
490
490
|
});
|
|
491
|
+
|
|
492
|
+
it("appends paged news instead of replacing the first page", async () => {
|
|
493
|
+
const first = makeItem({ url: "https://example.com/a", title: "First" });
|
|
494
|
+
const second = makeItem({ url: "https://example.com/b", title: "Second" });
|
|
495
|
+
const fetchNewsPage = mock(async (query: { cursor?: string }) => (
|
|
496
|
+
query.cursor === "page-2"
|
|
497
|
+
? { articles: [second], nextCursor: null }
|
|
498
|
+
: { articles: [first], nextCursor: "page-2" }
|
|
499
|
+
));
|
|
500
|
+
agg.register(newsProvider({
|
|
501
|
+
id: "cloud",
|
|
502
|
+
name: "cloud",
|
|
503
|
+
provider: {
|
|
504
|
+
fetchNews: async (query) => (await fetchNewsPage(query)).articles,
|
|
505
|
+
fetchNewsPage,
|
|
506
|
+
},
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
const initial = await agg.load({ feed: "latest", limit: 50 });
|
|
510
|
+
expect(initial.articles.map((article) => article.url)).toEqual(["https://example.com/a"]);
|
|
511
|
+
expect(initial.nextCursor).toBe("page-2");
|
|
512
|
+
|
|
513
|
+
await agg.loadMore({ feed: "latest", limit: 50 });
|
|
514
|
+
const state = agg.getQueryState({ feed: "latest", limit: 50 });
|
|
515
|
+
expect(state.articles.map((article) => article.url)).toEqual([
|
|
516
|
+
"https://example.com/a",
|
|
517
|
+
"https://example.com/b",
|
|
518
|
+
]);
|
|
519
|
+
expect(state.nextCursor).toBeNull();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("keeps paged news past the query page size", async () => {
|
|
523
|
+
const first = makeItem({ url: "https://example.com/a", title: "First" });
|
|
524
|
+
const second = makeItem({ url: "https://example.com/b", title: "Second" });
|
|
525
|
+
const fetchNewsPage = mock(async (query: { cursor?: string }) => (
|
|
526
|
+
query.cursor === "page-2"
|
|
527
|
+
? { articles: [second], nextCursor: "page-3" }
|
|
528
|
+
: { articles: [first], nextCursor: "page-2" }
|
|
529
|
+
));
|
|
530
|
+
agg.register(newsProvider({
|
|
531
|
+
id: "cloud",
|
|
532
|
+
name: "cloud",
|
|
533
|
+
provider: {
|
|
534
|
+
fetchNews: async (query) => (await fetchNewsPage(query)).articles,
|
|
535
|
+
fetchNewsPage,
|
|
536
|
+
},
|
|
537
|
+
}));
|
|
538
|
+
|
|
539
|
+
await agg.load({ feed: "latest", limit: 1 });
|
|
540
|
+
await agg.loadMore({ feed: "latest", limit: 1 });
|
|
541
|
+
const state = agg.getQueryState({ feed: "latest", limit: 1 });
|
|
542
|
+
expect(state.articles.map((article) => article.url)).toEqual([
|
|
543
|
+
"https://example.com/a",
|
|
544
|
+
"https://example.com/b",
|
|
545
|
+
]);
|
|
546
|
+
expect(state.nextCursor).toBe("page-3");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("refresh keeps already paged news", async () => {
|
|
550
|
+
const first = makeItem({ url: "https://example.com/a", title: "First", publishedAt: new Date("2026-01-02") });
|
|
551
|
+
const second = makeItem({ url: "https://example.com/b", title: "Second", publishedAt: new Date("2026-01-01") });
|
|
552
|
+
const newer = makeItem({ url: "https://example.com/c", title: "Newer", publishedAt: new Date("2026-01-03") });
|
|
553
|
+
let head: "initial" | "refresh" = "initial";
|
|
554
|
+
const fetchNewsPage = mock(async (query: { cursor?: string }) => {
|
|
555
|
+
if (query.cursor === "page-2") return { articles: [second], nextCursor: "page-3" };
|
|
556
|
+
if (head === "refresh") return { articles: [newer, first], nextCursor: "page-2" };
|
|
557
|
+
head = "refresh";
|
|
558
|
+
return { articles: [first], nextCursor: "page-2" };
|
|
559
|
+
});
|
|
560
|
+
agg.register(newsProvider({
|
|
561
|
+
id: "cloud",
|
|
562
|
+
name: "cloud",
|
|
563
|
+
provider: {
|
|
564
|
+
fetchNews: async (query) => (await fetchNewsPage(query)).articles,
|
|
565
|
+
fetchNewsPage,
|
|
566
|
+
},
|
|
567
|
+
}));
|
|
568
|
+
|
|
569
|
+
await agg.load({ feed: "latest", limit: 1 });
|
|
570
|
+
await agg.loadMore({ feed: "latest", limit: 1 });
|
|
571
|
+
await agg.poll({ feed: "latest", limit: 1 });
|
|
572
|
+
const state = agg.getQueryState({ feed: "latest", limit: 1 });
|
|
573
|
+
expect(state.articles.map((article) => article.url)).toEqual([
|
|
574
|
+
"https://example.com/c",
|
|
575
|
+
"https://example.com/a",
|
|
576
|
+
"https://example.com/b",
|
|
577
|
+
]);
|
|
578
|
+
expect(state.nextCursor).toBe("page-3");
|
|
579
|
+
});
|
|
491
580
|
});
|
package/src/news/aggregator.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ConnectionHealthRegistry } from "../core/connection-health";
|
|
|
3
3
|
import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_GLOBAL_QUERY,
|
|
6
|
+
MAX_ARTICLES,
|
|
6
7
|
buildNewsQueryKey,
|
|
7
8
|
createIdleNewsQueryState,
|
|
8
9
|
dedupeNewsArticles,
|
|
@@ -36,12 +37,14 @@ interface SourceFetchResult {
|
|
|
36
37
|
articles: NewsArticle[];
|
|
37
38
|
sourceIds: string[];
|
|
38
39
|
failedSourceIds: string[];
|
|
40
|
+
nextCursor: string | null;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface NewsQueryEntry {
|
|
42
44
|
query: NewsQuery;
|
|
43
45
|
state: NewsQueryState;
|
|
44
46
|
inFlight: Promise<NewsQueryState> | null;
|
|
47
|
+
loadMoreInFlight: Promise<void> | null;
|
|
45
48
|
refs: number;
|
|
46
49
|
lastAccessedAt: number;
|
|
47
50
|
}
|
|
@@ -169,6 +172,46 @@ export class NewsService {
|
|
|
169
172
|
return this.refreshQuery(normalizeNewsQuery(query), true);
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
async loadMore(query: NewsQuery): Promise<void> {
|
|
176
|
+
const normalized = normalizeNewsQuery(query);
|
|
177
|
+
const entry = this.queries.get(buildNewsQueryKey(normalized));
|
|
178
|
+
if (!entry || entry.loadMoreInFlight || !entry.state.nextCursor || entry.state.loadingMore) return;
|
|
179
|
+
if (entry.state.articles.length >= MAX_ARTICLES) {
|
|
180
|
+
entry.state = { ...entry.state, nextCursor: null };
|
|
181
|
+
this.notify();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const request = (async () => {
|
|
186
|
+
entry.state = { ...entry.state, loadingMore: true };
|
|
187
|
+
this.notify();
|
|
188
|
+
try {
|
|
189
|
+
const result = await this.fetchFromSources({
|
|
190
|
+
...normalized,
|
|
191
|
+
cursor: entry.state.nextCursor ?? undefined,
|
|
192
|
+
});
|
|
193
|
+
entry.state = {
|
|
194
|
+
...entry.state,
|
|
195
|
+
loadingMore: false,
|
|
196
|
+
articles: filterNewsArticlesForQuery(
|
|
197
|
+
dedupeNewsArticles([...entry.state.articles, ...result.articles]),
|
|
198
|
+
normalized,
|
|
199
|
+
),
|
|
200
|
+
nextCursor: result.nextCursor,
|
|
201
|
+
};
|
|
202
|
+
this.rebuildArticlePool();
|
|
203
|
+
this.notify();
|
|
204
|
+
} catch {
|
|
205
|
+
entry.state = { ...entry.state, loadingMore: false };
|
|
206
|
+
this.notify();
|
|
207
|
+
} finally {
|
|
208
|
+
entry.loadMoreInFlight = null;
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
entry.loadMoreInFlight = request;
|
|
212
|
+
await request;
|
|
213
|
+
}
|
|
214
|
+
|
|
172
215
|
async loadStory(storyId: string): Promise<NewsArticle | null> {
|
|
173
216
|
const sources = this.enabledSources({ feed: "latest" })
|
|
174
217
|
.filter((source) => !!source.provider.fetchNewsStory);
|
|
@@ -227,7 +270,14 @@ export class NewsService {
|
|
|
227
270
|
if (result.sourceIds.length === 0 && result.failedSourceIds.length > 0) {
|
|
228
271
|
throw new Error("News sources unavailable.");
|
|
229
272
|
}
|
|
230
|
-
|
|
273
|
+
if (entry.loadMoreInFlight) return entry.state;
|
|
274
|
+
const incoming = filterNewsArticlesForQuery(dedupeNewsArticles(result.articles), query);
|
|
275
|
+
const existing = entry.state.articles;
|
|
276
|
+
const incomingIds = new Set(incoming.map((article) => article.id));
|
|
277
|
+
const hasOlderPages = existing.some((article) => !incomingIds.has(article.id));
|
|
278
|
+
const articles = existing.length > 0
|
|
279
|
+
? filterNewsArticlesForQuery(dedupeNewsArticles([...incoming, ...existing]), query)
|
|
280
|
+
: incoming;
|
|
231
281
|
const state: NewsQueryState = {
|
|
232
282
|
phase: "ready",
|
|
233
283
|
articles,
|
|
@@ -238,6 +288,8 @@ export class NewsService {
|
|
|
238
288
|
: null,
|
|
239
289
|
updatedAt: this.now(),
|
|
240
290
|
sourceIds: result.sourceIds,
|
|
291
|
+
nextCursor: hasOlderPages ? entry.state.nextCursor : result.nextCursor,
|
|
292
|
+
loadingMore: entry.state.loadingMore,
|
|
241
293
|
};
|
|
242
294
|
entry.state = state;
|
|
243
295
|
entry.lastAccessedAt = this.now();
|
|
@@ -277,6 +329,7 @@ export class NewsService {
|
|
|
277
329
|
query,
|
|
278
330
|
state: createIdleNewsQueryState(),
|
|
279
331
|
inFlight: null,
|
|
332
|
+
loadMoreInFlight: null,
|
|
280
333
|
refs: 0,
|
|
281
334
|
lastAccessedAt: now,
|
|
282
335
|
};
|
|
@@ -313,10 +366,36 @@ export class NewsService {
|
|
|
313
366
|
|
|
314
367
|
private async fetchFromSources(query: NewsQuery): Promise<SourceFetchResult> {
|
|
315
368
|
const sources = this.enabledSources(query);
|
|
369
|
+
const pageSources = query.cursor
|
|
370
|
+
? sources.filter((source) => !!source.provider.fetchNewsPage)
|
|
371
|
+
: sources;
|
|
316
372
|
if (normalizeNewsFeed(query) === "ticker") {
|
|
317
|
-
return this.fetchTickerNews(query,
|
|
373
|
+
return this.fetchTickerNews(query, pageSources);
|
|
318
374
|
}
|
|
319
|
-
return this.fetchMergedNews(query,
|
|
375
|
+
return this.fetchMergedNews(query, pageSources);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async readSourcePage(
|
|
379
|
+
source: NewsCapability,
|
|
380
|
+
query: NewsQuery,
|
|
381
|
+
): Promise<{ articles: NewsArticle[]; nextCursor: string | null }> {
|
|
382
|
+
if (source.provider.fetchNewsPage) {
|
|
383
|
+
const page = await this.trackSourceRequest(
|
|
384
|
+
source,
|
|
385
|
+
"fetchNewsPage",
|
|
386
|
+
() => source.provider.fetchNewsPage!(query),
|
|
387
|
+
);
|
|
388
|
+
return {
|
|
389
|
+
articles: page.articles.map((article) => markDetailCapableArticle(source, article)),
|
|
390
|
+
nextCursor: page.nextCursor ?? null,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const articles = (await this.trackSourceRequest(
|
|
394
|
+
source,
|
|
395
|
+
"fetchNews",
|
|
396
|
+
() => source.provider.fetchNews(query),
|
|
397
|
+
)).map((article) => markDetailCapableArticle(source, article));
|
|
398
|
+
return { articles, nextCursor: null };
|
|
320
399
|
}
|
|
321
400
|
|
|
322
401
|
private async fetchTickerNews(query: NewsQuery, sources: NewsCapability[]): Promise<SourceFetchResult> {
|
|
@@ -324,45 +403,44 @@ export class NewsService {
|
|
|
324
403
|
const failedSourceIds: string[] = [];
|
|
325
404
|
for (const source of sources) {
|
|
326
405
|
try {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
406
|
+
const page = await this.readSourcePage(source, query);
|
|
407
|
+
const result = {
|
|
408
|
+
articles: page.articles,
|
|
409
|
+
sourceIds: [newsCapabilitySourceId(source)],
|
|
410
|
+
failedSourceIds,
|
|
411
|
+
nextCursor: page.nextCursor,
|
|
412
|
+
};
|
|
413
|
+
if (page.articles.length > 0) return result;
|
|
334
414
|
firstEmpty ??= result;
|
|
335
415
|
} catch {
|
|
336
416
|
failedSourceIds.push(newsCapabilitySourceId(source));
|
|
337
417
|
}
|
|
338
418
|
}
|
|
339
|
-
return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds };
|
|
419
|
+
return firstEmpty ?? { articles: [], sourceIds: [], failedSourceIds, nextCursor: null };
|
|
340
420
|
}
|
|
341
421
|
|
|
342
422
|
private async fetchMergedNews(query: NewsQuery, sources: NewsCapability[]): Promise<SourceFetchResult> {
|
|
343
423
|
const settled = await Promise.allSettled(
|
|
344
424
|
sources.map(async (source) => ({
|
|
345
425
|
source,
|
|
346
|
-
|
|
347
|
-
source,
|
|
348
|
-
"fetchNews",
|
|
349
|
-
() => source.provider.fetchNews(query),
|
|
350
|
-
)).map((article) => markDetailCapableArticle(source, article)),
|
|
426
|
+
page: await this.readSourcePage(source, query),
|
|
351
427
|
})),
|
|
352
428
|
);
|
|
353
429
|
const articles: NewsArticle[] = [];
|
|
354
430
|
const sourceIds: string[] = [];
|
|
355
431
|
const failedSourceIds: string[] = [];
|
|
432
|
+
let nextCursor: string | null = null;
|
|
356
433
|
settled.forEach((result, index) => {
|
|
357
434
|
if (result.status !== "fulfilled") {
|
|
358
435
|
const source = sources[index];
|
|
359
436
|
if (source) failedSourceIds.push(newsCapabilitySourceId(source));
|
|
360
437
|
return;
|
|
361
438
|
}
|
|
362
|
-
articles.push(...result.value.articles);
|
|
439
|
+
articles.push(...result.value.page.articles);
|
|
363
440
|
sourceIds.push(newsCapabilitySourceId(result.value.source));
|
|
441
|
+
nextCursor ??= result.value.page.nextCursor;
|
|
364
442
|
});
|
|
365
|
-
return { articles, sourceIds, failedSourceIds };
|
|
443
|
+
return { articles, sourceIds, failedSourceIds, nextCursor };
|
|
366
444
|
}
|
|
367
445
|
|
|
368
446
|
private trackSourceRequest<T>(
|
|
@@ -393,6 +471,8 @@ export class NewsService {
|
|
|
393
471
|
error: null,
|
|
394
472
|
updatedAt: this.now(),
|
|
395
473
|
sourceIds: [...new Set([...entry.state.sourceIds, newsCapabilitySourceId(source)])],
|
|
474
|
+
nextCursor: entry.state.nextCursor,
|
|
475
|
+
loadingMore: false,
|
|
396
476
|
};
|
|
397
477
|
changed = true;
|
|
398
478
|
}
|
package/src/news/hooks.test.tsx
CHANGED
package/src/news/hooks.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { buildNewsQueryKey, type NewsService } from "./aggregator";
|
|
3
3
|
import type { NewsArticle, NewsQuery, NewsQueryState } from "./types";
|
|
4
|
+
import { useTableLoadMore } from "../components/table-view-shared";
|
|
5
|
+
import type { ScrollBoxRenderable } from "../ui";
|
|
4
6
|
|
|
5
7
|
let sharedService: NewsService | null = null;
|
|
6
8
|
|
|
@@ -18,6 +20,8 @@ const IDLE_NEWS_QUERY_STATE: NewsQueryState = {
|
|
|
18
20
|
error: null,
|
|
19
21
|
updatedAt: null,
|
|
20
22
|
sourceIds: [],
|
|
23
|
+
nextCursor: null,
|
|
24
|
+
loadingMore: false,
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
export function useNewsArticles(query: NewsQuery | null | undefined): NewsQueryState {
|
|
@@ -38,3 +42,25 @@ export function useNewsArticles(query: NewsQuery | null | undefined): NewsQueryS
|
|
|
38
42
|
export function useLoadNewsStory(): (storyId: string) => Promise<NewsArticle | null> {
|
|
39
43
|
return useCallback(async (storyId: string) => sharedService?.loadStory(storyId) ?? null, []);
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
export function useNewsLoadMore(query: NewsQuery | null | undefined): () => void {
|
|
47
|
+
const key = query ? buildNewsQueryKey(query) : null;
|
|
48
|
+
return useCallback(() => {
|
|
49
|
+
if (!query) return;
|
|
50
|
+
void sharedService?.loadMore(query);
|
|
51
|
+
}, [key]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useNewsTableLoadMore(
|
|
55
|
+
query: NewsQuery | null | undefined,
|
|
56
|
+
state: Pick<NewsQueryState, "nextCursor" | "loadingMore">,
|
|
57
|
+
) {
|
|
58
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
59
|
+
const loadMore = useNewsLoadMore(query);
|
|
60
|
+
const onBodyScrollActivity = useTableLoadMore(
|
|
61
|
+
scrollRef,
|
|
62
|
+
!!state.nextCursor && !state.loadingMore,
|
|
63
|
+
loadMore,
|
|
64
|
+
);
|
|
65
|
+
return { scrollRef, onBodyScrollActivity };
|
|
66
|
+
}
|
package/src/news/news-model.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { canonicalExchange, normalizeSymbol } from "../utils/exchanges";
|
|
|
2
2
|
import type { NewsCapability } from "../capabilities";
|
|
3
3
|
import type { NewsArticle, NewsFeed, NewsQuery, NewsQueryState } from "./types";
|
|
4
4
|
|
|
5
|
-
const MAX_ARTICLES =
|
|
5
|
+
export const MAX_ARTICLES = 10_000;
|
|
6
6
|
export const DEFAULT_GLOBAL_QUERY: NewsQuery = { feed: "latest", limit: MAX_ARTICLES };
|
|
7
7
|
|
|
8
8
|
const FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
|
|
@@ -86,6 +86,8 @@ export function createIdleNewsQueryState(): NewsQueryState {
|
|
|
86
86
|
error: null,
|
|
87
87
|
updatedAt: null,
|
|
88
88
|
sourceIds: [],
|
|
89
|
+
nextCursor: null,
|
|
90
|
+
loadingMore: false,
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -204,5 +206,6 @@ export function filterNewsArticlesForQuery(items: NewsArticle[], query: NewsQuer
|
|
|
204
206
|
if (query.breaking != null) {
|
|
205
207
|
filtered = filtered.filter((item) => item.isBreaking === query.breaking);
|
|
206
208
|
}
|
|
207
|
-
|
|
209
|
+
// Page size is a fetch hint. Accumulated load-more results keep MAX_ARTICLES.
|
|
210
|
+
return filtered.slice(0, MAX_ARTICLES);
|
|
208
211
|
}
|
package/src/news/types.ts
CHANGED
|
@@ -68,10 +68,17 @@ export interface NewsQuery {
|
|
|
68
68
|
|
|
69
69
|
export type NewsQueryPhase = "idle" | "loading" | "ready" | "refreshing" | "error";
|
|
70
70
|
|
|
71
|
+
export interface NewsPage {
|
|
72
|
+
articles: NewsArticle[];
|
|
73
|
+
nextCursor?: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
export interface NewsQueryState {
|
|
72
77
|
phase: NewsQueryPhase;
|
|
73
78
|
articles: NewsArticle[];
|
|
74
79
|
error: string | null;
|
|
75
80
|
updatedAt: number | null;
|
|
76
81
|
sourceIds: string[];
|
|
82
|
+
nextCursor: string | null;
|
|
83
|
+
loadingMore: boolean;
|
|
77
84
|
}
|
|
@@ -55,6 +55,35 @@ describe("broker position normalization", () => {
|
|
|
55
55
|
markPrice: 250,
|
|
56
56
|
}));
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test("stores shorts as absolute shares and weighted-averages merged short lots", () => {
|
|
60
|
+
const snapshot = normalizeRobinhoodSnapshot(
|
|
61
|
+
{ accounts: [{ account_number: "RH-1", account_type: "INDIVIDUAL" }] },
|
|
62
|
+
[{ result: { positions: [
|
|
63
|
+
{
|
|
64
|
+
accountNumber: "RH-1",
|
|
65
|
+
instrument: { symbol: "TSLA" },
|
|
66
|
+
quantity: "-10",
|
|
67
|
+
average_cost: "100",
|
|
68
|
+
market_value: "-800",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
accountNumber: "RH-1",
|
|
72
|
+
instrument: { symbol: "TSLA" },
|
|
73
|
+
quantity: "-5",
|
|
74
|
+
average_cost: "110",
|
|
75
|
+
market_value: "-400",
|
|
76
|
+
},
|
|
77
|
+
] } }],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(snapshot.positions).toEqual([expect.objectContaining({
|
|
81
|
+
ticker: "TSLA",
|
|
82
|
+
shares: 15,
|
|
83
|
+
avgCost: 1550 / 15,
|
|
84
|
+
side: "short",
|
|
85
|
+
})]);
|
|
86
|
+
});
|
|
58
87
|
});
|
|
59
88
|
|
|
60
89
|
describe("broker authentication boundaries", () => {
|
|
@@ -108,6 +137,35 @@ describe("broker authentication boundaries", () => {
|
|
|
108
137
|
])).toThrow("read-only get_equity_positions");
|
|
109
138
|
});
|
|
110
139
|
|
|
140
|
+
test("aggregates identical same-account lots instead of keeping only the last row", () => {
|
|
141
|
+
const snapshot = normalizeRobinhoodSnapshot(
|
|
142
|
+
{ accounts: [{ account_number: "RH-1", currency: "USD" }] },
|
|
143
|
+
{ positions: [
|
|
144
|
+
{
|
|
145
|
+
accountNumber: "RH-1",
|
|
146
|
+
instrument: { symbol: "HOOD" },
|
|
147
|
+
quantity: "2",
|
|
148
|
+
total_cost: "80",
|
|
149
|
+
market_value: "100",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
accountNumber: "RH-1",
|
|
153
|
+
instrument: { symbol: "HOOD" },
|
|
154
|
+
quantity: "3",
|
|
155
|
+
total_cost: "150",
|
|
156
|
+
market_value: "150",
|
|
157
|
+
},
|
|
158
|
+
] },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(snapshot.positions).toEqual([expect.objectContaining({
|
|
162
|
+
ticker: "HOOD",
|
|
163
|
+
shares: 5,
|
|
164
|
+
avgCost: 46,
|
|
165
|
+
marketValue: 250,
|
|
166
|
+
})]);
|
|
167
|
+
});
|
|
168
|
+
|
|
111
169
|
test("SimpleFIN accepts URL-safe setup tokens and rejects non-HTTPS claims", () => {
|
|
112
170
|
const claimUrl = "https://bridge.simplefin.org/simplefin/claim/demo";
|
|
113
171
|
const token = Buffer.from(claimUrl).toString("base64url");
|
|
@@ -80,11 +80,48 @@ function uniqueSnapshot(accounts: BrokerAccount[], positions: BrokerPosition[]):
|
|
|
80
80
|
currency: position.currency,
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
return { accounts: uniqueAccounts, positions: mergeIdenticalPositions(positions.map(withCanonicalShares)) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function withCanonicalShares(position: BrokerPosition): BrokerPosition {
|
|
87
|
+
const side = position.side ?? (position.shares < 0 ? "short" : "long");
|
|
88
|
+
const shares = Math.abs(position.shares);
|
|
89
|
+
if (shares === position.shares && side === position.side) return position;
|
|
90
|
+
return { ...position, shares, side };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function mergeIdenticalPositions(positions: BrokerPosition[]): BrokerPosition[] {
|
|
94
|
+
const merged = new Map<string, BrokerPosition>();
|
|
95
|
+
for (const position of positions) {
|
|
96
|
+
const key = [
|
|
97
|
+
position.accountId ?? "",
|
|
98
|
+
position.ticker,
|
|
99
|
+
position.assetCategory ?? "",
|
|
100
|
+
position.exchange ?? "",
|
|
101
|
+
position.brokerContract?.conId ?? "",
|
|
102
|
+
].join(":");
|
|
103
|
+
const existing = merged.get(key);
|
|
104
|
+
if (!existing) {
|
|
105
|
+
merged.set(key, position);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const shares = existing.shares + position.shares;
|
|
109
|
+
const existingCost = (existing.avgCost ?? 0) * existing.shares;
|
|
110
|
+
const nextCost = (position.avgCost ?? 0) * position.shares;
|
|
111
|
+
merged.set(key, {
|
|
112
|
+
...existing,
|
|
113
|
+
shares,
|
|
114
|
+
avgCost: shares !== 0 ? (existingCost + nextCost) / shares : existing.avgCost,
|
|
115
|
+
marketValue: sumOptional(existing.marketValue, position.marketValue),
|
|
116
|
+
unrealizedPnl: sumOptional(existing.unrealizedPnl, position.unrealizedPnl),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return [...merged.values()];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sumOptional(left?: number, right?: number): number | undefined {
|
|
123
|
+
if (left == null && right == null) return undefined;
|
|
124
|
+
return (left ?? 0) + (right ?? 0);
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
export function normalizeRobinhoodSnapshot(accountsPayload: unknown, positionsPayload: unknown): BrokerPortfolioSnapshot {
|