gloomberb 0.18.4 → 0.19.0

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 (600) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +169 -5
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/cot.ts +6 -2
  9. package/src/api-client/credit-documents.ts +6 -0
  10. package/src/api-client/data.ts +31 -6
  11. package/src/api-client/equity-screener.ts +24 -0
  12. package/src/api-client/fund-facts.ts +45 -4
  13. package/src/api-client/index.ts +171 -11
  14. package/src/api-client/live-scan.ts +91 -0
  15. package/src/api-client/market-discovery.ts +10 -0
  16. package/src/api-client/market-movers.ts +7 -0
  17. package/src/api-client/members.ts +13 -1
  18. package/src/api-client/perps.ts +43 -0
  19. package/src/api-client/request.ts +222 -41
  20. package/src/api-client/research-activity.ts +6 -12
  21. package/src/api-client/types.ts +74 -0
  22. package/src/app/keybindings/index.ts +3 -0
  23. package/src/app/keybindings/labels.ts +18 -1
  24. package/src/app/keybindings/resolve.ts +14 -0
  25. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  26. package/src/app/runtime/broker-import.ts +42 -27
  27. package/src/app/runtime/plugin-bindings.ts +73 -56
  28. package/src/architecture/core-builtin-allowlist.json +1 -1
  29. package/src/architecture/pane-tabs-allowlist.json +3 -0
  30. package/src/architecture/ttl-caches-allowlist.json +19 -0
  31. package/src/brokers/sync-broker-instance.ts +2 -8
  32. package/src/capabilities/chart-series.ts +1 -1
  33. package/src/cli/commands/crypto-hints.ts +40 -24
  34. package/src/cli/commands/fred-series.ts +5 -0
  35. package/src/cli/commands/keybindings.ts +23 -0
  36. package/src/cli/commands/market.ts +138 -46
  37. package/src/cli/commands/options-chain.ts +107 -10
  38. package/src/cli/commands/options-leaps.ts +9 -3
  39. package/src/cli/commands/overview.ts +176 -39
  40. package/src/cli/commands/search.ts +39 -3
  41. package/src/cli/commands/themes.ts +11 -2
  42. package/src/cli/commands/ticker.ts +57 -35
  43. package/src/cli/company-name-hint.ts +41 -0
  44. package/src/cli/earnings-calendar.ts +1 -1
  45. package/src/cli/errors.ts +10 -2
  46. package/src/cli/export-source.ts +29 -1
  47. package/src/cli/glossary.ts +73 -1
  48. package/src/cli/history-rows.ts +87 -16
  49. package/src/cli/index.ts +2 -0
  50. package/src/cli/known-gaps.ts +5 -2
  51. package/src/cli/listing-arg.ts +195 -56
  52. package/src/cli/output-file.ts +68 -0
  53. package/src/cli/pane-functions/capabilities.ts +3 -0
  54. package/src/cli/pane-functions/catalog.ts +80 -7
  55. package/src/cli/pane-functions/dom.ts +13 -3
  56. package/src/cli/pane-functions/freshness.ts +111 -16
  57. package/src/cli/pane-functions/function-help.ts +23 -13
  58. package/src/cli/pane-functions/headless.ts +56 -12
  59. package/src/cli/pane-functions/index.ts +58 -16
  60. package/src/cli/pane-functions/market-session.ts +81 -9
  61. package/src/cli/pane-functions/options.ts +19 -1
  62. package/src/cli/pane-functions/report.ts +5 -1
  63. package/src/cli/pane-functions/resolver.ts +35 -8
  64. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  65. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  66. package/src/cli/quote-answer.ts +30 -13
  67. package/src/cli/report-tables.ts +18 -7
  68. package/src/cli/restore-plugins.ts +13 -1
  69. package/src/cli/result.ts +57 -5
  70. package/src/cli/ticker-resolution.ts +3 -2
  71. package/src/components/chart/composite/composite-chart.tsx +26 -4
  72. package/src/components/chart/composite/format.ts +27 -0
  73. package/src/components/chart/composite/interactions.ts +6 -0
  74. package/src/components/chart/composite/legend.tsx +45 -7
  75. package/src/components/chart/composite/levels.ts +2 -0
  76. package/src/components/chart/composite/panel-gestures.ts +33 -0
  77. package/src/components/chart/composite/panel-series.ts +1 -0
  78. package/src/components/chart/composite/panel-surface.tsx +61 -5
  79. package/src/components/chart/composite/range-statistics.ts +62 -0
  80. package/src/components/chart/composite/rasterizer.ts +24 -13
  81. package/src/components/chart/composite/scene.ts +46 -7
  82. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  83. package/src/components/chart/composite/series-cues.ts +14 -0
  84. package/src/components/chart/composite/text-renderer.ts +9 -2
  85. package/src/components/chart/composite/tool-catalog.ts +36 -0
  86. package/src/components/chart/composite/tools.ts +203 -44
  87. package/src/components/chart/composite/types.ts +4 -0
  88. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  89. package/src/components/command-bar/assist/inventory.ts +2 -1
  90. package/src/components/command-bar/commands/direct/index.ts +28 -13
  91. package/src/components/command-bar/pane-templates/items.ts +3 -2
  92. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  93. package/src/components/command-bar/workflow/alert.ts +1 -1
  94. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  95. package/src/components/layout/header.tsx +26 -40
  96. package/src/components/layout/pane/footer/index.tsx +1 -0
  97. package/src/components/layout/pane/footer/model.ts +27 -0
  98. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  99. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  100. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
  101. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  102. package/src/components/onboarding/wizard-positions.ts +3 -3
  103. package/src/components/ticker/list-table-view.tsx +4 -1
  104. package/src/components/ui/choice-dialog.tsx +23 -18
  105. package/src/components/ui/choice-groups.ts +18 -0
  106. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  107. package/src/components/ui/data-table/opentui/model.ts +26 -1
  108. package/src/components/ui/data-table/types.ts +2 -0
  109. package/src/components/ui/display.tsx +5 -1
  110. package/src/components/ui/list-view.tsx +8 -4
  111. package/src/components/ui/query-bar-keys.tsx +2 -0
  112. package/src/components/ui/query-bar.tsx +1 -0
  113. package/src/components/ui/select-button.tsx +5 -1
  114. package/src/components/ui/select-field.tsx +1 -0
  115. package/src/components/ui/table-layout.ts +8 -5
  116. package/src/core/app-runtime.ts +2 -0
  117. package/src/core/app-services.ts +2 -2
  118. package/src/core/state/app/layout.ts +2 -0
  119. package/src/core/state/app/state.ts +9 -0
  120. package/src/core/state/app/types.ts +3 -0
  121. package/src/data/config/chart-settings.ts +1 -1
  122. package/src/data/json-ticker-repository.ts +17 -8
  123. package/src/data/saved-copies.ts +55 -16
  124. package/src/data/ticker-repository.ts +16 -9
  125. package/src/data/ticker-writes.ts +81 -0
  126. package/src/data/ttl-cache.ts +165 -0
  127. package/src/docs/snapshot.json +427 -216
  128. package/src/i18n/es.ts +19 -3
  129. package/src/i18n/ja.ts +19 -3
  130. package/src/i18n/ko.ts +19 -3
  131. package/src/i18n/zh-cn.ts +19 -3
  132. package/src/i18n/zh-tw.ts +19 -3
  133. package/src/market-data/coordinator/financials.ts +10 -2
  134. package/src/market-data/coordinator/index.ts +3 -1
  135. package/src/market-data/coordinator/quotes.ts +4 -2
  136. package/src/market-data/depositary-pair.ts +2 -2
  137. package/src/market-data/fund-facts.ts +50 -0
  138. package/src/market-data/market/change-basis.ts +48 -7
  139. package/src/market-data/market/dated-previous-close.ts +203 -0
  140. package/src/market-data/market/format.ts +25 -0
  141. package/src/market-data/market/freshness.ts +2 -1
  142. package/src/market-data/market/futures-week.ts +68 -0
  143. package/src/market-data/market/status.ts +16 -4
  144. package/src/market-data/options-alternatives.ts +141 -4
  145. package/src/market-data/quotes/contributions.ts +20 -2
  146. package/src/market-data/quotes/resolution.ts +3 -0
  147. package/src/market-data/selectors.ts +3 -0
  148. package/src/news/aggregator.ts +113 -9
  149. package/src/news/cloud-session.ts +21 -0
  150. package/src/news/hooks.ts +1 -0
  151. package/src/news/news-model.ts +1 -0
  152. package/src/news/types.ts +11 -0
  153. package/src/notifications/notification-log.ts +6 -4
  154. package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
  155. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  156. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  157. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  158. package/src/plugins/builtin/alerts/cli.ts +380 -82
  159. package/src/plugins/builtin/alerts/command.ts +31 -0
  160. package/src/plugins/builtin/alerts/constants.ts +1 -1
  161. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  162. package/src/plugins/builtin/alerts/fired-log.ts +113 -8
  163. package/src/plugins/builtin/alerts/format.ts +80 -4
  164. package/src/plugins/builtin/alerts/headless.ts +30 -7
  165. package/src/plugins/builtin/alerts/history-rows.ts +7 -3
  166. package/src/plugins/builtin/alerts/history.ts +2 -0
  167. package/src/plugins/builtin/alerts/index.tsx +135 -22
  168. package/src/plugins/builtin/alerts/live.ts +3 -3
  169. package/src/plugins/builtin/alerts/metric-command.ts +134 -0
  170. package/src/plugins/builtin/alerts/metric-data.ts +111 -0
  171. package/src/plugins/builtin/alerts/metric-model.ts +324 -0
  172. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  173. package/src/plugins/builtin/alerts/pane.tsx +90 -39
  174. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  175. package/src/plugins/builtin/alerts/storage.ts +37 -10
  176. package/src/plugins/builtin/alerts/types.ts +7 -0
  177. package/src/plugins/builtin/analytics/index.tsx +2 -1
  178. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  179. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  180. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  181. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  182. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  183. package/src/plugins/builtin/attention/index.tsx +2 -1
  184. package/src/plugins/builtin/attention/listing.ts +14 -0
  185. package/src/plugins/builtin/backtest/headless.ts +10 -5
  186. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  187. package/src/plugins/builtin/backtest/presets.ts +29 -3
  188. package/src/plugins/builtin/broker-manager/index.tsx +10 -5
  189. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  190. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  191. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  192. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  193. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  194. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  195. package/src/plugins/builtin/buildout/format.ts +38 -0
  196. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  197. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  198. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  199. package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
  200. package/src/plugins/builtin/catalysts/model.ts +2 -1
  201. package/src/plugins/builtin/changelog/index.tsx +9 -6
  202. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
  203. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  204. package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
  205. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
  206. package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
  207. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  208. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  209. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  210. package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
  211. package/src/plugins/builtin/chart-composer/headless.ts +19 -3
  212. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  213. package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
  214. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  215. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  216. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  217. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  218. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  219. package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
  220. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  221. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  222. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  223. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  224. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  225. package/src/plugins/builtin/chat/index.tsx +23 -0
  226. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  227. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  228. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  229. package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
  230. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  231. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  232. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  233. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  234. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  235. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  236. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  237. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  238. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  239. package/src/plugins/builtin/company-attention/client.ts +8 -9
  240. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  241. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  242. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  243. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  244. package/src/plugins/builtin/correlation/headless.ts +13 -5
  245. package/src/plugins/builtin/correlation/history.ts +22 -0
  246. package/src/plugins/builtin/cot/client.ts +22 -18
  247. package/src/plugins/builtin/cot/headless.ts +15 -10
  248. package/src/plugins/builtin/cot/model.ts +14 -1
  249. package/src/plugins/builtin/cot/pane.tsx +22 -14
  250. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  251. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  252. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  253. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  254. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  255. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  256. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  257. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  258. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  259. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  260. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  261. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  262. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  263. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  265. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  266. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  267. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  268. package/src/plugins/builtin/docs/targets.ts +5 -1
  269. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  270. package/src/plugins/builtin/earnings/board.tsx +62 -71
  271. package/src/plugins/builtin/earnings/headless.ts +40 -4
  272. package/src/plugins/builtin/earnings/scope.ts +31 -43
  273. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  274. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  275. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  276. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  277. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  278. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  279. package/src/plugins/builtin/econ/figures.ts +72 -0
  280. package/src/plugins/builtin/econ/index.tsx +33 -3
  281. package/src/plugins/builtin/econ/types.ts +4 -0
  282. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  283. package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
  284. package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
  285. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  286. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  287. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  288. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  289. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  290. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  291. package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
  292. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  293. package/src/plugins/builtin/exposure/model.ts +13 -1
  294. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  295. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  296. package/src/plugins/builtin/futures/index.tsx +1 -0
  297. package/src/plugins/builtin/futures/model.ts +2 -1
  298. package/src/plugins/builtin/futures/table.tsx +2 -2
  299. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  300. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  301. package/src/plugins/builtin/fx-matrix/client.ts +11 -3
  302. package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
  303. package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
  304. package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
  305. package/src/plugins/builtin/gpu/headless.ts +8 -2
  306. package/src/plugins/builtin/help/function-index.ts +4 -1
  307. package/src/plugins/builtin/help/index.tsx +1 -0
  308. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  309. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  310. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  311. package/src/plugins/builtin/holders/headless.ts +18 -1
  312. package/src/plugins/builtin/iv-history/client.ts +52 -11
  313. package/src/plugins/builtin/iv-history/format.ts +36 -23
  314. package/src/plugins/builtin/iv-history/headless.ts +36 -18
  315. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  316. package/src/plugins/builtin/iv-history/model.ts +163 -27
  317. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  318. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  319. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  320. package/src/plugins/builtin/jobs/client.ts +25 -29
  321. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  322. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  323. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  324. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  325. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  326. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  327. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  328. package/src/plugins/builtin/market-movers/client.ts +25 -7
  329. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  330. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  331. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  332. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  333. package/src/plugins/builtin/market-movers/model.ts +4 -1
  334. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  335. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  336. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  337. package/src/plugins/builtin/market-movers/table.ts +17 -5
  338. package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
  339. package/src/plugins/builtin/members/client.ts +19 -4
  340. package/src/plugins/builtin/members/headless.ts +20 -7
  341. package/src/plugins/builtin/members/index.tsx +9 -9
  342. package/src/plugins/builtin/members/model.ts +23 -4
  343. package/src/plugins/builtin/members/pane.tsx +25 -15
  344. package/src/plugins/builtin/news/index.tsx +1 -0
  345. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  346. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  347. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  348. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  349. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  350. package/src/plugins/builtin/openfda/client.ts +14 -2
  351. package/src/plugins/builtin/openfda/index.tsx +3 -2
  352. package/src/plugins/builtin/openfda/model.ts +9 -0
  353. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  354. package/src/plugins/builtin/options/analytics.ts +54 -6
  355. package/src/plugins/builtin/options/enrichment.ts +17 -5
  356. package/src/plugins/builtin/options/model-spot.ts +5 -2
  357. package/src/plugins/builtin/options/strike-window.ts +2 -1
  358. package/src/plugins/builtin/options/table.ts +6 -9
  359. package/src/plugins/builtin/options/types.ts +2 -0
  360. package/src/plugins/builtin/options/view.tsx +42 -12
  361. package/src/plugins/builtin/options-positioning/client.ts +30 -0
  362. package/src/plugins/builtin/options-positioning/headless.ts +74 -21
  363. package/src/plugins/builtin/options-positioning/index.tsx +4 -4
  364. package/src/plugins/builtin/options-positioning/model.ts +122 -14
  365. package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
  366. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  367. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  368. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  369. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  370. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  371. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  372. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  373. package/src/plugins/builtin/pe-band/model.ts +12 -49
  374. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  375. package/src/plugins/builtin/perps/cascade.ts +236 -0
  376. package/src/plugins/builtin/perps/client.ts +56 -2
  377. package/src/plugins/builtin/perps/freshness.ts +12 -10
  378. package/src/plugins/builtin/perps/headless.ts +65 -9
  379. package/src/plugins/builtin/perps/index.tsx +7 -5
  380. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  381. package/src/plugins/builtin/perps/model.ts +18 -4
  382. package/src/plugins/builtin/perps/pane.tsx +32 -31
  383. package/src/plugins/builtin/perps/status-line.ts +25 -0
  384. package/src/plugins/builtin/plugin-module.ts +5 -1
  385. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  386. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  387. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  388. package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
  389. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  390. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  391. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  392. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  393. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  394. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  395. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  396. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  397. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  398. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  399. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  400. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  401. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  402. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  403. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  404. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  405. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  406. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  407. package/src/plugins/builtin/research/event-model.ts +10 -8
  408. package/src/plugins/builtin/research/headless.ts +1 -1
  409. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  410. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  411. package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
  412. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  413. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  414. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  415. package/src/plugins/builtin/scanner/feed.ts +2 -2
  416. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  417. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  418. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  419. package/src/plugins/builtin/scanner/headless.ts +204 -35
  420. package/src/plugins/builtin/scanner/index.ts +40 -6
  421. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  422. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  423. package/src/plugins/builtin/scanner/live-headless.ts +138 -0
  424. package/src/plugins/builtin/scanner/live-model.ts +362 -0
  425. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  426. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  427. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  428. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  429. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  430. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  431. package/src/plugins/builtin/sec/headless.ts +2 -1
  432. package/src/plugins/builtin/series-inventory.ts +3 -0
  433. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  434. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  435. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  436. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  437. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  438. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  439. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  440. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  441. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  442. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  443. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  444. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  445. package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
  446. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  447. package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
  448. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  449. package/src/plugins/builtin/themes/headless.ts +6 -4
  450. package/src/plugins/builtin/themes/model.ts +17 -8
  451. package/src/plugins/builtin/themes/pane.tsx +7 -7
  452. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  453. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  456. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  457. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  458. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  459. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  460. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  461. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  462. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  463. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  464. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  465. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  466. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  467. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  468. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  469. package/src/plugins/builtin/vol-surface/client.ts +9 -17
  470. package/src/plugins/builtin/vol-surface/headless.ts +63 -14
  471. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  472. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  473. package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
  474. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  475. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  476. package/src/plugins/builtin/volatility/index.tsx +2 -2
  477. package/src/plugins/builtin/volatility/pane.tsx +4 -4
  478. package/src/plugins/builtin/volatility/series.ts +26 -0
  479. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  480. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  481. package/src/plugins/catalog-browser.ts +13 -8
  482. package/src/plugins/catalog-manifest.ts +2 -1
  483. package/src/plugins/marketplace/model.ts +18 -5
  484. package/src/portfolio/column-values.ts +5 -5
  485. package/src/portfolio/futures-positions.ts +6 -3
  486. package/src/portfolio/position-quantity.ts +15 -9
  487. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  488. package/src/remote/resources.ts +2 -1
  489. package/src/renderers/browser/app-services.ts +1 -7
  490. package/src/renderers/browser/config-host.ts +2 -55
  491. package/src/renderers/browser/main.tsx +7 -57
  492. package/src/renderers/dom/data-table/row.tsx +26 -0
  493. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  494. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  495. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  496. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  497. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  498. package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
  499. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  500. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  501. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  502. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  503. package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
  504. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  505. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  506. package/src/renderers/electrobun/bun/index.ts +25 -1
  507. package/src/renderers/electrobun/shared/app-name.ts +30 -0
  508. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  509. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  510. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  511. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  512. package/src/renderers/electrobun/view/main.tsx +24 -1
  513. package/src/renderers/electrobun/view/news-source.ts +3 -1
  514. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  515. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  516. package/src/renderers/opentui/start.tsx +5 -0
  517. package/src/renderers/opentui/toast-host.tsx +71 -43
  518. package/src/series/registry.ts +1 -0
  519. package/src/series/types.ts +6 -0
  520. package/src/series/validate.ts +1 -0
  521. package/src/sources/gloomberb-cloud/index.ts +1 -0
  522. package/src/sources/gloomberb-cloud/news.ts +47 -7
  523. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  524. package/src/sources/provider-router/batches.ts +10 -2
  525. package/src/sources/provider-router/financial-routes.ts +3 -3
  526. package/src/sources/provider-router/financials.ts +15 -3
  527. package/src/sources/provider-router/history.ts +28 -7
  528. package/src/sources/provider-router/index.ts +149 -10
  529. package/src/sources/provider-router/search.ts +13 -2
  530. package/src/state/app/context/index.tsx +3 -0
  531. package/src/state/ticker-cleanup.ts +50 -0
  532. package/src/sync/auth-transition.ts +19 -9
  533. package/src/sync/baseline.ts +10 -4
  534. package/src/sync/controller.ts +70 -19
  535. package/src/sync/core-contributors.ts +90 -31
  536. package/src/sync/react.ts +4 -1
  537. package/src/sync/types.ts +3 -0
  538. package/src/telemetry/stall-reports-dom.ts +38 -0
  539. package/src/telemetry/stall-reports-node.ts +75 -0
  540. package/src/telemetry/stall-reports.ts +214 -0
  541. package/src/telemetry/usage-counts.ts +0 -18
  542. package/src/test-support/preload.ts +14 -0
  543. package/src/theme/colors.ts +8 -0
  544. package/src/theme/themes.ts +10 -0
  545. package/src/tickers/company-name.ts +103 -3
  546. package/src/tickers/listing-venues.ts +38 -3
  547. package/src/tickers/search/bare-symbol.ts +36 -0
  548. package/src/tickers/search/index.ts +36 -7
  549. package/src/tickers/search/ranking.ts +10 -1
  550. package/src/tickers/search/types.ts +5 -0
  551. package/src/tickers/terminal-codes.ts +19 -0
  552. package/src/time-series/alignment.ts +1 -1
  553. package/src/time-series/base-series.ts +13 -5
  554. package/src/time-series/daily-valuation.ts +62 -0
  555. package/src/time-series/economic.ts +18 -4
  556. package/src/time-series/field-catalog.ts +2 -4
  557. package/src/time-series/filed-trailing-eps.ts +197 -0
  558. package/src/time-series/forward-valuation.ts +32 -0
  559. package/src/time-series/fundamentals.ts +198 -7
  560. package/src/time-series/hooks.ts +1 -0
  561. package/src/time-series/ice-credit.ts +3 -0
  562. package/src/time-series/price-comparison.ts +30 -2
  563. package/src/time-series/price-units.ts +72 -2
  564. package/src/time-series/resolve-context.ts +7 -1
  565. package/src/time-series/resolve.ts +116 -15
  566. package/src/time-series/series-presentation.ts +40 -6
  567. package/src/time-series/session-bar.ts +11 -0
  568. package/src/time-series/spec.ts +5 -3
  569. package/src/time-series/studies.ts +278 -74
  570. package/src/time-series/total-return.ts +160 -0
  571. package/src/time-series/transforms.ts +3 -3
  572. package/src/time-series/types.ts +27 -1
  573. package/src/time-series/valuation-releases.ts +39 -0
  574. package/src/types/data-provider.ts +9 -1
  575. package/src/types/desktop-window.ts +2 -0
  576. package/src/types/financials.ts +73 -2
  577. package/src/types/headless.ts +61 -5
  578. package/src/types/instrument.ts +3 -0
  579. package/src/types/news-source.ts +1 -0
  580. package/src/types/plugin.ts +10 -1
  581. package/src/ui/host.tsx +3 -0
  582. package/src/updater/command-names.ts +12 -7
  583. package/src/updater/failure.ts +4 -1
  584. package/src/utils/cli-output.ts +42 -1
  585. package/src/utils/crypto-pair.ts +44 -0
  586. package/src/utils/financial-statements.ts +6 -1
  587. package/src/utils/forward-dividend.ts +2 -0
  588. package/src/utils/fundamentals.ts +24 -3
  589. package/src/utils/fx-market-hours.ts +26 -3
  590. package/src/utils/main-thread-stall.ts +35 -2
  591. package/src/utils/market-capitalization.ts +24 -1
  592. package/src/utils/network-failure.ts +9 -6
  593. package/src/utils/option-expiry.ts +48 -1
  594. package/src/utils/options.ts +10 -0
  595. package/src/utils/plugin-proxy-hosts.json +1 -0
  596. package/src/utils/price-history.ts +12 -1
  597. package/src/utils/shell-word.ts +8 -0
  598. package/src/utils/trailing-dividend.ts +53 -4
  599. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
  600. package/src/renderers/browser/first-screen.ts +0 -193
