gloomberb 0.10.0 → 0.10.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/package.json +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataProvider, QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
2
2
|
import type { Quote } from "../../types/financials";
|
|
3
3
|
import type { InstrumentRef } from "../request-types";
|
|
4
|
+
import { mergeQuoteSubscriptionTargets } from "../quote-subscription-target";
|
|
4
5
|
import { QueryStore } from "../query-store";
|
|
5
6
|
import type { QueryEntry } from "../result-types";
|
|
6
7
|
import { buildQuoteKey, toMarketDataContext } from "../selectors";
|
|
@@ -61,47 +62,10 @@ const STREAM_QUOTE_FIELDS: Array<keyof Quote> = [
|
|
|
61
62
|
"low",
|
|
62
63
|
"mark",
|
|
63
64
|
"dataSource",
|
|
65
|
+
"delivery",
|
|
66
|
+
"stale",
|
|
64
67
|
];
|
|
65
68
|
|
|
66
|
-
function quoteSubscriptionPriorityScore(target: QuoteSubscriptionTarget): number {
|
|
67
|
-
let score = Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0;
|
|
68
|
-
if (target.selected) score += 10_000;
|
|
69
|
-
if (target.visible) score += 5_000;
|
|
70
|
-
if (target.surface === "detail" || target.surface === "monitor") score += 4_000;
|
|
71
|
-
if (target.surface === "portfolio" || target.surface === "watchlist") score += 1_000;
|
|
72
|
-
if (target.surface === "screener") score += 700;
|
|
73
|
-
if (target.surface === "inline") score += 200;
|
|
74
|
-
return score;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function mergeQuoteSubscriptionTargets(targets: Iterable<QuoteSubscriptionTarget>): QuoteSubscriptionTarget | null {
|
|
78
|
-
let selectedTarget: QuoteSubscriptionTarget | null = null;
|
|
79
|
-
let selectedScore = -1;
|
|
80
|
-
let visible = false;
|
|
81
|
-
let selected = false;
|
|
82
|
-
let weight = 0;
|
|
83
|
-
|
|
84
|
-
for (const target of targets) {
|
|
85
|
-
const score = quoteSubscriptionPriorityScore(target);
|
|
86
|
-
if (!selectedTarget || score > selectedScore) {
|
|
87
|
-
selectedTarget = target;
|
|
88
|
-
selectedScore = score;
|
|
89
|
-
}
|
|
90
|
-
visible ||= target.visible === true;
|
|
91
|
-
selected ||= target.selected === true;
|
|
92
|
-
weight = Math.max(weight, Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return selectedTarget
|
|
96
|
-
? {
|
|
97
|
-
...selectedTarget,
|
|
98
|
-
visible,
|
|
99
|
-
selected,
|
|
100
|
-
weight,
|
|
101
|
-
}
|
|
102
|
-
: null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
69
|
function quoteTargetFromInstrument(
|
|
106
70
|
instrument: InstrumentRef,
|
|
107
71
|
priority: QuoteSubscriptionPriority = {},
|
|
@@ -289,7 +253,7 @@ export class QuoteSubscriptionManager {
|
|
|
289
253
|
if (!this.dataProvider.subscribeQuotes) return;
|
|
290
254
|
|
|
291
255
|
const activeEntries = [...this.quoteSubscriptions.entries()]
|
|
292
|
-
.filter(([, entry]) => entry.targets.size > 0
|
|
256
|
+
.filter(([, entry]) => entry.targets.size > 0)
|
|
293
257
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
294
258
|
const nextSignature = activeEntries.map(([key, entry]) => [
|
|
295
259
|
key,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, jest, test } from "bun:test";
|
|
2
2
|
import { MarketDataCoordinator } from "./index";
|
|
3
3
|
import { buildQuoteKey } from "../selectors";
|
|
4
4
|
import { createTestDataProvider } from "../../test-support/data-provider";
|
|
@@ -117,6 +117,43 @@ describe("MarketDataCoordinator key subscriptions", () => {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("replaces a capped target window without retaining pending removals", () => {
|
|
121
|
+
jest.useFakeTimers();
|
|
122
|
+
try {
|
|
123
|
+
const subscriptions: string[][] = [];
|
|
124
|
+
let disposals = 0;
|
|
125
|
+
const provider = createTestDataProvider({
|
|
126
|
+
id: "test-provider",
|
|
127
|
+
subscribeQuotes: (targets) => {
|
|
128
|
+
subscriptions.push(targets.map((target) => target.symbol));
|
|
129
|
+
return () => {
|
|
130
|
+
disposals += 1;
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
135
|
+
const oldTargets = Array.from({ length: 16 }, (_, index) => ({
|
|
136
|
+
instrument: { symbol: `OPT${index}`, exchange: "OPTIONS" },
|
|
137
|
+
}));
|
|
138
|
+
const newTargets = Array.from({ length: 16 }, (_, index) => ({
|
|
139
|
+
instrument: { symbol: `OPT${index + 2}`, exchange: "OPTIONS" },
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const unsubscribeOld = coordinator.subscribeQuotes(oldTargets);
|
|
143
|
+
unsubscribeOld();
|
|
144
|
+
const unsubscribeNew = coordinator.subscribeQuotes(newTargets);
|
|
145
|
+
|
|
146
|
+
expect(disposals).toBe(1);
|
|
147
|
+
expect(subscriptions).toHaveLength(2);
|
|
148
|
+
expect(subscriptions[1]).toEqual(newTargets.map(({ instrument }) => instrument.symbol).sort());
|
|
149
|
+
|
|
150
|
+
unsubscribeNew();
|
|
151
|
+
jest.runAllTimers();
|
|
152
|
+
} finally {
|
|
153
|
+
jest.useRealTimers();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
120
157
|
test("notifies listeners for changed keys only", async () => {
|
|
121
158
|
const { provider, emitQuote } = createProvider();
|
|
122
159
|
const coordinator = new MarketDataCoordinator(provider);
|
|
@@ -207,4 +244,55 @@ describe("MarketDataCoordinator key subscriptions", () => {
|
|
|
207
244
|
Date.now = realDateNow;
|
|
208
245
|
}
|
|
209
246
|
});
|
|
247
|
+
|
|
248
|
+
test("does not refresh receipt time from an explicitly stale polled quote", async () => {
|
|
249
|
+
const realDateNow = Date.now;
|
|
250
|
+
const { provider, emitQuote } = createProvider();
|
|
251
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
252
|
+
const option = { symbol: "AAPL260731C00110000", exchange: "OPTIONS" };
|
|
253
|
+
const firstTimestamp = 1_800_000_000_000;
|
|
254
|
+
try {
|
|
255
|
+
coordinator.subscribeQuotes([{ instrument: option }]);
|
|
256
|
+
|
|
257
|
+
Date.now = () => firstTimestamp;
|
|
258
|
+
emitQuote(
|
|
259
|
+
option,
|
|
260
|
+
quote(option.symbol, 2.5, {
|
|
261
|
+
lastUpdated: firstTimestamp,
|
|
262
|
+
dataSource: "live",
|
|
263
|
+
delivery: "stream",
|
|
264
|
+
stale: false,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
await flushCoordinator();
|
|
268
|
+
|
|
269
|
+
expect(coordinator.getQuoteEntry(option).data).toMatchObject({
|
|
270
|
+
delivery: "stream",
|
|
271
|
+
stale: false,
|
|
272
|
+
receivedAt: firstTimestamp,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
Date.now = () => firstTimestamp + 10_000;
|
|
276
|
+
emitQuote(
|
|
277
|
+
option,
|
|
278
|
+
quote(option.symbol, 2.5, {
|
|
279
|
+
lastUpdated: firstTimestamp,
|
|
280
|
+
dataSource: "live",
|
|
281
|
+
delivery: "poll",
|
|
282
|
+
stale: true,
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
await flushCoordinator();
|
|
286
|
+
|
|
287
|
+
const entry = coordinator.getQuoteEntry(option);
|
|
288
|
+
expect(entry.data).toBeNull();
|
|
289
|
+
expect(entry.lastGoodData).toMatchObject({
|
|
290
|
+
delivery: "stream",
|
|
291
|
+
stale: false,
|
|
292
|
+
receivedAt: firstTimestamp,
|
|
293
|
+
});
|
|
294
|
+
} finally {
|
|
295
|
+
Date.now = realDateNow;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
210
298
|
});
|
|
@@ -30,6 +30,10 @@ interface ChartQueryOptions {
|
|
|
30
30
|
refreshIntervalMs?: number;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
interface OptionsQueryOptions {
|
|
34
|
+
refreshIntervalMs?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
type SharedCoordinator = NonNullable<ReturnType<typeof getSharedMarketDataCoordinator>>;
|
|
34
38
|
|
|
35
39
|
function loadChartRequests(
|
|
@@ -180,6 +184,34 @@ export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerR
|
|
|
180
184
|
return entry;
|
|
181
185
|
}
|
|
182
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Observe quote entries already populated by the shared stream without issuing
|
|
189
|
+
* one HTTP snapshot request per instrument.
|
|
190
|
+
*/
|
|
191
|
+
export function useQuoteEntries(
|
|
192
|
+
instruments: readonly InstrumentRef[],
|
|
193
|
+
): Map<string, QueryEntry<Quote>> {
|
|
194
|
+
const instrumentKey = instruments
|
|
195
|
+
.map((instrument) => buildQuoteKey(instrument))
|
|
196
|
+
.join("\u001f");
|
|
197
|
+
const keys = useMemo(
|
|
198
|
+
() => instruments.map((instrument) => buildQuoteKey(instrument)),
|
|
199
|
+
[instrumentKey],
|
|
200
|
+
);
|
|
201
|
+
const keysVersion = useCoordinatorKeysVersion(keys);
|
|
202
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
203
|
+
|
|
204
|
+
return useMemo(() => {
|
|
205
|
+
if (!coordinator) return new Map<string, QueryEntry<Quote>>();
|
|
206
|
+
return new Map(
|
|
207
|
+
instruments.map((instrument) => [
|
|
208
|
+
buildQuoteKey(instrument),
|
|
209
|
+
coordinator.getQuoteEntry(instrument),
|
|
210
|
+
]),
|
|
211
|
+
);
|
|
212
|
+
}, [coordinator, instrumentKey, keysVersion]);
|
|
213
|
+
}
|
|
214
|
+
|
|
183
215
|
export function useChartQuery(
|
|
184
216
|
request: ChartRequest | null | undefined,
|
|
185
217
|
options: ChartQueryOptions = {},
|
|
@@ -275,11 +307,16 @@ export function useChartQueries(
|
|
|
275
307
|
return useMemo(() => new Map(entries), [entries]);
|
|
276
308
|
}
|
|
277
309
|
|
|
278
|
-
export function useOptionsQuery(
|
|
310
|
+
export function useOptionsQuery(
|
|
311
|
+
request: OptionsRequest | null | undefined,
|
|
312
|
+
options: OptionsQueryOptions = {},
|
|
313
|
+
): QueryEntry<OptionsChain> | null {
|
|
279
314
|
const requestKey = request ? buildOptionsKey(request) : null;
|
|
280
315
|
useCoordinatorKeysVersion(requestKey ? [requestKey] : []);
|
|
281
316
|
const coordinator = getSharedMarketDataCoordinator();
|
|
282
317
|
const entry = coordinator && request ? coordinator.getOptionsEntry(request) : null;
|
|
318
|
+
const appActive = useAppActive();
|
|
319
|
+
const refreshIntervalMs = Math.max(0, options.refreshIntervalMs ?? 0);
|
|
283
320
|
|
|
284
321
|
useEffect(() => {
|
|
285
322
|
const coordinator = getSharedMarketDataCoordinator();
|
|
@@ -287,6 +324,15 @@ export function useOptionsQuery(request: OptionsRequest | null | undefined): Que
|
|
|
287
324
|
void coordinator.loadOptions(request).catch(() => {});
|
|
288
325
|
}, [requestKey]);
|
|
289
326
|
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
329
|
+
if (!coordinator || !request || !appActive || refreshIntervalMs <= 0) return;
|
|
330
|
+
const interval = setInterval(() => {
|
|
331
|
+
void coordinator.loadOptions(request, { forceRefresh: true }).catch(() => {});
|
|
332
|
+
}, refreshIntervalMs);
|
|
333
|
+
return () => clearInterval(interval);
|
|
334
|
+
}, [appActive, refreshIntervalMs, requestKey]);
|
|
335
|
+
|
|
290
336
|
return entry;
|
|
291
337
|
}
|
|
292
338
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MarketState, Quote, QuoteFieldProvenance } from "../../types/financials";
|
|
2
|
-
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
2
|
+
import { blendHex, colors, priceColor, type ThemeColors } from "../../theme/colors";
|
|
3
3
|
|
|
4
4
|
const CLOSED_CHANGE_MUTING_RATIO = 0.55;
|
|
5
5
|
|
|
@@ -20,14 +20,14 @@ export function marketStateLabel(state: MarketState): string {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function marketStateColor(state: MarketState): string {
|
|
23
|
+
export function marketStateColor(state: MarketState, palette: ThemeColors = colors): string {
|
|
24
24
|
switch (state) {
|
|
25
|
-
case "REGULAR": return
|
|
25
|
+
case "REGULAR": return palette.positive;
|
|
26
26
|
case "PRE":
|
|
27
|
-
case "POST": return
|
|
27
|
+
case "POST": return palette.textBright;
|
|
28
28
|
case "PREPRE":
|
|
29
29
|
case "POSTPOST":
|
|
30
|
-
case "CLOSED": return
|
|
30
|
+
case "CLOSED": return palette.textDim;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -47,7 +47,7 @@ export function marketStateDot(state?: MarketState): string {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
function isClosedMarketState(state?: MarketState): boolean {
|
|
51
51
|
return state === "CLOSED" || state === "PREPRE" || state === "POSTPOST";
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -27,7 +27,7 @@ function coercePointDate(value: Date | string | number): Date | null {
|
|
|
27
27
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
function normalizePriceReturnHistory(points: readonly PricePoint[]): PricePoint[] {
|
|
31
31
|
const byTimestamp = new Map<number, PricePoint>();
|
|
32
32
|
for (const point of points) {
|
|
33
33
|
const date = coercePointDate(point.date as Date | string | number);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mergeQuoteSubscriptionTargets } from "./quote-subscription-target";
|
|
3
|
+
|
|
4
|
+
describe("mergeQuoteSubscriptionTargets", () => {
|
|
5
|
+
test("does not synthesize optional priority fields", () => {
|
|
6
|
+
const merged = mergeQuoteSubscriptionTargets([
|
|
7
|
+
{ symbol: "AAPL", exchange: "NASDAQ" },
|
|
8
|
+
{ symbol: "AAPL", exchange: "NASDAQ" },
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
expect(merged).toEqual({ symbol: "AAPL", exchange: "NASDAQ" });
|
|
12
|
+
expect(merged).not.toHaveProperty("visible");
|
|
13
|
+
expect(merged).not.toHaveProperty("selected");
|
|
14
|
+
expect(merged).not.toHaveProperty("weight");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("combines priority fields that callers explicitly provide", () => {
|
|
18
|
+
const merged = mergeQuoteSubscriptionTargets([
|
|
19
|
+
{ symbol: "TSLA", surface: "watchlist" as const, visible: true, selected: false, weight: 5 },
|
|
20
|
+
{ symbol: "TSLA", surface: "detail" as const, visible: false, selected: true, weight: 20 },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
expect(merged).toEqual({
|
|
24
|
+
symbol: "TSLA",
|
|
25
|
+
surface: "detail",
|
|
26
|
+
visible: true,
|
|
27
|
+
selected: true,
|
|
28
|
+
weight: 20,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("preserves explicit false and zero values", () => {
|
|
33
|
+
expect(mergeQuoteSubscriptionTargets([
|
|
34
|
+
{ symbol: "MSFT", visible: false, selected: false, weight: 0 },
|
|
35
|
+
])).toEqual({ symbol: "MSFT", visible: false, selected: false, weight: 0 });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface QuoteSubscriptionPriorityTarget {
|
|
2
|
+
surface?: "portfolio" | "watchlist" | "detail" | "monitor" | "inline" | "options" | "screener" | "unknown";
|
|
3
|
+
visible?: boolean;
|
|
4
|
+
selected?: boolean;
|
|
5
|
+
weight?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function quoteSubscriptionPriorityScore(target: QuoteSubscriptionPriorityTarget): number {
|
|
9
|
+
let score = Number.isFinite(target.weight) ? Math.max(0, target.weight ?? 0) : 0;
|
|
10
|
+
if (target.selected) score += 10_000;
|
|
11
|
+
if (target.visible) score += 5_000;
|
|
12
|
+
if (target.surface === "detail" || target.surface === "monitor" || target.surface === "options") score += 4_000;
|
|
13
|
+
if (target.surface === "portfolio" || target.surface === "watchlist") score += 1_000;
|
|
14
|
+
if (target.surface === "screener") score += 700;
|
|
15
|
+
if (target.surface === "inline") score += 200;
|
|
16
|
+
return score;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function mergeQuoteSubscriptionTargets<T extends QuoteSubscriptionPriorityTarget>(
|
|
20
|
+
targets: Iterable<T>,
|
|
21
|
+
): T | null {
|
|
22
|
+
let selectedTarget: T | null = null;
|
|
23
|
+
let selectedScore = -1;
|
|
24
|
+
let visible = false;
|
|
25
|
+
let selected = false;
|
|
26
|
+
let weight = 0;
|
|
27
|
+
let hasVisible = false;
|
|
28
|
+
let hasSelected = false;
|
|
29
|
+
let hasWeight = false;
|
|
30
|
+
|
|
31
|
+
for (const target of targets) {
|
|
32
|
+
const score = quoteSubscriptionPriorityScore(target);
|
|
33
|
+
if (!selectedTarget || score > selectedScore) {
|
|
34
|
+
selectedTarget = target;
|
|
35
|
+
selectedScore = score;
|
|
36
|
+
}
|
|
37
|
+
if (target.visible !== undefined) {
|
|
38
|
+
hasVisible = true;
|
|
39
|
+
visible ||= target.visible;
|
|
40
|
+
}
|
|
41
|
+
if (target.selected !== undefined) {
|
|
42
|
+
hasSelected = true;
|
|
43
|
+
selected ||= target.selected;
|
|
44
|
+
}
|
|
45
|
+
if (Number.isFinite(target.weight)) {
|
|
46
|
+
hasWeight = true;
|
|
47
|
+
weight = Math.max(weight, Math.max(0, target.weight ?? 0));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!selectedTarget) return null;
|
|
52
|
+
const merged = { ...selectedTarget };
|
|
53
|
+
if (hasVisible) merged.visible = visible;
|
|
54
|
+
else delete merged.visible;
|
|
55
|
+
if (hasSelected) merged.selected = selected;
|
|
56
|
+
else delete merged.selected;
|
|
57
|
+
if (hasWeight) merged.weight = weight;
|
|
58
|
+
else delete merged.weight;
|
|
59
|
+
return merged;
|
|
60
|
+
}
|
|
@@ -21,6 +21,7 @@ function isQuoteMissingActiveSessionPrice(quote: Quote): boolean {
|
|
|
21
21
|
|
|
22
22
|
export function isQuoteStaleForCurrentSession(quote: Quote | null | undefined, now = Date.now()): boolean {
|
|
23
23
|
if (!quote) return false;
|
|
24
|
+
if (quote.stale === true) return true;
|
|
24
25
|
if (isQuoteMissingActiveSessionPrice(quote)) return true;
|
|
25
26
|
|
|
26
27
|
return isTimestampStaleForExchangeSession(
|
|
@@ -41,6 +41,8 @@ const PRICE_FIELD_KEYS = [
|
|
|
41
41
|
"mark",
|
|
42
42
|
"lastUpdated",
|
|
43
43
|
"receivedAt",
|
|
44
|
+
"delivery",
|
|
45
|
+
"stale",
|
|
44
46
|
] as const;
|
|
45
47
|
|
|
46
48
|
const SESSION_FIELD_KEYS = [
|
|
@@ -441,6 +443,8 @@ export function resolveCanonicalQuote(
|
|
|
441
443
|
name: resolved.name as Quote["name"],
|
|
442
444
|
lastUpdated: Number(resolved.lastUpdated ?? priceProvider?.lastUpdated ?? sessionProvider?.lastUpdated ?? now),
|
|
443
445
|
receivedAt: resolved.receivedAt as Quote["receivedAt"],
|
|
446
|
+
delivery: resolved.delivery as Quote["delivery"],
|
|
447
|
+
stale: resolved.stale as Quote["stale"],
|
|
444
448
|
exchangeName: resolved.exchangeName as Quote["exchangeName"],
|
|
445
449
|
fullExchangeName: resolved.fullExchangeName as Quote["fullExchangeName"],
|
|
446
450
|
listingExchangeName: resolved.listingExchangeName as Quote["listingExchangeName"],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { TimeRange } from "../
|
|
2
|
-
import type { ManualChartResolution } from "../
|
|
1
|
+
import type { TimeRange } from "../time-series/range";
|
|
2
|
+
import type { ManualChartResolution } from "../time-series/resolution";
|
|
3
3
|
import type { QuoteSubscriptionTarget } from "../types/data-provider";
|
|
4
4
|
import type { BrokerContractRef } from "../types/instrument";
|
|
5
5
|
import type { TickerRecord } from "../types/ticker";
|
|
@@ -95,7 +95,7 @@ export function quoteSubscriptionTargetFromTicker(
|
|
|
95
95
|
return quoteSubscriptionTargetFromInstrument(instrument, route);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
function quoteSubscriptionTargetFromInstrument(
|
|
99
99
|
instrument: InstrumentRef | null | undefined,
|
|
100
100
|
route: QuoteSubscriptionTarget["route"] = "auto",
|
|
101
101
|
): QuoteSubscriptionTarget | null {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, mock } from "bun:test";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
2
|
import { NewsService } from "./aggregator";
|
|
3
3
|
import { newsProvider, type NewsCapability } from "../capabilities";
|
|
4
4
|
import type { MarketNewsItem } from "../types/news-source";
|
|
@@ -69,6 +69,10 @@ describe("NewsService", () => {
|
|
|
69
69
|
agg = new NewsService();
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
agg.stop();
|
|
74
|
+
});
|
|
75
|
+
|
|
72
76
|
it("deduplicates by URL, keeping higher importance", async () => {
|
|
73
77
|
const low = makeItem({ url: "https://example.com/1", importance: 40 });
|
|
74
78
|
const high = makeItem({ url: "https://example.com/1", importance: 80 });
|
|
@@ -224,6 +228,98 @@ describe("NewsService", () => {
|
|
|
224
228
|
expect(states).toHaveLength(callCount);
|
|
225
229
|
});
|
|
226
230
|
|
|
231
|
+
it("polls referenced queries only and stops after the last watcher leaves", async () => {
|
|
232
|
+
const query = { feed: "latest" as const, limit: 20 };
|
|
233
|
+
let fetchCount = 0;
|
|
234
|
+
const source = newsProvider({
|
|
235
|
+
id: "poll-lifecycle",
|
|
236
|
+
name: "poll-lifecycle",
|
|
237
|
+
provider: {
|
|
238
|
+
fetchNews: async () => {
|
|
239
|
+
fetchCount++;
|
|
240
|
+
return [];
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
agg.register(source);
|
|
245
|
+
await agg.load(query);
|
|
246
|
+
expect(fetchCount).toBe(1);
|
|
247
|
+
|
|
248
|
+
agg.start();
|
|
249
|
+
agg.register(makeSource("inactive-trigger", []));
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
251
|
+
expect(fetchCount).toBe(1);
|
|
252
|
+
|
|
253
|
+
const dispose = agg.watchQuery(query, () => {});
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
255
|
+
expect(fetchCount).toBe(2);
|
|
256
|
+
|
|
257
|
+
const disposeSecond = agg.watchQuery(query, () => {});
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
259
|
+
expect(fetchCount).toBe(3);
|
|
260
|
+
|
|
261
|
+
agg.register(makeSource("active-trigger", []));
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
263
|
+
expect(fetchCount).toBe(4);
|
|
264
|
+
|
|
265
|
+
dispose();
|
|
266
|
+
agg.register(makeSource("single-watcher-trigger", []));
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
268
|
+
expect(fetchCount).toBe(5);
|
|
269
|
+
|
|
270
|
+
disposeSecond();
|
|
271
|
+
agg.register(makeSource("disposed-trigger", []));
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
273
|
+
expect(fetchCount).toBe(5);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("retains inactive query state for remounts", async () => {
|
|
277
|
+
const item = makeItem({ url: "https://remount.example.com/1" });
|
|
278
|
+
const query = { feed: "latest" as const, limit: 20 };
|
|
279
|
+
agg.register(makeSource("remount", [item]));
|
|
280
|
+
await agg.load(query);
|
|
281
|
+
|
|
282
|
+
const firstStates: string[][] = [];
|
|
283
|
+
const dispose = agg.watchQuery(query, (state) => {
|
|
284
|
+
firstStates.push(state.articles.map((article) => article.url));
|
|
285
|
+
});
|
|
286
|
+
dispose();
|
|
287
|
+
|
|
288
|
+
const remountedStates: string[][] = [];
|
|
289
|
+
const disposeRemount = agg.watchQuery(query, (state) => {
|
|
290
|
+
remountedStates.push(state.articles.map((article) => article.url));
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(firstStates[0]).toEqual([item.url]);
|
|
294
|
+
expect(remountedStates[0]).toEqual([item.url]);
|
|
295
|
+
disposeRemount();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("bounds inactive query state by LRU and TTL", async () => {
|
|
299
|
+
let now = 0;
|
|
300
|
+
agg = new NewsService({
|
|
301
|
+
inactiveQueryTtlMs: 10,
|
|
302
|
+
maxInactiveQueries: 2,
|
|
303
|
+
now: () => now,
|
|
304
|
+
});
|
|
305
|
+
agg.register(makeSource("bounded", [makeItem({ url: "https://bounded.example.com/1" })]));
|
|
306
|
+
const first = { feed: "latest" as const, topics: ["first"], limit: 20 };
|
|
307
|
+
const second = { feed: "latest" as const, topics: ["second"], limit: 20 };
|
|
308
|
+
const third = { feed: "latest" as const, topics: ["third"], limit: 20 };
|
|
309
|
+
|
|
310
|
+
await agg.load(first);
|
|
311
|
+
now = 1;
|
|
312
|
+
await agg.load(second);
|
|
313
|
+
now = 2;
|
|
314
|
+
await agg.load(third);
|
|
315
|
+
|
|
316
|
+
expect(agg.getQueryState(first).phase).toBe("idle");
|
|
317
|
+
expect(agg.getQueryState(third).phase).toBe("ready");
|
|
318
|
+
|
|
319
|
+
now = 20;
|
|
320
|
+
expect(agg.getQueryState(third).phase).toBe("idle");
|
|
321
|
+
});
|
|
322
|
+
|
|
227
323
|
it("seeds cached source items immediately on register", () => {
|
|
228
324
|
const cached = makeItem({ url: "https://cached.example.com/1", importance: 70 });
|
|
229
325
|
let callCount = 0;
|