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.
Files changed (351) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  274. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  275. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  276. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  277. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  278. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  279. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  280. package/src/renderers/electrobun/view/update-host.ts +1 -1
  281. package/src/renderers/opentui/chart-surface.tsx +2 -1
  282. package/src/renderers/opentui/dialog-host.tsx +20 -4
  283. package/src/renderers/opentui/image/loader.ts +12 -2
  284. package/src/renderers/opentui/input-host.tsx +7 -49
  285. package/src/renderers/opentui/start.tsx +2 -0
  286. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  287. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  288. package/src/sources/gloomberb-cloud/index.ts +42 -20
  289. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  290. package/src/sources/provider-router/cache.ts +1 -1
  291. package/src/sources/provider-router/document-routes.ts +10 -5
  292. package/src/sources/provider-router/financials.test.ts +14 -0
  293. package/src/sources/provider-router/financials.ts +6 -1
  294. package/src/sources/provider-router/history.test.ts +163 -1
  295. package/src/sources/provider-router/history.ts +278 -347
  296. package/src/sources/provider-router/index.ts +2 -2
  297. package/src/sources/provider-router/routing.test.ts +39 -0
  298. package/src/sources/provider-router/routing.ts +24 -11
  299. package/src/sources/provider-router/supplemental.ts +37 -17
  300. package/src/sources/sec-edgar/content.ts +2 -12
  301. package/src/sources/sec-edgar.ts +2 -13
  302. package/src/sources/yahoo-finance/history.ts +2 -2
  303. package/src/sources/yahoo-finance/options.ts +5 -0
  304. package/src/sources/yahoo-finance.ts +2 -2
  305. package/src/state/app/bootstrap.ts +2 -2
  306. package/src/state/app/context/index.tsx +2 -2
  307. package/src/state/hooks/quote-streaming.ts +66 -1
  308. package/src/state/hooks/session-snapshot.ts +2 -2
  309. package/src/state/selectors.ts +0 -9
  310. package/src/sync/controller.test.ts +98 -0
  311. package/src/sync/controller.ts +29 -5
  312. package/src/sync/react.ts +2 -2
  313. package/src/sync/types.ts +2 -2
  314. package/src/theme/colors.test.ts +10 -0
  315. package/src/theme/colors.ts +75 -78
  316. package/src/theme/theme-context.test.tsx +67 -0
  317. package/src/theme/theme-context.tsx +9 -5
  318. package/src/tickers/open-target.ts +2 -2
  319. package/src/tickers/search/upsert.ts +2 -2
  320. package/src/time-series/alignment.ts +0 -7
  321. package/src/time-series/chart-data.ts +145 -0
  322. package/src/time-series/date-window.ts +44 -0
  323. package/src/time-series/history-window.ts +21 -0
  324. package/src/time-series/hooks.test.ts +59 -0
  325. package/src/time-series/hooks.ts +33 -12
  326. package/src/time-series/market-resolution.ts +2 -2
  327. package/src/time-series/market.ts +1 -1
  328. package/src/time-series/range.ts +9 -0
  329. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  330. package/src/time-series/resolve.test.ts +203 -1
  331. package/src/time-series/resolve.ts +96 -18
  332. package/src/time-series/spec-studies.test.ts +15 -0
  333. package/src/time-series/spec.ts +2 -2
  334. package/src/time-series/studies.ts +5 -2
  335. package/src/time-series/types.ts +3 -1
  336. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  337. package/src/time-series/use-chart-resolution.ts +46 -4
  338. package/src/types/broker.ts +2 -2
  339. package/src/types/data-provider.ts +3 -3
  340. package/src/types/financials.ts +12 -0
  341. package/src/types/plugin.ts +2 -2
  342. package/src/ui/context-menu.tsx +3 -2
  343. package/src/ui/dialog.tsx +2 -2
  344. package/src/utils/html-entities.ts +3 -5
  345. package/src/utils/main-thread-monitor.ts +1 -1
  346. package/src/utils/price-history.ts +5 -5
  347. package/src/version.ts +3 -1
  348. package/src/components/chart/core/viewport.test.ts +0 -8
  349. package/src/components/chart/core/viewport.ts +0 -24
  350. package/src/time-series/index.ts +0 -14
  351. /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 "../../components/chart/core/types";