@@ -21,7 +21,8 @@ import type {
21
21
  } from "../../types/data-provider";
22
22
  import type { CapabilityRouteSource } from "../../types/capability-route-source";
23
23
  import { routeSourcePriority } from "../../types/capability-route-source";
24
- import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, QuoteMetadata, TickerFinancials } from "../../types/financials";
24
+ import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, QuoteContributionMap, QuoteMetadata, TickerFinancials } from "../../types/financials";
25
+ import { datedCloseListingKey, PreviousCloseDates, type DatedCloseListing } from "../../market-data/market/dated-previous-close";
25
26
  import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
26
27
  import type { NewsArticle, NewsQuery } from "../../news/types";
27
28
  import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
@@ -52,6 +53,7 @@ import { ProviderRouterSearchRoutes } from "./search";
52
53
  import { collectCapabilityRouteSources, normalizeRouteSource } from "./sources";
53
54
  import type { ProviderRouterCoreDeps } from "./route-types";
54
55
  import { withProviderConnectionHealth } from "./connection-health";
56
+ import { withFiledTrailingEps } from "../../time-series/filed-trailing-eps";
55
57
  import {
56
58
  contextFromCachedTarget,
57
59
  getBrokerCandidates,
@@ -64,6 +66,14 @@ import {
64
66
 
65
67
  const providerLog = debugLog.createLogger("asset-data-router");
66
68
 
69
+ /**
70
+ * How long a short-lived process (the CLI) waits in all, per request that
71
+ * prints what its moves are from, for the daily closes that date them. The
72
+ * long-lived app never waits: it answers with what was read and dates the
73
+ * copies after.
74
+ */
75
+ const CHANGE_BASIS_WAIT_MS = 2_500;
76
+
67
77
  export class AssetDataRouter implements DataProvider {
68
78
  readonly id = "asset-data-router";
69
79
  readonly name = "Asset Data Router";
@@ -86,6 +96,18 @@ export class AssetDataRouter implements DataProvider {
86
96
  private realtimeCloudAccess: RealtimeCloudAccess = createSignedInRealtimeCloudAccess();
87
97
  /** A short-lived process exits before a background refresh can land, so it must await stale entries. */
88
98
  private revalidateInBackground = true;
99
+ /**
100
+ * Every quote this router answers names the day its previous close is from,
101
+ * where the venue's calendar cannot. The closes come from the providers
102
+ * alone, the chart's month of them shared: a broker is never asked once
103
+ * per listing. A read that lands dates the latest copy for its listing's
104
+ * stream subscribers.
105
+ */
106
+ private readonly previousCloseDates = new PreviousCloseDates(
107
+ (listing) => this.historyRoutes.getPriceHistory(listing.symbol, listing.exchange, "1M", listingHistoryContext(listing.context), { askBrokers: false }),
108
+ { onDated: (listing, quote) => { for (const listener of this.datedQuoteListeners.get(datedCloseListingKey(listing)) ?? []) listener(quote); } },
109
+ );
110
+ private readonly datedQuoteListeners = new Map<string, Set<(quote: Quote) => void>>();
89
111
 
90
112
  constructor(
91
113
  fallbackSource: CapabilityRouteSource | DataProvider | null = null,
@@ -188,7 +210,13 @@ export class AssetDataRouter implements DataProvider {
188
210
  options: { allowExpired?: boolean; includeStaleQuotes?: boolean } = {},
189
211
  ): Map<string, TickerFinancials> {
190
212
  const { valid } = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, contextFromCachedTarget(target)));
191
- return this.financialRoutes.getCachedFinancialsForTargets(valid, options);
213
+ const cached = this.financialRoutes.getCachedFinancialsForTargets(valid, options);
214
+ for (const target of valid) {
215
+ const key = target.symbol.toUpperCase();
216
+ const financials = cached.get(key);
217
+ if (financials) cached.set(key, withFiledTrailingEps(this.knownFinancials(financials, listingOf(target, contextFromCachedTarget(target)))));
218
+ }
219
+ return cached;
192
220
  }
193
221
 
194
222
  getCachedExchangeRates(currencies: string[], options: { allowExpired?: boolean } = {}): Map<string, number> {
@@ -200,7 +228,18 @@ export class AssetDataRouter implements DataProvider {
200
228
  options: QuoteBatchOptions = {},
201
229
  ): Promise<QuoteBatchResult[]> {
202
230
  const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, target.context));
203
- const results = await this.batchRoutes.getQuotesBatch(mapped.valid, options);
231
+ const answered = await this.batchRoutes.getQuotesBatch(mapped.valid, withoutDating(options));
232
+ const quoted = answered.flatMap((result) => result.quote ? [{ quote: result.quote, listing: listingOf(result.target, result.target.context) }] : []);
233
+ // A cache-only answer never reaches the network, so it is dated only by what was read already.
234
+ const dated = options.cacheOnly
235
+ ? quoted.map(({ quote, listing }) => this.previousCloseDates.known(quote, listing))
236
+ : await this.datedAnswers(quoted, options.datesChangeBasis, options.signal);
237
+ let next = 0;
238
+ const results = answered.map((result) => {
239
+ if (!result.quote) return result;
240
+ const quote = dated[next++]!;
241
+ return quote === result.quote ? result : { ...result, quote };
242
+ });
204
243
  const restored = new Map<QuoteSubscriptionTarget, QuoteBatchResult>(results.map((result) => {
205
244
  const target = mapped.original(result.target);
206
245
  return [target, { ...result, target }] as const;
@@ -216,10 +255,20 @@ export class AssetDataRouter implements DataProvider {
216
255
 
217
256
  async getTickerFinancialsBatch(
218
257
  targets: CachedFinancialsTarget[],
219
- options: { forceRefresh?: boolean } = {},
258
+ options: { forceRefresh?: boolean; datesChangeBasis?: boolean } = {},
220
259
  ): Promise<TickerFinancialsBatchResult[]> {
221
260
  const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, contextFromCachedTarget(target)));
222
- const results = await this.batchRoutes.getTickerFinancialsBatch(mapped.valid, options);
261
+ const answered = await this.batchRoutes.getTickerFinancialsBatch(mapped.valid, { forceRefresh: options.forceRefresh });
262
+ const listingOfResult = (result: TickerFinancialsBatchResult) => listingOf(result.target, contextFromCachedTarget(result.target));
263
+ const quoted = answered.flatMap((result) => result.financials?.quote ? [{ quote: result.financials.quote, listing: listingOfResult(result) }] : []);
264
+ const dated = await this.datedAnswers(quoted, options.datesChangeBasis);
265
+ let next = 0;
266
+ const results = answered.map((result) => {
267
+ if (!result.financials) return result;
268
+ const quoted = result.financials.quote ? this.withDatedQuote(result.financials, dated[next++]!, listingOfResult(result)) : result.financials;
269
+ const financials = withFiledTrailingEps(quoted);
270
+ return financials === result.financials ? result : { ...result, financials };
271
+ });
223
272
  const restored = new Map(results.map((result) => {
224
273
  const target = mapped.original(result.target);
225
274
  return [target, { ...result, target }] as const;
@@ -228,14 +277,56 @@ export class AssetDataRouter implements DataProvider {
228
277
  return targets.map((target) => restored.get(target) ?? { target, financials: null });
229
278
  }
230
279
 
231
- async getTickerFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<TickerFinancials> {
280
+ async getTickerFinancials(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<TickerFinancials> {
281
+ const { context, datesChangeBasis } = splitDating(requestContext);
232
282
  exchange = this.resolvePublicExchange(ticker, exchange, context);
233
- return this.financialRoutes.getTickerFinancials(ticker, exchange, context);
283
+ const financials = await this.financialRoutes.getTickerFinancials(ticker, exchange, context);
284
+ if (!financials.quote) return withFiledTrailingEps(financials);
285
+ const listing = listingOf({ symbol: ticker, exchange }, context);
286
+ const [quote] = await this.datedAnswers([{ quote: financials.quote, listing }], datesChangeBasis);
287
+ // One trailing EPS and P/E for every view of the listing: the filed quarters', as PEB and GE read them.
288
+ return withFiledTrailingEps(this.withDatedQuote(financials, quote!, listing));
234
289
  }
235
290
 
236
- async getQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<Quote> {
291
+ async getQuote(ticker: string, exchange?: string, requestContext?: MarketDataRequestContext): Promise<Quote> {
292
+ const { context, datesChangeBasis } = splitDating(requestContext);
237
293
  exchange = this.resolvePublicExchange(ticker, exchange, context);
238
- return this.financialRoutes.getQuote(ticker, exchange, context);
294
+ const quote = await this.financialRoutes.getQuote(ticker, exchange, context);
295
+ return (await this.datedAnswers([{ quote, listing: listingOf({ symbol: ticker, exchange }, context) }], datesChangeBasis))[0]!;
296
+ }
297
+
298
+ /**
299
+ * The quotes as this router answers them, their previous closes dated
300
+ * (PreviousCloseDates). The long-lived app never waits: each is dated by
301
+ * what was read already, and the reads it lacks start for the copies after
302
+ * it. A short-lived process starts reads only for a request that prints
303
+ * what its moves are from (`datesChangeBasis`), and waits for them a few
304
+ * seconds in all.
305
+ */
306
+ private async datedAnswers(
307
+ entries: Array<{ quote: Quote; listing: DatedCloseListing }>,
308
+ datesChangeBasis?: boolean,
309
+ signal?: AbortSignal,
310
+ ): Promise<Quote[]> {
311
+ if (this.revalidateInBackground) return entries.map(({ quote, listing }) => this.previousCloseDates.observe(quote, listing));
312
+ if (datesChangeBasis) await this.previousCloseDates.settle(entries, { waitMs: CHANGE_BASIS_WAIT_MS, signal });
313
+ return entries.map(({ quote, listing }) => this.previousCloseDates.known(quote, listing));
314
+ }
315
+
316
+ /** The financials with their dated quote, and the contributions it is resolved from dated alike. */
317
+ private withDatedQuote(financials: TickerFinancials, quote: Quote, listing: DatedCloseListing): TickerFinancials {
318
+ return quote === financials.quote ? financials : { ...this.knownFinancials(financials, listing), quote };
319
+ }
320
+
321
+ /** The financials dated by what was read already: their quote and the contributions it is resolved from. */
322
+ private knownFinancials(financials: TickerFinancials, listing: DatedCloseListing): TickerFinancials {
323
+ const quote = this.previousCloseDates.known(financials.quote, listing);
324
+ const contributions = financials.quoteContributions;
325
+ const quoteContributions = contributions && Object.fromEntries(Object.entries(contributions)
326
+ .map(([key, contribution]) => [key, this.previousCloseDates.known(contribution, listing)])) as QuoteContributionMap;
327
+ const contributionsChanged = !!contributions && Object.keys(contributions).some((key) => quoteContributions![key] !== contributions[key]);
328
+ if (quote === financials.quote && !contributionsChanged) return financials;
329
+ return { ...financials, quote, ...(contributionsChanged ? { quoteContributions } : {}) };
239
330
  }
240
331
 
241
332
  async getQuoteMetadata(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<QuoteMetadata | null> {
@@ -569,6 +660,54 @@ export class AssetDataRouter implements DataProvider {
569
660
  onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
570
661
  ): () => void {
571
662
  const mapped = mapListingTargets(targets, (target) => this.resolvePublicExchange(target.symbol, target.exchange, target.context));
572
- return this.streamingRoutes.subscribeQuotes(mapped.valid, (target, quote) => onQuote(mapped.original(target), quote));
663
+ // A read that lands dates the latest copy of a quote for the listing's subscribers.
664
+ const listeners = mapped.valid.map((target) => {
665
+ const key = datedCloseListingKey(listingOf(target));
666
+ const listener = (quote: Quote) => onQuote(mapped.original(target), quote);
667
+ const set = this.datedQuoteListeners.get(key) ?? new Set();
668
+ set.add(listener);
669
+ this.datedQuoteListeners.set(key, set);
670
+ return { key, listener };
671
+ });
672
+ const unsubscribe = this.streamingRoutes.subscribeQuotes(mapped.valid, (target, quote) => {
673
+ // A tick is dated by what was read already; one not yet read is read for the ticks after it.
674
+ onQuote(mapped.original(target), this.previousCloseDates.observe(quote, listingOf(target, target.context)));
675
+ });
676
+ return () => {
677
+ unsubscribe();
678
+ for (const { key, listener } of listeners) {
679
+ const set = this.datedQuoteListeners.get(key);
680
+ set?.delete(listener);
681
+ if (set?.size === 0) this.datedQuoteListeners.delete(key);
682
+ }
683
+ };
573
684
  }
574
685
  }
686
+
687
+ function listingOf(target: { symbol: string; exchange?: string }, context?: MarketDataRequestContext): DatedCloseListing {
688
+ return { symbol: target.symbol, exchange: target.exchange ?? "", ...(context ? { context } : {}) };
689
+ }
690
+
691
+ /** A request's context without the dating flag, which no source reads. */
692
+ function splitDating(requestContext?: MarketDataRequestContext): { context?: MarketDataRequestContext; datesChangeBasis: boolean } {
693
+ if (!requestContext || !("datesChangeBasis" in requestContext)) return { context: requestContext, datesChangeBasis: false };
694
+ const { datesChangeBasis, ...context } = requestContext;
695
+ return { context: Object.keys(context).length ? context : undefined, datesChangeBasis: datesChangeBasis === true };
696
+ }
697
+
698
+ function withoutDating(options: QuoteBatchOptions): QuoteBatchOptions {
699
+ if (!("datesChangeBasis" in options)) return options;
700
+ const { datesChangeBasis: _dates, ...rest } = options;
701
+ return rest;
702
+ }
703
+
704
+ /**
705
+ * The context a listing's daily closes are read with: its broker contract, so
706
+ * the chart's copy of them is shared, and nothing about the quote request.
707
+ */
708
+ function listingHistoryContext(context?: MarketDataRequestContext): MarketDataRequestContext | undefined {
709
+ if (!context) return undefined;
710
+ const { brokerId, brokerInstanceId, instrument } = context;
711
+ const history = { ...(brokerId ? { brokerId } : {}), ...(brokerInstanceId ? { brokerInstanceId } : {}), ...(instrument ? { instrument } : {}) };
712
+ return Object.keys(history).length ? history : undefined;
713
+ }
@@ -5,6 +5,7 @@ import type { BrokerCandidate } from "./brokers";
5
5
  import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE, shouldLogProviderError } from "../provider-errors";
6
6
  import { searchInstrumentKey } from "../../tickers/search/identity";
7
7
  import { OperationTimeoutError, settleWithin, withDeadline } from "../../utils/async-deadline";
8
+ import { isNetworkFailure } from "../../utils/network-failure";
8
9
 
9
10
  const SEARCH_CACHE_TTL_MS = 30_000;
10
11
  const SEARCH_CACHE_MAX_ENTRIES = 100;
@@ -142,6 +143,9 @@ export class ProviderRouterSearchRoutes {
142
143
  * A search nothing answered because a provider was busy (rate limited,
143
144
  * overloaded, out of time) rejects with a `ProviderBusyError` instead of
144
145
  * reading as an empty one, and is not cached, so asking again asks again.
146
+ * One nothing answered because the data service could not be reached
147
+ * (offline, a refused or hung connection) rejects with that failure: it is
148
+ * no answer, not a search that found nothing.
145
149
  */
146
150
  private fetchSearchResults(
147
151
  query: string,
@@ -172,6 +176,7 @@ export class ProviderRouterSearchRoutes {
172
176
  });
173
177
  let answered = false;
174
178
  let busy = false;
179
+ let unreachable: unknown = null;
175
180
 
176
181
  const record = (items: InstrumentSearchResult[] | null) => {
177
182
  if (!items?.length) return;
@@ -204,8 +209,10 @@ export class ProviderRouterSearchRoutes {
204
209
  record(await withDeadline(provider.search(query, context), timeoutMs, `${provider.id} search timed out`));
205
210
  } catch (error) {
206
211
  const timedOut = error instanceof OperationTimeoutError;
207
- // A keystroke gives up on a slow source by design and asks again on the next one; only a full wait is busy.
208
- if (isProviderBusy(error) || (timedOut && !context?.interactive)) {
212
+ if (isNetworkFailure(error)) {
213
+ unreachable ??= error;
214
+ } else if (isProviderBusy(error) || (timedOut && !context?.interactive)) {
215
+ // A keystroke gives up on a slow source by design and asks again on the next one; only a full wait is busy.
209
216
  busy = true;
210
217
  } else if (!timedOut && shouldLogProviderError(error)) {
211
218
  this.deps.logProviderError(`${provider.id} failed: ${error}`);
@@ -231,6 +238,10 @@ export class ProviderRouterSearchRoutes {
231
238
  })().then(() => {
232
239
  if (answered) return;
233
240
  answered = true;
241
+ if (unreachable) {
242
+ rejectFirst(unreachable);
243
+ return;
244
+ }
234
245
  if (busy) {
235
246
  rejectFirst(new ProviderBusyError(SEARCH_BUSY_MESSAGE));
236
247
  return;
@@ -495,6 +495,7 @@ export function AppProvider({
495
495
  paneState: currentState.paneState,
496
496
  focusedPaneId: currentState.focusedPaneId,
497
497
  statusBarVisible: currentState.statusBarVisible,
498
+ brokerSyncErrors: currentState.brokerSyncErrors,
498
499
  }), []);
499
500
 
500
501
  const serializeDesktopSnapshot = useCallback((snapshot: DesktopSharedStateSnapshot): string => JSON.stringify({
@@ -502,6 +503,7 @@ export function AppProvider({
502
503
  paneState: snapshot.paneState,
503
504
  focusedPaneId: snapshot.focusedPaneId,
504
505
  statusBarVisible: snapshot.statusBarVisible,
506
+ brokerSyncErrors: snapshot.brokerSyncErrors ?? {},
505
507
  }), []);
506
508
 
507
509
  const serializePaneState = useCallback((paneState: PaneRuntimeState | undefined): string => JSON.stringify(paneState ?? {}), []);
@@ -594,6 +596,7 @@ export function AppProvider({
594
596
  state.focusedPaneId,
595
597
  state.paneState,
596
598
  state.statusBarVisible,
599
+ state.brokerSyncErrors,
597
600
  ]);
598
601
 
599
602
  useEffect(() => {
@@ -0,0 +1,50 @@
1
+ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
2
+ import { hasPendingTickerWrites, settleTickerWrites, tickerWriteRevision } from "../data/ticker-writes";
3
+ import type { TickerRecord } from "../types/ticker";
4
+
5
+ /**
6
+ * Persist cleanup before publishing it, retrying against edits acknowledged in between.
7
+ * Return the cleaned map so callers can recheck it after their config save.
8
+ */
9
+ export async function persistTickerCleanup({
10
+ tickerRepository,
11
+ getTickers,
12
+ transform,
13
+ publish,
14
+ isCurrent = () => true,
15
+ }: {
16
+ tickerRepository: AppTickerRepositoryPort;
17
+ getTickers: () => ReadonlyMap<string, TickerRecord>;
18
+ transform: (ticker: TickerRecord) => TickerRecord | null;
19
+ publish: (symbol: string, ticker: TickerRecord | null) => void;
20
+ isCurrent?: () => boolean;
21
+ }): Promise<ReadonlyMap<string, TickerRecord> | null> {
22
+ for (;;) {
23
+ do {
24
+ await settleTickerWrites(tickerRepository);
25
+ } while (hasPendingTickerWrites(tickerRepository));
26
+ if (!isCurrent()) return null;
27
+ const before = getTickers();
28
+ let wrote = false;
29
+ for (const symbol of before.keys()) {
30
+ do {
31
+ await settleTickerWrites(tickerRepository);
32
+ } while (hasPendingTickerWrites(tickerRepository));
33
+ if (!isCurrent()) return null;
34
+ const source = getTickers().get(symbol);
35
+ if (!source) continue;
36
+ const next = transform(source);
37
+ if (next === source) continue;
38
+ wrote = true;
39
+ const save = next ? tickerRepository.saveTicker(next) : tickerRepository.deleteTicker(symbol);
40
+ const revision = tickerWriteRevision(tickerRepository, symbol);
41
+ await save;
42
+ if (!isCurrent()) return null;
43
+ // An edit can start its save before publishing state, especially over RPC.
44
+ if (tickerWriteRevision(tickerRepository, symbol) !== revision || getTickers().get(symbol) !== source) continue;
45
+ publish(symbol, next);
46
+ }
47
+ // Include rows created or recreated while earlier rows were being saved.
48
+ if (!wrote && getTickers() === before) return before;
49
+ }
50
+ }
@@ -1,22 +1,32 @@
1
1
  export interface CloudSessionLifecycle {
2
- isVerified(): boolean;
2
+ getCurrentUser(): { id: string } | null;
3
+ isSignedIn(): boolean;
4
+ getVerifiedSessionIdentity(): object | null;
3
5
  subscribeCurrentUser(listener: () => void): () => void;
4
6
  }
5
7
 
6
- /**
7
- * Runs exactly when a signed-in desktop session becomes verified. It deliberately
8
- * ignores ordinary account and entitlement refreshes so they cannot re-run the
9
- * initial sync pull and push.
10
- */
8
+ /** Starts a first merge for a newly verified account, ignoring ordinary user and entitlement refreshes. */
11
9
  export function subscribeToCloudVerification(
12
10
  session: CloudSessionLifecycle,
13
11
  onVerified: () => void,
12
+ onAccountChange: (accountId: string) => void,
13
+ onSuspended?: () => void,
14
14
  ): () => void {
15
- let wasVerified = session.isVerified();
15
+ let accountId = session.getCurrentUser()?.id ?? null;
16
+ let wasVerified = session.getVerifiedSessionIdentity() !== null;
16
17
  return session.subscribeCurrentUser(() => {
17
- const isVerified = session.isVerified();
18
- const becameVerified = !wasVerified && isVerified;
18
+ const isVerified = session.getVerifiedSessionIdentity() !== null;
19
+ const nextAccountId = isVerified ? session.getCurrentUser()?.id ?? null : null;
20
+ const accountChanged = nextAccountId !== null && nextAccountId !== accountId;
21
+ const becameVerified = isVerified && (!wasVerified || accountChanged);
22
+ const becameSuspended = wasVerified && !isVerified;
23
+ const resetAccount = accountChanged && accountId !== null;
24
+ // Sign-out and failed validation retire pending work, but keep the last
25
+ // account's merge history until a different account is confirmed.
26
+ if (nextAccountId !== null) accountId = nextAccountId;
19
27
  wasVerified = isVerified;
28
+ if (becameSuspended) onSuspended?.();
29
+ if (resetAccount) onAccountChange(nextAccountId!);
20
30
  if (becameVerified) onVerified();
21
31
  });
22
32
  }
@@ -4,16 +4,17 @@ import type { SyncBaselineStore } from "./types";
4
4
  const BASELINE_PLUGIN_ID = "core.sync";
5
5
  const BASELINE_KEY = "last-synced-payloads";
6
6
  const MEMOS_KEY = "contributor-memos";
7
+ const ACCOUNT_KEY = "account";
7
8
 
8
9
  /**
9
10
  * Persists what this device last pushed so the next pull can tell "the cloud is
10
11
  * newer" from "local changed while the app was closed". Without it every launch
11
12
  * treats local state as pristine and the cloud overwrites CLI/offline edits.
12
13
  *
13
- * Storage failures are swallowed: a missing baseline only falls back to keeping
14
- * local state, never to discarding it.
14
+ * Storage failures do not stop sync, but missing merge history can prevent
15
+ * contributors from recognizing unsynced local edits.
15
16
  */
16
- export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): SyncBaselineStore {
17
+ export function createSyncBaselineStore(pluginState: Pick<AppPluginStateStorePort, "get" | "set">): SyncBaselineStore {
17
18
  const load = (key: string) => {
18
19
  try {
19
20
  return pluginState.get<Record<string, unknown>>(BASELINE_PLUGIN_ID, key)?.value ?? null;
@@ -25,10 +26,15 @@ export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): S
25
26
  try {
26
27
  pluginState.set(BASELINE_PLUGIN_ID, key, value);
27
28
  } catch {
28
- // Ignored: a stale baseline or memo only makes the next pull more conservative.
29
+ // Persistence is best effort; keep the in-memory merge history usable.
29
30
  }
30
31
  };
31
32
  return {
33
+ loadAccountId: () => {
34
+ const id = load(ACCOUNT_KEY)?.id;
35
+ return typeof id === "string" ? id : null;
36
+ },
37
+ saveAccountId: (id) => save(ACCOUNT_KEY, { id }),
32
38
  load: () => load(BASELINE_KEY),
33
39
  save: (payloads) => save(BASELINE_KEY, payloads),
34
40
  loadMemos: () => load(MEMOS_KEY),
@@ -33,11 +33,16 @@ interface SyncRuntime {
33
33
  getContributors: () => RegisteredSyncContributor[];
34
34
  getTransport: () => RegisteredSyncTransport | null;
35
35
  baselineStore?: SyncBaselineStore;
36
+ /** Retires pending operations when the Cloud session changes; transports decide availability. */
37
+ getSessionIdentity?: () => object | null;
38
+ /** Confirmed Cloud account, used to keep persisted merge history scoped across restarts. */
39
+ getAccountId?: () => string | null;
36
40
  /** Sources whose quotes and bars never leave this device (`BrokerAdapter.localMarketData`); none when absent. */
37
41
  isLocalMarketData?: LocalMarketDataLookup;
38
42
  }
39
43
 
40
- const CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
44
+ /** Kept in the view's storage; the desktop app also keeps a copy on disk (electrobun/view/view-ids.ts). */
45
+ export const SYNC_CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
41
46
  const PUSH_DEBOUNCE_MS = 2500;
42
47
  /** How soon after a change contributors date it, so an edit made just before quitting still is. */
43
48
  const OBSERVE_DEBOUNCE_MS = 250;
@@ -56,10 +61,10 @@ function nowIso(): string {
56
61
 
57
62
  function resolveClientId(): string {
58
63
  const storage = globalThis.localStorage;
59
- const existing = storage?.getItem(CLIENT_ID_STORAGE_KEY);
64
+ const existing = storage?.getItem(SYNC_CLIENT_ID_STORAGE_KEY);
60
65
  if (existing) return existing;
61
66
  const id = `client_${crypto.randomUUID()}`;
62
- storage?.setItem(CLIENT_ID_STORAGE_KEY, id);
67
+ storage?.setItem(SYNC_CLIENT_ID_STORAGE_KEY, id);
63
68
  return id;
64
69
  }
65
70
 
@@ -88,7 +93,9 @@ export class CloudSyncController {
88
93
  private memosLoaded = false;
89
94
  private inFlight: Promise<void> | null = null;
90
95
  private syncQueued = false;
96
+ private generation = 0;
91
97
  private clientId: string | null = null;
98
+ private accountId: string | null = null;
92
99
  private lastSignature: string | null = null;
93
100
  /**
94
101
  * The content signature of the cloud's copy, with the revision and the
@@ -146,6 +153,7 @@ export class CloudSyncController {
146
153
  if (this.runtime !== runtime) {
147
154
  this.resetOperations();
148
155
  this.runtime = runtime;
156
+ this.accountId = runtime.baselineStore?.loadAccountId?.() ?? null;
149
157
  this.updateAvailability();
150
158
  }
151
159
  return () => this.clearRuntime(runtime);
@@ -158,6 +166,31 @@ export class CloudSyncController {
158
166
  this.setStatus({ phase: "disabled", transportId: null, error: null });
159
167
  }
160
168
 
169
+ /** Retires pending session work without losing unsynced edits for the same account. */
170
+ retireSession(): void {
171
+ this.generation += 1;
172
+ if (this.pushTimer) clearTimeout(this.pushTimer);
173
+ this.pushTimer = null;
174
+ this.inFlight = null;
175
+ this.syncQueued = false;
176
+ this.updateAvailability();
177
+ }
178
+
179
+ /** Account revisions, merge baselines and edit provenance never carry into another account. */
180
+ resetAccount(accountId?: string): void {
181
+ this.resetOperations();
182
+ this.accountId = accountId ?? null;
183
+ this.baseline = {};
184
+ this.baselineLoaded = true;
185
+ this.memos = {};
186
+ this.memosLoaded = true;
187
+ this.runtime?.baselineStore?.save({});
188
+ this.runtime?.baselineStore?.saveMemos?.({});
189
+ if (accountId) this.runtime?.baselineStore?.saveAccountId?.(accountId);
190
+ this.setStatus({ phase: "idle", transportId: null, lastSyncAt: null, lastPullAt: null, revision: null, error: null });
191
+ this.updateAvailability();
192
+ }
193
+
161
194
  subscribe(listener: () => void): () => void {
162
195
  this.listeners.add(listener);
163
196
  return () => this.listeners.delete(listener);
@@ -206,6 +239,17 @@ export class CloudSyncController {
206
239
  private async syncOnce(options: { reason?: string; force?: boolean }): Promise<void> {
207
240
  const runtime = this.runtime;
208
241
  if (!runtime) return;
242
+ const accountId = runtime.getAccountId?.();
243
+ if (accountId && accountId !== this.accountId) {
244
+ if (this.accountId !== null) this.resetAccount(accountId);
245
+ else {
246
+ // Legacy stores have no owner; retain their offline edits on first validation.
247
+ this.accountId = accountId;
248
+ runtime.baselineStore?.saveAccountId?.(accountId);
249
+ }
250
+ }
251
+ const generation = this.generation;
252
+ const sessionIdentity = runtime.getSessionIdentity?.();
209
253
  this.observe(runtime);
210
254
  const registration = runtime.getTransport();
211
255
  if (!registration?.transport.isAvailable()) {
@@ -222,11 +266,11 @@ export class CloudSyncController {
222
266
  // Always pull before push. Another device (TUI, browser, phone) may have
223
267
  // written since this process last synced, and contributors already merge
224
268
  // so local-only edits survive the apply.
225
- const pulled = await this.runPull(runtime, transport);
226
- if (!pulled || !this.isCurrent(runtime, transport)) return;
269
+ const pulled = await this.runPull(runtime, transport, generation, sessionIdentity);
270
+ if (!pulled || !this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
227
271
 
228
- const snapshot = await this.assembleSnapshot(runtime);
229
- if (!this.isCurrent(runtime, transport)) return;
272
+ const snapshot = await this.assembleSnapshot(runtime, generation, sessionIdentity);
273
+ if (!snapshot || !this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
230
274
  const signature = snapshotContentSignature(snapshot);
231
275
  if (!options.force && signature === this.lastSignature) return;
232
276
  if (!options.force && signature === pulled.signature) {
@@ -242,7 +286,7 @@ export class CloudSyncController {
242
286
  // The revision this snapshot was merged with. The cloud refuses the
243
287
  // write if another device wrote since, instead of losing that write.
244
288
  const result = await transport.pushSnapshot(snapshot, { baseRevision: pulled.revision });
245
- if (!this.isCurrent(runtime, transport)) return;
289
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
246
290
  this.lastSignature = signature;
247
291
  this.appliedRevision = result.revision;
248
292
  this.saveBaseline(runtime, snapshot);
@@ -255,10 +299,10 @@ export class CloudSyncController {
255
299
  });
256
300
  return;
257
301
  } catch (error) {
258
- if (!this.isCurrent(runtime, transport)) return;
302
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return;
259
303
  if (error instanceof SyncRevisionConflictError && attempt < MAX_CONFLICT_RETRIES) {
260
304
  // Pull whatever the other device wrote, even an older revision than
261
- // this process last saw (an account switch, a restored snapshot).
305
+ // this process last saw, such as a restored snapshot.
262
306
  this.appliedRevision = null;
263
307
  continue;
264
308
  }
@@ -272,18 +316,18 @@ export class CloudSyncController {
272
316
  }
273
317
  }
274
318
 
275
- private async runPull(runtime: SyncRuntime, transport: SyncTransport): Promise<PullResult | null> {
319
+ private async runPull(runtime: SyncRuntime, transport: SyncTransport, generation: number, sessionIdentity: object | null | undefined): Promise<PullResult | null> {
276
320
  const baselineState = runtime.getState();
277
321
  this.setStatus({ phase: "syncing", transportId: transport.id, error: null });
278
322
  try {
279
323
  const response = await transport.pullSnapshot();
280
- if (!this.isCurrent(runtime, transport)) return null;
324
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
281
325
 
282
326
  if (response.snapshot && this.shouldApplyPulledRevision(response.revision)) {
283
327
  this.assertSnapshotCompatible(response.snapshot, runtime.getContributors());
284
328
  this.pulledContributors = response.snapshot.contributors;
285
329
  for (const entry of runtime.getContributors()) {
286
- if (!this.isCurrent(runtime, transport)) return null;
330
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
287
331
  const contributorPayload = response.snapshot.contributors[entry.contributor.id];
288
332
  if (!contributorPayload || !entry.contributor.apply) continue;
289
333
  await entry.contributor.apply(contributorPayload.payload, {
@@ -292,16 +336,17 @@ export class CloudSyncController {
292
336
  baselinePayload: this.loadBaseline(runtime)?.[entry.contributor.id] ?? null,
293
337
  state: runtime.getState(),
294
338
  getState: runtime.getState,
295
- isCurrent: () => this.isCurrent(runtime, transport),
339
+ isCurrent: () => this.isCurrent(runtime, transport, generation, sessionIdentity),
296
340
  dispatch: runtime.dispatch,
297
341
  tickerRepository: runtime.tickerRepository,
298
342
  memo: this.memoFor(runtime, entry.contributor.id),
299
343
  });
344
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
300
345
  }
301
346
  if (response.revision != null) this.appliedRevision = response.revision;
302
347
  }
303
348
 
304
- if (!this.isCurrent(runtime, transport)) return null;
349
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
305
350
  this.setStatus({
306
351
  phase: response.snapshot ? "synced" : "idle",
307
352
  transportId: transport.id,
@@ -315,7 +360,7 @@ export class CloudSyncController {
315
360
  signature: response.snapshot ? this.pulledSignature(transport, response.revision, response.snapshot) : null,
316
361
  };
317
362
  } catch (error) {
318
- if (!this.isCurrent(runtime, transport)) return null;
363
+ if (!this.isCurrent(runtime, transport, generation, sessionIdentity)) return null;
319
364
  this.setStatus({
320
365
  phase: "error",
321
366
  transportId: transport.id,
@@ -348,7 +393,7 @@ export class CloudSyncController {
348
393
  }
349
394
  }
350
395
 
351
- private async assembleSnapshot(runtime: SyncRuntime): Promise<SyncSnapshot> {
396
+ private async assembleSnapshot(runtime: SyncRuntime, generation: number, sessionIdentity: object | null | undefined): Promise<SyncSnapshot | null> {
352
397
  const contributors = runtime.getContributors();
353
398
  const createdAt = nowIso();
354
399
  // The snapshot replaces the stored one wholesale, so data owned by a
@@ -356,6 +401,7 @@ export class CloudSyncController {
356
401
  // carried over instead of being deleted for every other device.
357
402
  const payloads: SyncSnapshot["contributors"] = { ...this.pulledContributors };
358
403
  for (const entry of contributors) {
404
+ if (this.runtime !== runtime || this.generation !== generation || runtime.getSessionIdentity?.() !== sessionIdentity) return null;
359
405
  const payload = await entry.contributor.collect({
360
406
  state: runtime.getState(),
361
407
  baselinePayload: this.loadBaseline(runtime)?.[entry.contributor.id] ?? null,
@@ -415,9 +461,12 @@ export class CloudSyncController {
415
461
  }
416
462
 
417
463
  private memoFor(runtime: SyncRuntime, contributorId: string) {
464
+ const generation = this.generation;
465
+ const sessionIdentity = runtime.getSessionIdentity?.();
418
466
  return {
419
467
  get: () => this.loadMemos(runtime)[contributorId] ?? null,
420
468
  set: (value: unknown) => {
469
+ if (this.runtime !== runtime || this.generation !== generation || runtime.getSessionIdentity?.() !== sessionIdentity) return;
421
470
  const memos = { ...this.loadMemos(runtime), [contributorId]: value };
422
471
  this.memos = memos;
423
472
  runtime.baselineStore?.saveMemos?.(memos);
@@ -442,8 +491,9 @@ export class CloudSyncController {
442
491
  runtime.baselineStore?.save(payloads);
443
492
  }
444
493
 
445
- private isCurrent(runtime: SyncRuntime, transport: SyncTransport): boolean {
446
- return this.runtime === runtime && runtime.getTransport()?.transport === transport;
494
+ private isCurrent(runtime: SyncRuntime, transport: SyncTransport, generation: number, sessionIdentity: object | null | undefined): boolean {
495
+ return this.generation === generation && this.runtime === runtime && runtime.getTransport()?.transport === transport
496
+ && runtime.getSessionIdentity?.() === sessionIdentity;
447
497
  }
448
498
 
449
499
  private pulledSignature(transport: SyncTransport, revision: number | null, snapshot: SyncSnapshot): string {
@@ -470,6 +520,7 @@ export class CloudSyncController {
470
520
  }
471
521
 
472
522
  private resetOperations(): void {
523
+ this.generation += 1;
473
524
  if (this.pushTimer) clearTimeout(this.pushTimer);
474
525
  this.pushTimer = null;
475
526
  if (this.observeTimer) clearTimeout(this.observeTimer);