gloomberb 0.10.1 → 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/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
|
@@ -18,8 +18,8 @@ import { routeSourcePriority } from "../../types/capability-route-source";
|
|
|
18
18
|
import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
19
19
|
import type { NewsArticle, NewsQuery } from "../../news/types";
|
|
20
20
|
import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
|
|
21
|
-
import type { TimeRange } from "../../
|
|
22
|
-
import type { ChartResolutionSupport, ManualChartResolution } from "../../
|
|
21
|
+
import type { TimeRange } from "../../time-series/range";
|
|
22
|
+
import type { ChartResolutionSupport, ManualChartResolution } from "../../time-series/resolution";
|
|
23
23
|
import { debugLog } from "../../utils/debug-log";
|
|
24
24
|
import { ProviderRouterBatchRoutes } from "./batches";
|
|
25
25
|
import { ProviderRouterDocumentRoutes } from "./document-routes";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getRouterEntityKey } from "./cache";
|
|
3
|
+
import { makeRouterRequestIdentity } from "./routing";
|
|
4
|
+
|
|
5
|
+
const deps = { getEntityKey: getRouterEntityKey };
|
|
6
|
+
|
|
7
|
+
describe("router request identity", () => {
|
|
8
|
+
test("uses the same canonical variant for cache and revalidation identity", () => {
|
|
9
|
+
const identity = makeRouterRequestIdentity(deps, {
|
|
10
|
+
kind: "options-chain",
|
|
11
|
+
ticker: "aapl",
|
|
12
|
+
variantParts: [["exchange", "NASDAQ"], ["expiration", 1_800_000_000]],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(identity).toEqual({
|
|
16
|
+
kind: "options-chain",
|
|
17
|
+
entityKey: "AAPL",
|
|
18
|
+
variantKey: "exchange=NASDAQ;expiration=1800000000",
|
|
19
|
+
revalidationKey: "options-chain|AAPL|exchange=NASDAQ;expiration=1800000000",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("does not collapse concurrent exchange variants", () => {
|
|
24
|
+
const nasdaq = makeRouterRequestIdentity(deps, {
|
|
25
|
+
kind: "holders",
|
|
26
|
+
ticker: "shop",
|
|
27
|
+
variantParts: [["exchange", "NASDAQ"]],
|
|
28
|
+
});
|
|
29
|
+
const tsx = makeRouterRequestIdentity(deps, {
|
|
30
|
+
kind: "holders",
|
|
31
|
+
ticker: "shop",
|
|
32
|
+
variantParts: [["exchange", "TSX"]],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(nasdaq.entityKey).toBe(tsx.entityKey);
|
|
36
|
+
expect(nasdaq.variantKey).not.toBe(tsx.variantKey);
|
|
37
|
+
expect(nasdaq.revalidationKey).not.toBe(tsx.revalidationKey);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import type { DataProvider, MarketDataRequestContext } from "../../types/data-provider";
|
|
2
2
|
import { shouldLogProviderError } from "../provider-errors";
|
|
3
3
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
4
|
+
import { buildVariantKey } from "./cache";
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export interface RouterRequestIdentity {
|
|
7
|
+
kind: string;
|
|
8
|
+
entityKey: string;
|
|
9
|
+
variantKey: string;
|
|
10
|
+
revalidationKey: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function makeRouterRequestIdentity(
|
|
6
14
|
deps: Pick<ProviderRouterCoreDeps, "getEntityKey">,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
input: {
|
|
16
|
+
kind: string;
|
|
17
|
+
ticker: string;
|
|
18
|
+
context?: MarketDataRequestContext;
|
|
19
|
+
variantParts?: Array<[string, string | number | undefined | null]>;
|
|
20
|
+
},
|
|
21
|
+
): RouterRequestIdentity {
|
|
22
|
+
const entityKey = deps.getEntityKey(input.ticker, input.context?.instrument);
|
|
23
|
+
const variantKey = buildVariantKey(input.variantParts ?? []);
|
|
24
|
+
return {
|
|
25
|
+
kind: input.kind,
|
|
26
|
+
entityKey,
|
|
27
|
+
variantKey,
|
|
28
|
+
revalidationKey: [input.kind, entityKey, variantKey].join("|"),
|
|
29
|
+
};
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export function scheduleRouterRevalidation(
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
26
26
|
import {
|
|
27
27
|
firstProviderResult,
|
|
28
|
-
|
|
28
|
+
makeRouterRequestIdentity,
|
|
29
29
|
resolveProviderBySourceKey,
|
|
30
30
|
scheduleRouterRevalidation,
|
|
31
31
|
} from "./routing";
|
|
@@ -68,12 +68,17 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async getHolders(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<HolderData> {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const identity = makeRouterRequestIdentity(this.deps, {
|
|
72
|
+
kind: "holders",
|
|
73
|
+
ticker,
|
|
74
|
+
context,
|
|
75
|
+
variantParts: [["exchange", canonicalExchange(exchange)]],
|
|
76
|
+
});
|
|
77
|
+
const variantKeys = [...new Set([identity.variantKey, ""])];
|
|
78
|
+
const cached = selectCachedResource<HolderData>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
|
|
74
79
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
75
80
|
if (cached && !forceRefresh) {
|
|
76
|
-
scheduleRouterRevalidation(this.revalidationInFlight,
|
|
81
|
+
scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
|
|
77
82
|
await this.revalidateHolders(ticker, exchange, context);
|
|
78
83
|
});
|
|
79
84
|
return cached.value;
|
|
@@ -86,12 +91,17 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
async getAnalystResearch(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<AnalystResearchData> {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const identity = makeRouterRequestIdentity(this.deps, {
|
|
95
|
+
kind: "analystResearch",
|
|
96
|
+
ticker,
|
|
97
|
+
context,
|
|
98
|
+
variantParts: [["exchange", canonicalExchange(exchange)]],
|
|
99
|
+
});
|
|
100
|
+
const variantKeys = [...new Set([identity.variantKey, ""])];
|
|
101
|
+
const cached = this.selectCachedAnalystResearch(identity.entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
|
|
92
102
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
93
103
|
if (cached && !forceRefresh && !isAnalystResearchMissingRatingTargets(cached.value)) {
|
|
94
|
-
scheduleRouterRevalidation(this.revalidationInFlight,
|
|
104
|
+
scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
|
|
95
105
|
await this.revalidateAnalystResearch(ticker, exchange, context);
|
|
96
106
|
});
|
|
97
107
|
return cached.value;
|
|
@@ -104,12 +114,17 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
async getCorporateActions(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<CorporateActionsData> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const identity = makeRouterRequestIdentity(this.deps, {
|
|
118
|
+
kind: "corporateActions",
|
|
119
|
+
ticker,
|
|
120
|
+
context,
|
|
121
|
+
variantParts: [["exchange", canonicalExchange(exchange)]],
|
|
122
|
+
});
|
|
123
|
+
const variantKeys = [...new Set([identity.variantKey, ""])];
|
|
124
|
+
const cached = selectCachedResource<CorporateActionsData>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
|
|
110
125
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
111
126
|
if (cached && !forceRefresh) {
|
|
112
|
-
scheduleRouterRevalidation(this.revalidationInFlight,
|
|
127
|
+
scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
|
|
113
128
|
await this.revalidateCorporateActions(ticker, exchange, context);
|
|
114
129
|
});
|
|
115
130
|
return cached.value;
|
|
@@ -167,9 +182,14 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain> {
|
|
170
|
-
const
|
|
185
|
+
const identity = makeRouterRequestIdentity(this.deps, {
|
|
186
|
+
kind: "options-chain",
|
|
187
|
+
ticker,
|
|
188
|
+
context,
|
|
189
|
+
variantParts: [["exchange", canonicalExchange(exchange)], ["expiration", expirationDate ?? "default"]],
|
|
190
|
+
});
|
|
171
191
|
const variantKeys = [
|
|
172
|
-
|
|
192
|
+
identity.variantKey,
|
|
173
193
|
buildVariantKey([["expiration", expirationDate ?? "default"]]),
|
|
174
194
|
"",
|
|
175
195
|
];
|
|
@@ -177,9 +197,9 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
177
197
|
...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
|
|
178
198
|
...this.deps.getProviderSourceKeys(),
|
|
179
199
|
];
|
|
180
|
-
const cached = selectCachedResource<OptionsChain>(this.deps.resources,
|
|
200
|
+
const cached = selectCachedResource<OptionsChain>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, sourceKeys, false);
|
|
181
201
|
if (cached) {
|
|
182
|
-
scheduleRouterRevalidation(this.revalidationInFlight,
|
|
202
|
+
scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
|
|
183
203
|
await this.revalidateOptionsChain(ticker, exchange, expirationDate, context);
|
|
184
204
|
});
|
|
185
205
|
return cached.value;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { decodeHtmlEntities } from "../../utils/html-entities";
|
|
2
|
+
|
|
1
3
|
const MAX_CONTENT_CHARS = 16_000;
|
|
2
4
|
const OWNERSHIP_FORMS = new Set(["3", "3/A", "4", "4/A", "5", "5/A"]);
|
|
3
5
|
|
|
@@ -11,18 +13,6 @@ export function isPdfDocument(body: string, contentType = "", url = ""): boolean
|
|
|
11
13
|
|| /<PDF>/i.test(body);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function decodeHtmlEntities(text: string): string {
|
|
15
|
-
return text
|
|
16
|
-
.replace(/ /gi, " ")
|
|
17
|
-
.replace(/&/gi, "&")
|
|
18
|
-
.replace(/</gi, "<")
|
|
19
|
-
.replace(/>/gi, ">")
|
|
20
|
-
.replace(/"/gi, "\"")
|
|
21
|
-
.replace(/'|'/gi, "'")
|
|
22
|
-
.replace(/&#(\d+);/g, (_match, digits: string) => String.fromCodePoint(Number(digits)))
|
|
23
|
-
.replace(/&#x([0-9a-f]+);/gi, (_match, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
function collapseDocumentLines(text: string): string[] {
|
|
27
17
|
const rawLines = text
|
|
28
18
|
.replace(/\r/g, "")
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SecFilingDocument, SecFilingItem } from "../types/data-provider";
|
|
2
2
|
import type { FinancialStatement } from "../types/financials";
|
|
3
3
|
import { truncateWithEllipsis } from "../utils/text-wrap";
|
|
4
|
+
import { decodeHtmlEntities } from "../utils/html-entities";
|
|
4
5
|
import {
|
|
5
6
|
PDF_FALLBACK_MESSAGE,
|
|
6
7
|
extractFilingContent,
|
|
@@ -161,20 +162,8 @@ function normalizeDocumentName(value: string | undefined): string {
|
|
|
161
162
|
return (value ?? "").trim().toLowerCase();
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
function decodeSecHtmlEntities(text: string): string {
|
|
165
|
-
return text
|
|
166
|
-
.replace(/ /gi, " ")
|
|
167
|
-
.replace(/&/gi, "&")
|
|
168
|
-
.replace(/</gi, "<")
|
|
169
|
-
.replace(/>/gi, ">")
|
|
170
|
-
.replace(/"/gi, "\"")
|
|
171
|
-
.replace(/'|'/gi, "'")
|
|
172
|
-
.replace(/&#(\d+);/g, (_match, digits: string) => String.fromCodePoint(Number(digits)))
|
|
173
|
-
.replace(/&#x([0-9a-f]+);/gi, (_match, hex: string) => String.fromCodePoint(parseInt(hex, 16)));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
165
|
function cleanSecTableText(value: string): string {
|
|
177
|
-
return
|
|
166
|
+
return decodeHtmlEntities(
|
|
178
167
|
value
|
|
179
168
|
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
180
169
|
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TimeRange } from "../../
|
|
1
|
+
import type { TimeRange } from "../../time-series/range";
|
|
2
2
|
import {
|
|
3
3
|
normalizeChartResolutionSupport,
|
|
4
4
|
type ChartResolutionSupport,
|
|
5
5
|
type ManualChartResolution,
|
|
6
|
-
} from "../../
|
|
6
|
+
} from "../../time-series/resolution";
|
|
7
7
|
import type { PricePoint } from "../../types/financials";
|
|
8
8
|
import { normalizeSubUnitCurrency } from "./mappers";
|
|
9
9
|
import { getYahooSymbolsToTry } from "./symbols";
|
|
@@ -105,6 +105,11 @@ export async function loadYahooOptionsChainResult({
|
|
|
105
105
|
expirationDates: result.expirationDates ?? [],
|
|
106
106
|
calls: (opts?.calls ?? []).map((contract) => mapYahooOptionContract(contract)),
|
|
107
107
|
puts: (opts?.puts ?? []).map((contract) => mapYahooOptionContract(contract)),
|
|
108
|
+
dataSource: "delayed",
|
|
109
|
+
feed: "yahoo",
|
|
110
|
+
delayMinutes: 15,
|
|
111
|
+
realtimeEligible: false,
|
|
112
|
+
asOf: new Date().toISOString(),
|
|
108
113
|
},
|
|
109
114
|
underlyingMarketState: normalizeYahooMarketState(result.quote?.marketState),
|
|
110
115
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Quote, PricePoint, TickerFinancials, OptionsChain, CompanyProfile, HolderData, AnalystResearchData, CorporateActionsData } from "../types/financials";
|
|
2
2
|
import type { DataProvider, EarningsEvent, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
|
|
3
|
-
import type { TimeRange } from "../
|
|
3
|
+
import type { TimeRange } from "../time-series/range";
|
|
4
4
|
import {
|
|
5
5
|
type ChartResolutionSupport,
|
|
6
6
|
type ManualChartResolution,
|
|
7
|
-
} from "../
|
|
7
|
+
} from "../time-series/resolution";
|
|
8
8
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
9
9
|
import { parseOptionSymbol } from "../utils/options";
|
|
10
10
|
import { SecEdgarClient } from "./sec-edgar";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Dispatch } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
3
3
|
import { findPaneInstance, isTickerPaneId, type AppConfig } from "../../types/config";
|
|
4
4
|
import type { CachedFinancialsTarget, DataProvider } from "../../types/data-provider";
|
|
5
5
|
import type { TickerFinancials } from "../../types/financials";
|
|
@@ -47,7 +47,7 @@ const startupLog = debugLog.createLogger("startup");
|
|
|
47
47
|
|
|
48
48
|
export interface InitializeAppStateArgs {
|
|
49
49
|
config: AppConfig;
|
|
50
|
-
tickerRepository:
|
|
50
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
51
51
|
dataProvider: DataProvider;
|
|
52
52
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
53
53
|
dispatch: Dispatch<AppAction>;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type SetStateAction,
|
|
13
13
|
type ReactNode,
|
|
14
14
|
} from "react";
|
|
15
|
-
import type {
|
|
15
|
+
import type { AppSessionStorePort } from "../../../core/app-service-ports";
|
|
16
16
|
import { ThemeProvider } from "../../../theme/theme-context";
|
|
17
17
|
import {
|
|
18
18
|
findPaneInstance,
|
|
@@ -333,7 +333,7 @@ export function AppProvider({
|
|
|
333
333
|
}: {
|
|
334
334
|
config: AppConfig;
|
|
335
335
|
children: ReactNode;
|
|
336
|
-
sessionStore?:
|
|
336
|
+
sessionStore?: AppSessionStorePort;
|
|
337
337
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
338
338
|
desktopBridge?: DesktopWindowBridge;
|
|
339
339
|
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useAppActive } from "../app/activity";
|
|
3
3
|
import type { QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
4
|
+
import type { Quote } from "../../types/financials";
|
|
4
5
|
import { debugLog } from "../../utils/debug-log";
|
|
5
6
|
import { normalizeSymbol } from "../../utils/exchanges";
|
|
6
7
|
import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
8
|
+
import { useQuoteEntries } from "../../market-data/hooks";
|
|
9
|
+
import type { InstrumentRef } from "../../market-data/request-types";
|
|
10
|
+
import type { QueryEntry } from "../../market-data/result-types";
|
|
11
|
+
import { buildQuoteKey } from "../../market-data/selectors";
|
|
7
12
|
|
|
8
13
|
const quoteStreamLog = debugLog.createLogger("quote-stream");
|
|
9
14
|
|
|
@@ -91,3 +96,63 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
|
|
|
91
96
|
};
|
|
92
97
|
}, [appActive, coordinator, subscriptionKey]);
|
|
93
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe to a bounded target set and observe the coordinator entries filled
|
|
102
|
+
* by that stream. This intentionally avoids per-symbol HTTP quote loads.
|
|
103
|
+
*/
|
|
104
|
+
export function useLiveQuoteEntries(targets: QuoteSubscriptionTarget[]): {
|
|
105
|
+
entries: Map<string, QueryEntry<Quote>>;
|
|
106
|
+
freshnessNow: number;
|
|
107
|
+
subscriptionStartedAt: number;
|
|
108
|
+
} {
|
|
109
|
+
const appActive = useAppActive();
|
|
110
|
+
const normalizedTargets = targets.flatMap((target) => {
|
|
111
|
+
const normalized = normalizeQuoteStreamSubscriptionTarget(target);
|
|
112
|
+
return normalized ? [normalized] : [];
|
|
113
|
+
});
|
|
114
|
+
const targetKey = normalizedTargets
|
|
115
|
+
.map((target) => buildQuoteStreamSubscriptionKey(target))
|
|
116
|
+
.sort()
|
|
117
|
+
.join("\u001f");
|
|
118
|
+
const subscriptionKey = `${appActive ? "active" : "inactive"}\u001f${targetKey}`;
|
|
119
|
+
const subscriptionRef = useRef({
|
|
120
|
+
key: subscriptionKey,
|
|
121
|
+
startedAt: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
if (subscriptionRef.current.key !== subscriptionKey) {
|
|
124
|
+
subscriptionRef.current = {
|
|
125
|
+
key: subscriptionKey,
|
|
126
|
+
startedAt: Date.now(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const [freshnessNow, setFreshnessNow] = useState(() => Date.now());
|
|
130
|
+
const instruments = useMemo(() => {
|
|
131
|
+
const unique = new Map<string, InstrumentRef>();
|
|
132
|
+
for (const target of normalizedTargets) {
|
|
133
|
+
const instrument: InstrumentRef = {
|
|
134
|
+
symbol: target.symbol,
|
|
135
|
+
exchange: target.exchange,
|
|
136
|
+
brokerId: target.context?.brokerId,
|
|
137
|
+
brokerInstanceId: target.context?.brokerInstanceId,
|
|
138
|
+
instrument: target.context?.instrument ?? null,
|
|
139
|
+
};
|
|
140
|
+
unique.set(buildQuoteKey(instrument), instrument);
|
|
141
|
+
}
|
|
142
|
+
return [...unique.values()];
|
|
143
|
+
}, [targetKey]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
setFreshnessNow(Date.now());
|
|
147
|
+
if (!appActive || normalizedTargets.length === 0) return;
|
|
148
|
+
const interval = setInterval(() => setFreshnessNow(Date.now()), 15_000);
|
|
149
|
+
return () => clearInterval(interval);
|
|
150
|
+
}, [appActive, targetKey]);
|
|
151
|
+
|
|
152
|
+
useQuoteStreaming(targets);
|
|
153
|
+
return {
|
|
154
|
+
entries: useQuoteEntries(instruments),
|
|
155
|
+
freshnessNow,
|
|
156
|
+
subscriptionStartedAt: subscriptionRef.current.startedAt,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AppSessionStorePort } from "../../core/app-service-ports";
|
|
3
3
|
import {
|
|
4
4
|
buildAppSessionSnapshot,
|
|
5
5
|
type AppSessionSnapshot,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../persist-scheduler";
|
|
13
13
|
|
|
14
14
|
export function usePersistSessionSnapshot(
|
|
15
|
-
sessionStore:
|
|
15
|
+
sessionStore: AppSessionStorePort | undefined,
|
|
16
16
|
state: AppState,
|
|
17
17
|
sessionId: string,
|
|
18
18
|
schemaVersion: number,
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AppState } from "./app/context";
|
|
2
|
-
import type { TickerRecord } from "../types/ticker";
|
|
3
2
|
|
|
4
3
|
function getCollectionMembershipKey(state: AppState, collectionId: string | null): "portfolios" | "watchlists" | null {
|
|
5
4
|
if (!collectionId) return null;
|
|
@@ -8,14 +7,6 @@ function getCollectionMembershipKey(state: AppState, collectionId: string | null
|
|
|
8
7
|
return null;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export function getCollectionTickers(state: AppState, collectionId: string | null): TickerRecord[] {
|
|
12
|
-
const membershipKey = getCollectionMembershipKey(state, collectionId);
|
|
13
|
-
if (!collectionId || !membershipKey) return [];
|
|
14
|
-
return [...state.tickers.values()]
|
|
15
|
-
.filter((ticker) => ticker.metadata[membershipKey].includes(collectionId))
|
|
16
|
-
.sort((a, b) => a.metadata.ticker.localeCompare(b.metadata.ticker));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
export function getCollectionTickerCount(state: AppState, collectionId: string | null): number {
|
|
20
11
|
const membershipKey = getCollectionMembershipKey(state, collectionId);
|
|
21
12
|
if (!collectionId || !membershipKey) return 0;
|
|
@@ -46,6 +46,104 @@ test("does not push local state when the initial pull fails", async () => {
|
|
|
46
46
|
expect(controller.getStatus()).toMatchObject({ phase: "error", error: "pull failed" });
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test("rejects incompatible snapshot versions before applying or pushing", async () => {
|
|
50
|
+
let applied = 0;
|
|
51
|
+
let pushes = 0;
|
|
52
|
+
const contributor: SyncContributor = {
|
|
53
|
+
id: "test.settings",
|
|
54
|
+
schemaVersion: 2,
|
|
55
|
+
collect: () => ({ local: true }),
|
|
56
|
+
apply: () => {
|
|
57
|
+
applied += 1;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const snapshot = {
|
|
61
|
+
schemaVersion: SYNC_SNAPSHOT_SCHEMA_VERSION + 1,
|
|
62
|
+
appId: "gloomberb",
|
|
63
|
+
clientId: "future-client",
|
|
64
|
+
createdAt: "2026-07-26T00:00:00.000Z",
|
|
65
|
+
contributors: {},
|
|
66
|
+
} as unknown as SyncSnapshot;
|
|
67
|
+
const transport: SyncTransport = {
|
|
68
|
+
id: "future-snapshot",
|
|
69
|
+
isAvailable: () => true,
|
|
70
|
+
pullSnapshot: async () => ({ snapshot, revision: 1, updatedAt: snapshot.createdAt }),
|
|
71
|
+
pushSnapshot: async () => {
|
|
72
|
+
pushes += 1;
|
|
73
|
+
return { revision: 2, updatedAt: snapshot.createdAt };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const controller = new CloudSyncController();
|
|
77
|
+
controller.setRuntime({
|
|
78
|
+
getState: () => ({} as AppState),
|
|
79
|
+
dispatch: () => {},
|
|
80
|
+
tickerRepository: {} as TickerRepository,
|
|
81
|
+
getContributors: () => [{ pluginId: "test", contributor }],
|
|
82
|
+
getTransport: () => ({ pluginId: "test", transport }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await controller.requestSync({ force: true });
|
|
86
|
+
|
|
87
|
+
expect(applied).toBe(0);
|
|
88
|
+
expect(pushes).toBe(0);
|
|
89
|
+
expect(controller.getStatus()).toMatchObject({
|
|
90
|
+
phase: "error",
|
|
91
|
+
error: `Unsupported sync snapshot schema version ${SYNC_SNAPSHOT_SCHEMA_VERSION + 1}; expected ${SYNC_SNAPSHOT_SCHEMA_VERSION}`,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("rejects incompatible contributor versions before applying or pushing", async () => {
|
|
96
|
+
let applied = 0;
|
|
97
|
+
let pushes = 0;
|
|
98
|
+
const contributor: SyncContributor = {
|
|
99
|
+
id: "test.settings",
|
|
100
|
+
schemaVersion: 2,
|
|
101
|
+
collect: () => ({ local: true }),
|
|
102
|
+
apply: () => {
|
|
103
|
+
applied += 1;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
const snapshot: SyncSnapshot = {
|
|
107
|
+
schemaVersion: SYNC_SNAPSHOT_SCHEMA_VERSION,
|
|
108
|
+
appId: "gloomberb",
|
|
109
|
+
clientId: "old-client",
|
|
110
|
+
createdAt: "2026-07-26T00:00:00.000Z",
|
|
111
|
+
contributors: {
|
|
112
|
+
"test.settings": {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
updatedAt: "2026-07-26T00:00:00.000Z",
|
|
115
|
+
payload: { remote: true },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const transport: SyncTransport = {
|
|
120
|
+
id: "old-contributor",
|
|
121
|
+
isAvailable: () => true,
|
|
122
|
+
pullSnapshot: async () => ({ snapshot, revision: 1, updatedAt: snapshot.createdAt }),
|
|
123
|
+
pushSnapshot: async () => {
|
|
124
|
+
pushes += 1;
|
|
125
|
+
return { revision: 2, updatedAt: snapshot.createdAt };
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const controller = new CloudSyncController();
|
|
129
|
+
controller.setRuntime({
|
|
130
|
+
getState: () => ({} as AppState),
|
|
131
|
+
dispatch: () => {},
|
|
132
|
+
tickerRepository: {} as TickerRepository,
|
|
133
|
+
getContributors: () => [{ pluginId: "test", contributor }],
|
|
134
|
+
getTransport: () => ({ pluginId: "test", transport }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await controller.requestSync({ force: true });
|
|
138
|
+
|
|
139
|
+
expect(applied).toBe(0);
|
|
140
|
+
expect(pushes).toBe(0);
|
|
141
|
+
expect(controller.getStatus()).toMatchObject({
|
|
142
|
+
phase: "error",
|
|
143
|
+
error: "Unsupported sync contributor schema for test.settings: 1; expected 2",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
49
147
|
test("keeps startup layout changes while serializing pull and push", async () => {
|
|
50
148
|
let state = createInitialState(createDefaultConfig("/tmp/gloomberb-sync-controller-test"));
|
|
51
149
|
const dispatch = (action: AppAction) => {
|
package/src/sync/controller.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Dispatch } from "react";
|
|
2
2
|
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
4
4
|
import { stableStringify } from "../remote/revision";
|
|
5
5
|
import {
|
|
6
6
|
SYNC_SNAPSHOT_SCHEMA_VERSION,
|
|
@@ -25,7 +25,7 @@ export interface CloudSyncStatus {
|
|
|
25
25
|
interface SyncRuntime {
|
|
26
26
|
getState: () => AppState;
|
|
27
27
|
dispatch: Dispatch<AppAction>;
|
|
28
|
-
tickerRepository:
|
|
28
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
29
29
|
getContributors: () => RegisteredSyncContributor[];
|
|
30
30
|
getTransport: () => RegisteredSyncTransport | null;
|
|
31
31
|
}
|
|
@@ -223,11 +223,12 @@ export class CloudSyncController {
|
|
|
223
223
|
if (!this.isCurrent(runtime, transport)) return false;
|
|
224
224
|
|
|
225
225
|
if (response.snapshot) {
|
|
226
|
+
this.assertSnapshotCompatible(response.snapshot, runtime.getContributors());
|
|
226
227
|
for (const entry of runtime.getContributors()) {
|
|
227
228
|
if (!this.isCurrent(runtime, transport)) return false;
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
await entry.contributor.apply(payload, {
|
|
229
|
+
const contributorPayload = response.snapshot.contributors[entry.contributor.id];
|
|
230
|
+
if (!contributorPayload || !entry.contributor.apply) continue;
|
|
231
|
+
await entry.contributor.apply(contributorPayload.payload, {
|
|
231
232
|
snapshot: response.snapshot,
|
|
232
233
|
baselineState,
|
|
233
234
|
state: runtime.getState(),
|
|
@@ -260,6 +261,29 @@ export class CloudSyncController {
|
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
private assertSnapshotCompatible(
|
|
265
|
+
snapshot: SyncSnapshot,
|
|
266
|
+
contributors: RegisteredSyncContributor[],
|
|
267
|
+
): void {
|
|
268
|
+
if (snapshot.appId !== "gloomberb") {
|
|
269
|
+
throw new Error(`Unsupported sync snapshot app: ${String(snapshot.appId)}`);
|
|
270
|
+
}
|
|
271
|
+
if (snapshot.schemaVersion !== SYNC_SNAPSHOT_SCHEMA_VERSION) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Unsupported sync snapshot schema version ${String(snapshot.schemaVersion)}; expected ${SYNC_SNAPSHOT_SCHEMA_VERSION}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
for (const entry of contributors) {
|
|
277
|
+
const payload = snapshot.contributors[entry.contributor.id];
|
|
278
|
+
if (!payload || !entry.contributor.apply) continue;
|
|
279
|
+
if (payload.schemaVersion !== entry.contributor.schemaVersion) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Unsupported sync contributor schema for ${entry.contributor.id}: ${payload.schemaVersion}; expected ${entry.contributor.schemaVersion}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
263
287
|
private async assembleSnapshot(runtime: SyncRuntime): Promise<SyncSnapshot> {
|
|
264
288
|
const contributors = runtime.getContributors();
|
|
265
289
|
const createdAt = nowIso();
|
package/src/sync/react.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useSyncExternalStore, type Dispatch } from "react";
|
|
2
2
|
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
4
4
|
import type { PluginRegistry } from "../plugins/registry";
|
|
5
5
|
import { cloudSyncController } from "./controller";
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ interface CloudSyncRuntimeOptions {
|
|
|
8
8
|
state: AppState;
|
|
9
9
|
getState: () => AppState;
|
|
10
10
|
dispatch: Dispatch<AppAction>;
|
|
11
|
-
tickerRepository:
|
|
11
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
12
12
|
pluginRegistry: PluginRegistry;
|
|
13
13
|
initialized: boolean;
|
|
14
14
|
}
|
package/src/sync/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Dispatch } from "react";
|
|
2
2
|
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
4
4
|
|
|
5
5
|
export const SYNC_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
6
6
|
|
|
@@ -30,7 +30,7 @@ export interface SyncApplyContext {
|
|
|
30
30
|
getState: () => AppState;
|
|
31
31
|
isCurrent: () => boolean;
|
|
32
32
|
dispatch: Dispatch<AppAction>;
|
|
33
|
-
tickerRepository:
|
|
33
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface SyncContributor {
|