22
- import type { ChartResolutionSupport, ManualChartResolution } from "../../components/chart/core/resolution";
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 function makeRouterRevalidationKey(
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
- kind: string,
8
- ticker: string,
9
- context?: MarketDataRequestContext,
10
- extra?: string | number,
11
- ): string {
12
- return [
13
- kind,
14
- deps.getEntityKey(ticker, context?.instrument),
15
- extra != null ? String(extra) : "",
16
- ].join("|");
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
- makeRouterRevalidationKey,
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 entityKey = this.deps.getEntityKey(ticker, context?.instrument);
72
- const variantKeys = this.deps.getTickerVariantCandidates(exchange);
73
- const cached = selectCachedResource<HolderData>(this.deps.resources, "holders", entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
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, makeRouterRevalidationKey(this.deps, "holders", ticker, context), async () => {
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 entityKey = this.deps.getEntityKey(ticker, context?.instrument);
90
- const variantKeys = this.deps.getTickerVariantCandidates(exchange);
91
- const cached = this.selectCachedAnalystResearch(entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
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, makeRouterRevalidationKey(this.deps, "analystResearch", ticker, context), async () => {
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 entityKey = this.deps.getEntityKey(ticker, context?.instrument);
108
- const variantKeys = this.deps.getTickerVariantCandidates(exchange);
109
- const cached = selectCachedResource<CorporateActionsData>(this.deps.resources, "corporateActions", entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
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, makeRouterRevalidationKey(this.deps, "corporateActions", ticker, context), async () => {
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 entityKey = this.deps.getEntityKey(ticker, context?.instrument);
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
- buildVariantKey([["exchange", canonicalExchange(exchange)], ["expiration", expirationDate ?? "default"]]),
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, "options-chain", entityKey, variantKeys, sourceKeys, false);
200
+ const cached = selectCachedResource<OptionsChain>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, sourceKeys, false);
181
201
  if (cached) {
182
- scheduleRouterRevalidation(this.revalidationInFlight, makeRouterRevalidationKey(this.deps, "options-chain", ticker, context, expirationDate ?? "default"), async () => {
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(/&nbsp;/gi, " ")
17
- .replace(/&amp;/gi, "&")
18
- .replace(/&lt;/gi, "<")
19
- .replace(/&gt;/gi, ">")
20
- .replace(/&quot;/gi, "\"")
21
- .replace(/&#39;|&#x27;/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, "")
@@ -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(/&nbsp;/gi, " ")
167
- .replace(/&amp;/gi, "&")
168
- .replace(/&lt;/gi, "<")
169
- .replace(/&gt;/gi, ">")
170
- .replace(/&quot;/gi, "\"")
171
- .replace(/&#39;|&#x27;/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 decodeSecHtmlEntities(
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 "../../components/chart/core/types";
1
+ import type { TimeRange } from "../../time-series/range";
2
2
  import {
3
3
  normalizeChartResolutionSupport,
4
4
  type ChartResolutionSupport,
5
5
  type ManualChartResolution,
6
- } from "../../components/chart/core/resolution";
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 "../components/chart/core/types";
3
+ import type { TimeRange } from "../time-series/range";
4
4
  import {
5
5
  type ChartResolutionSupport,
6
6
  type ManualChartResolution,
7
- } from "../components/chart/core/resolution";
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 { TickerRepository } from "../../data/ticker-repository";
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: 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 { SessionStore } from "../../../data/session-store";
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?: 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 { SessionStore } from "../../data/session-store";
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: SessionStore | undefined,
15
+ sessionStore: AppSessionStorePort | undefined,
16
16
  state: AppState,
17
17
  sessionId: string,
18
18
  schemaVersion: number,
@@ -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) => {
@@ -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 { TickerRepository } from "../data/ticker-repository";
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: 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 payload = response.snapshot.contributors[entry.contributor.id]?.payload;
229
- if (payload === undefined || !entry.contributor.apply) continue;
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 { TickerRepository } from "../data/ticker-repository";
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: 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 { TickerRepository } from "../data/ticker-repository";
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: TickerRepository;
33
+ tickerRepository: AppTickerRepositoryPort;
34
34
  }
35
35
 
36
36
  export interface SyncContributor {