gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { act, useEffect, useRef, useState } from "react";
3
3
  import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
4
- import { TextAttributes } from "../../ui";
5
4
  import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
6
5
  import { ChoiceDialog } from "./choice-dialog";
7
6
  import { DataTable, type DataTableColumn } from "./data-table";
@@ -171,37 +170,6 @@ function DataTableHorizontalScrollHarness({
171
170
  );
172
171
  }
173
172
 
174
- function DataTableColumnGapHarness() {
175
- const headerScrollRef = useRef<ScrollBoxRenderable>(null);
176
- const scrollRef = useRef<ScrollBoxRenderable>(null);
177
- const items = [{ id: "row-0", left: "AA", right: "BB" }];
178
-
179
- return (
180
- <DataTableView
181
- rootWidth={8}
182
- rootHeight={4}
183
- selection={{ kind: "none" }}
184
- columns={[
185
- { id: "left", label: "L", width: 2, align: "left" },
186
- { id: "right", label: "R", width: 2, align: "left" },
187
- ]}
188
- items={items}
189
- sortColumnId={null}
190
- sortDirection="asc"
191
- onHeaderClick={() => {}}
192
- headerScrollRef={headerScrollRef}
193
- scrollRef={scrollRef}
194
- getItemKey={(row) => row.id}
195
- renderCell={(row, column) => ({ text: column.id === "left" ? row.left : row.right })}
196
- emptyStateTitle="No rows."
197
- columnGap={0}
198
- horizontalPadding={0}
199
- fillAvailableWidth={false}
200
- showHorizontalScrollbar={false}
201
- />
202
- );
203
- }
204
-
205
173
  function DataTableVirtualizationHarness() {
206
174
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
207
175
  const scrollRef = useRef<ScrollBoxRenderable>(null);
@@ -392,31 +360,6 @@ describe("shared UI kit", () => {
392
360
  expect(lastSelected).toBe("chart");
393
361
  });
394
362
 
395
- test("marks the active bare tab when it has keyboard focus", async () => {
396
- testSetup = await testRender(
397
- <Tabs
398
- tabs={[
399
- { label: "One", value: "one" },
400
- { label: "Two", value: "two" },
401
- ]}
402
- activeValue="one"
403
- onSelect={() => {}}
404
- variant="bare"
405
- compact
406
- focused
407
- />,
408
- { width: 24, height: 2 },
409
- );
410
-
411
- await act(async () => {
412
- await testSetup!.renderOnce();
413
- });
414
-
415
- const activeSpan = testSetup.captureSpans().lines[0]?.spans.find((span) => span.text.includes("One"));
416
- expect(activeSpan).toBeDefined();
417
- expect((activeSpan!.attributes & TextAttributes.UNDERLINE)).toBe(TextAttributes.UNDERLINE);
418
- });
419
-
420
363
  test("selects tabs by clicking their text labels", async () => {
421
364
  let lastSelected = "overview";
422
365
  const selectedValues: string[] = [];
@@ -794,27 +737,6 @@ describe("shared UI kit", () => {
794
737
  expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
795
738
  });
796
739
 
797
- test("renders data table columns without spacer cells when column gap is zero", async () => {
798
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
799
- testSetup = await testRender(
800
- <AppContext value={{ state, dispatch: () => {} }}>
801
- <PaneInstanceProvider paneId="portfolio-list:main">
802
- <DataTableColumnGapHarness />
803
- </PaneInstanceProvider>
804
- </AppContext>,
805
- { width: 8, height: 4 },
806
- );
807
-
808
- await act(async () => {
809
- await testSetup!.renderOnce();
810
- await testSetup!.renderOnce();
811
- });
812
-
813
- const frame = testSetup.captureCharFrame();
814
- expect(frame).toContain("AABB");
815
- expect(frame).not.toContain("AA BB");
816
- });
817
-
818
740
  test("hides data table horizontal scrolling when content fits", async () => {
819
741
  const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
820
742
  tableScrollBoxForTest = null;
@@ -2,7 +2,6 @@ import { cloneLayout, createBlankLayout, type SavedLayout } from "../../../types
2
2
  import {
3
3
  clonePaneStateMap,
4
4
  cloneSavedLayout,
5
- getActiveSavedPaneState,
6
5
  historyForIndex,
7
6
  removeHistoryIndex,
8
7
  setHistoryForIndex,
@@ -60,9 +60,9 @@ function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefine
60
60
  const docked = (() => {
61
61
  const raw = (value as PanePlacementMemory).docked;
62
62
  if (!raw || typeof raw !== "object") return undefined;
63
- const path = Array.isArray((raw as { path?: unknown }).path)
64
- ? (raw as { path?: unknown }).path
65
- ?.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
63
+ const rawPath = (raw as { path?: unknown }).path;
64
+ const path = Array.isArray(rawPath)
65
+ ? rawPath.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
66
66
  : undefined;
67
67
  const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
68
68
  ? (raw as { anchorInstanceId: string }).anchorInstanceId
@@ -291,6 +291,12 @@ export class ResourceStore {
291
291
  }
292
292
  records.push(record);
293
293
  }
294
+ if (options.touch !== false) {
295
+ for (const record of records) {
296
+ const touchedAt = this.touch(record);
297
+ if (touchedAt != null) record.lastAccessedAt = touchedAt;
298
+ }
299
+ }
294
300
  return records;
295
301
  }
296
302
 
@@ -35,6 +35,7 @@ describe("AppPersistence", () => {
35
35
  }, {
36
36
  cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
37
37
  schemaVersion: 1,
38
+ fetchedAt: 1,
38
39
  });
39
40
 
40
41
  expect(persistence.resources.get<{ price: number }>({
@@ -43,6 +44,10 @@ describe("AppPersistence", () => {
43
44
  entityKey: "AAPL",
44
45
  sourceKey: "provider:yahoo",
45
46
  }, { allowExpired: true })?.value.price).toBe(123);
47
+ const listed = persistence.resources.list<{ price: number }>({
48
+ namespace: "market", kind: "quote", entityKey: "AAPL",
49
+ }, { allowExpired: true });
50
+ expect(listed[0]?.lastAccessedAt).toBeGreaterThan(1);
46
51
  persistence.close();
47
52
  });
48
53
 
@@ -47,11 +47,4 @@ export class TickerStore {
47
47
  this.db.query("DELETE FROM tickers WHERE symbol = ?").run(symbol);
48
48
  });
49
49
  }
50
-
51
- count(): number {
52
- const row = withSqliteBusyRetry("count tickers", () => (
53
- this.db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM tickers").get()
54
- ));
55
- return row?.count ?? 0;
56
- }
57
50
  }
@@ -1,14 +1,12 @@
1
1
  import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
- import type { NewsArticle } from "../../news/types";
3
2
  import type { OptionsChain } from "../../types/financials";
4
- import type { NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
3
+ import type { OptionsRequest, SecFilingsRequest } from "../request-types";
5
4
  import type { QueryEntry } from "../result-types";
6
5
  import { QueryStore } from "../query-store";
7
6
  import {
8
7
  ARTICLE_SUMMARY_CACHE_TTL_MS,
9
8
  EXPECTED_EMPTY,
10
9
  FX_CACHE_TTL_MS,
11
- NEWS_CACHE_TTL_MS,
12
10
  OPTIONS_CACHE_TTL_MS,
13
11
  SEC_CONTENT_CACHE_TTL_MS,
14
12
  SEC_FILINGS_CACHE_TTL_MS,
@@ -23,7 +21,6 @@ import {
23
21
  import {
24
22
  buildArticleSummaryKey,
25
23
  buildFxKey,
26
- buildNewsKey,
27
24
  buildOptionsKey,
28
25
  buildSecContentKey,
29
26
  buildSecDocumentsKey,
@@ -42,46 +39,6 @@ interface AuxiliaryLoaderOptions<T> {
42
39
  load: (startedAt: number) => Promise<QueryEntry<T>>;
43
40
  }
44
41
 
45
- interface NewsDataProvider extends DataProvider {
46
- getNews?: (query: {
47
- feed: "ticker";
48
- ticker: string;
49
- exchange?: string;
50
- tickerTier: "primary";
51
- limit?: number;
52
- }) => Promise<NewsArticle[]>;
53
- }
54
-
55
- export function loadNewsEntry(options: {
56
- dataProvider: DataProvider;
57
- request: NewsRequest;
58
- store: QueryStore<NewsArticle[]>;
59
- runSingleFlight: RunSingleFlight;
60
- }): Promise<QueryEntry<NewsArticle[]>> {
61
- const { dataProvider, request, store, runSingleFlight } = options;
62
- const key = buildNewsKey(request);
63
- return loadAuxiliaryEntry({
64
- dataProvider,
65
- key,
66
- store,
67
- ttlMs: NEWS_CACHE_TTL_MS,
68
- runSingleFlight,
69
- load: async (startedAt) => {
70
- const newsProvider = dataProvider as NewsDataProvider;
71
- if (!newsProvider.getNews) throw new Error("No news provider available");
72
- const data = await newsProvider.getNews({
73
- feed: "ticker",
74
- ticker: request.instrument.symbol,
75
- exchange: request.instrument.exchange ?? "",
76
- tickerTier: "primary",
77
- limit: request.count ?? 50,
78
- });
79
- const attempts = [createAttempt(dataProvider.id, startedAt, data.length > 0 ? "success" : "empty", data.length === 0 ? "NO_DATA" : undefined)];
80
- return store.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
81
- },
82
- });
83
- }
84
-
85
42
  export function loadOptionsEntry(options: {
86
43
  dataProvider: DataProvider;
87
44
  request: OptionsRequest;
@@ -8,7 +8,6 @@ const EMPTY_MESSAGE = "No data available";
8
8
  export const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
9
9
  export const SNAPSHOT_CACHE_TTL_MS = 5 * 60_000;
10
10
  export const CHART_CACHE_TTL_MS = 10 * 60_000;
11
- export const NEWS_CACHE_TTL_MS = 2 * 60_000;
12
11
  export const OPTIONS_CACHE_TTL_MS = 10 * 60_000;
13
12
  export const SEC_FILINGS_CACHE_TTL_MS = 10 * 60_000;
14
13
  export const SEC_CONTENT_CACHE_TTL_MS = 24 * 60 * 60_000;
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { MarketDataCoordinatorEvents } from "./events";
3
+
4
+ const waitMicrotask = () => Promise.resolve();
5
+ const waitTimer = () => new Promise((resolve) => setTimeout(resolve, 0));
6
+
7
+ describe("MarketDataCoordinatorEvents", () => {
8
+ test("coalesces version bumps before notifying external-store listeners", async () => {
9
+ const events = new MarketDataCoordinatorEvents();
10
+ const calls: number[] = [];
11
+ events.subscribeKeys(["quote:AMD"], () => {
12
+ calls.push(events.getKeysVersion(["quote:AMD"]));
13
+ });
14
+
15
+ events.bump("quote:AMD");
16
+ events.bump("quote:AMD");
17
+
18
+ expect(events.getKeysVersion(["quote:AMD"])).toBe(0);
19
+ expect(calls).toEqual([]);
20
+
21
+ await waitMicrotask();
22
+ expect(events.getKeysVersion(["quote:AMD"])).toBe(1);
23
+ expect(calls).toEqual([]);
24
+
25
+ await waitTimer();
26
+ expect(calls).toEqual([1]);
27
+ });
28
+
29
+ test("delivers bumps scheduled during notification in a later notification pass", async () => {
30
+ const events = new MarketDataCoordinatorEvents();
31
+ const order: string[] = [];
32
+ events.subscribeKeys(["quote:AMD"], () => {
33
+ order.push("AMD");
34
+ events.bump("quote:NVDA");
35
+ });
36
+ events.subscribeKeys(["quote:NVDA"], () => {
37
+ order.push("NVDA");
38
+ });
39
+
40
+ events.bump("quote:AMD");
41
+ await waitMicrotask();
42
+ await waitTimer();
43
+ expect(order).toEqual(["AMD"]);
44
+
45
+ await waitMicrotask();
46
+ await waitTimer();
47
+ expect(order).toEqual(["AMD", "NVDA"]);
48
+ });
49
+ });
@@ -4,6 +4,8 @@ export class MarketDataCoordinatorEvents {
4
4
  private version = 0;
5
5
  private pendingVersionBump = false;
6
6
  private pendingChangedKeys = new Set<string>();
7
+ private pendingNotify = false;
8
+ private pendingListeners = new Set<() => void>();
7
9
  private readonly listeners = new Set<() => void>();
8
10
  private readonly keyListeners = new Map<string, Set<() => void>>();
9
11
  private readonly keyVersions = new Map<string, number>();
@@ -60,15 +62,34 @@ export class MarketDataCoordinatorEvents {
60
62
  for (const key of changedKeys) {
61
63
  this.keyVersions.set(key, (this.keyVersions.get(key) ?? 0) + 1);
62
64
  }
63
- const keyListeners = new Set<() => void>();
65
+ for (const listener of this.listeners) {
66
+ this.pendingListeners.add(listener);
67
+ }
64
68
  for (const key of changedKeys) {
65
69
  for (const listener of this.keyListeners.get(key) ?? []) {
66
- keyListeners.add(listener);
70
+ this.pendingListeners.add(listener);
67
71
  }
68
72
  }
69
73
 
70
- for (const listener of this.listeners) listener();
71
- for (const listener of keyListeners) listener();
74
+ this.scheduleNotify();
72
75
  }, { changedKeyCount: changedKeys.size });
73
76
  }
77
+
78
+ private scheduleNotify(): void {
79
+ if (this.pendingNotify) return;
80
+ this.pendingNotify = true;
81
+ setTimeout(() => this.flushNotify(), 0);
82
+ }
83
+
84
+ private flushNotify(): void {
85
+ this.pendingNotify = false;
86
+ const listeners = [...this.pendingListeners];
87
+ this.pendingListeners.clear();
88
+ for (const listener of listeners) {
89
+ listener();
90
+ }
91
+ if (this.pendingListeners.size > 0) {
92
+ this.scheduleNotify();
93
+ }
94
+ }
74
95
  }
@@ -155,13 +155,8 @@ describe("MarketDataCoordinator", () => {
155
155
  });
156
156
 
157
157
  it("reuses fresh empty tab query results instead of refetching on reopen", async () => {
158
- let newsCalls = 0;
159
158
  let optionsCalls = 0;
160
159
  const provider = createProvider({
161
- getNews: async () => {
162
- newsCalls += 1;
163
- return [];
164
- },
165
160
  getOptionsChain: async () => {
166
161
  optionsCalls += 1;
167
162
  return {
@@ -175,16 +170,11 @@ describe("MarketDataCoordinator", () => {
175
170
  const coordinator = new MarketDataCoordinator(provider);
176
171
  const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
177
172
 
178
- const firstNews = await coordinator.loadNews({ instrument, count: 20 });
179
- const secondNews = await coordinator.loadNews({ instrument, count: 20 });
180
173
  const firstOptions = await coordinator.loadOptions({ instrument });
181
174
  const secondOptions = await coordinator.loadOptions({ instrument });
182
175
 
183
- expect(firstNews.error?.reasonCode).toBe("NO_DATA");
184
- expect(secondNews.error?.reasonCode).toBe("NO_DATA");
185
176
  expect(firstOptions.error?.reasonCode).toBe("NO_DATA");
186
177
  expect(secondOptions.error?.reasonCode).toBe("NO_DATA");
187
- expect(newsCalls).toBe(1);
188
178
  expect(optionsCalls).toBe(1);
189
179
  });
190
180
 
@@ -347,6 +337,229 @@ describe("MarketDataCoordinator", () => {
347
337
  expect(coordinator.getQuoteEntry(instrument).data?.price).toBe(412.5);
348
338
  });
349
339
 
340
+ it("reconciles stream quotes with cached provider day references before a snapshot loads", () => {
341
+ let streamed: ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null = null;
342
+ const provider = createProvider({
343
+ getCachedFinancialsForTargets: () => new Map([[
344
+ "VICR",
345
+ {
346
+ quote: {
347
+ symbol: "VICR",
348
+ providerId: "gloomberb-cloud",
349
+ price: 292.83,
350
+ currency: "USD",
351
+ previousClose: 282.95,
352
+ change: 9.88,
353
+ changePercent: 3.49,
354
+ lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
355
+ marketState: "REGULAR",
356
+ dataSource: "live",
357
+ },
358
+ quoteContributions: {
359
+ "gloomberb-cloud": {
360
+ symbol: "VICR",
361
+ providerId: "gloomberb-cloud",
362
+ price: 292.83,
363
+ currency: "USD",
364
+ previousClose: 380.07,
365
+ change: -87.24,
366
+ changePercent: -22.95,
367
+ lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
368
+ marketState: "REGULAR",
369
+ dataSource: "live",
370
+ },
371
+ yahoo: {
372
+ symbol: "VICR",
373
+ providerId: "yahoo",
374
+ price: 294.39,
375
+ currency: "USD",
376
+ previousClose: 282.95,
377
+ change: 11.44,
378
+ changePercent: 4.04,
379
+ lastUpdated: Date.parse("2026-07-06T16:45:37Z"),
380
+ marketState: "REGULAR",
381
+ dataSource: "delayed",
382
+ },
383
+ },
384
+ annualStatements: [],
385
+ quarterlyStatements: [],
386
+ priceHistory: [],
387
+ },
388
+ ]]),
389
+ subscribeQuotes: (_targets, onQuote) => {
390
+ streamed = onQuote as typeof streamed;
391
+ return () => {};
392
+ },
393
+ });
394
+ const coordinator = new MarketDataCoordinator(provider);
395
+ const instrument = {
396
+ symbol: "VICR",
397
+ exchange: "NASDAQ",
398
+ brokerId: "ibkr",
399
+ brokerInstanceId: "ibkr-work",
400
+ instrument: {
401
+ brokerId: "ibkr",
402
+ brokerInstanceId: "ibkr-work",
403
+ conId: 275759,
404
+ symbol: "VICR",
405
+ },
406
+ };
407
+
408
+ coordinator.subscribeQuotes([{ instrument }]);
409
+ const onStreamed = streamed as ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null;
410
+ if (!onStreamed) throw new Error("expected streaming callback");
411
+ onStreamed(
412
+ {
413
+ symbol: "VICR",
414
+ exchange: "NASDAQ",
415
+ context: {
416
+ brokerId: "ibkr",
417
+ brokerInstanceId: "ibkr-work",
418
+ instrument: instrument.instrument,
419
+ },
420
+ },
421
+ {
422
+ symbol: "VICR",
423
+ providerId: "gloomberb-cloud",
424
+ price: 293.07,
425
+ currency: "USD",
426
+ previousClose: 380.07,
427
+ change: -87,
428
+ changePercent: -22.89,
429
+ lastUpdated: Date.parse("2026-07-06T17:17:00Z"),
430
+ marketState: "REGULAR",
431
+ dataSource: "live",
432
+ },
433
+ );
434
+
435
+ const quote = coordinator.getQuoteEntry(instrument).data;
436
+ expect(quote?.price).toBe(293.07);
437
+ expect(quote?.previousClose).toBe(282.95);
438
+ expect(quote?.changePercent).toBeCloseTo(((293.07 - 282.95) / 282.95) * 100, 10);
439
+ expect(quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
440
+ });
441
+
442
+ it("reconciles batch quote loads with cached provider day references before a snapshot loads", async () => {
443
+ const provider = createProvider({
444
+ getCachedFinancialsForTargets: () => new Map([[
445
+ "VICR",
446
+ {
447
+ quote: {
448
+ symbol: "VICR",
449
+ providerId: "gloomberb-cloud",
450
+ price: 292.83,
451
+ currency: "USD",
452
+ previousClose: 282.95,
453
+ change: 9.88,
454
+ changePercent: 3.49,
455
+ lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
456
+ marketState: "REGULAR",
457
+ dataSource: "live",
458
+ },
459
+ quoteContributions: {
460
+ "gloomberb-cloud": {
461
+ symbol: "VICR",
462
+ providerId: "gloomberb-cloud",
463
+ price: 292.83,
464
+ currency: "USD",
465
+ previousClose: 380.07,
466
+ change: -87.24,
467
+ changePercent: -22.95,
468
+ lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
469
+ marketState: "REGULAR",
470
+ dataSource: "live",
471
+ },
472
+ yahoo: {
473
+ symbol: "VICR",
474
+ providerId: "yahoo",
475
+ price: 294.39,
476
+ currency: "USD",
477
+ previousClose: 282.95,
478
+ change: 11.44,
479
+ changePercent: 4.04,
480
+ lastUpdated: Date.parse("2026-07-06T16:45:37Z"),
481
+ marketState: "REGULAR",
482
+ dataSource: "delayed",
483
+ },
484
+ },
485
+ annualStatements: [],
486
+ quarterlyStatements: [],
487
+ priceHistory: [],
488
+ },
489
+ ]]),
490
+ getQuotesBatch: async (targets) => targets.map((target) => ({
491
+ target,
492
+ quote: {
493
+ symbol: target.symbol,
494
+ providerId: "gloomberb-cloud",
495
+ price: 293.07,
496
+ currency: "USD",
497
+ previousClose: 380.07,
498
+ change: -87,
499
+ changePercent: -22.89,
500
+ lastUpdated: Date.parse("2026-07-06T17:17:00Z"),
501
+ marketState: "REGULAR",
502
+ dataSource: "live",
503
+ },
504
+ })),
505
+ });
506
+ const coordinator = new MarketDataCoordinator(provider);
507
+ const instrument = {
508
+ symbol: "VICR",
509
+ exchange: "NASDAQ",
510
+ brokerId: "ibkr",
511
+ brokerInstanceId: "ibkr-work",
512
+ instrument: {
513
+ brokerId: "ibkr",
514
+ brokerInstanceId: "ibkr-work",
515
+ conId: 275759,
516
+ symbol: "VICR",
517
+ },
518
+ };
519
+
520
+ await coordinator.loadQuotesBatch([instrument], { forceRefresh: true });
521
+
522
+ const quote = coordinator.getQuoteEntry(instrument).data;
523
+ expect(quote?.price).toBe(293.07);
524
+ expect(quote?.previousClose).toBe(282.95);
525
+ expect(quote?.changePercent).toBeCloseTo(((293.07 - 282.95) / 282.95) * 100, 10);
526
+ expect(quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
527
+ });
528
+
529
+ it("preserves requested quote stream routes", () => {
530
+ let subscribedTargets: QuoteSubscriptionTarget[] = [];
531
+ const provider = createProvider({
532
+ subscribeQuotes: (targets) => {
533
+ subscribedTargets = targets;
534
+ return () => {};
535
+ },
536
+ });
537
+ const coordinator = new MarketDataCoordinator(provider);
538
+
539
+ coordinator.subscribeQuotes([{
540
+ instrument: {
541
+ symbol: "AAPL",
542
+ exchange: "NASDAQ",
543
+ brokerId: "ibkr",
544
+ brokerInstanceId: "ibkr-work",
545
+ },
546
+ priority: { route: "provider", surface: "portfolio", visible: true },
547
+ }]);
548
+
549
+ expect(subscribedTargets).toEqual([{
550
+ symbol: "AAPL",
551
+ exchange: "NASDAQ",
552
+ route: "provider",
553
+ context: {
554
+ brokerId: "ibkr",
555
+ brokerInstanceId: "ibkr-work",
556
+ instrument: null,
557
+ },
558
+ surface: "portfolio",
559
+ visible: true,
560
+ }]);
561
+ });
562
+
350
563
  it("routes manual-resolution chart requests through the resolution-aware provider path", async () => {
351
564
  let requested: { range: string; resolution: string } | null = null;
352
565
  const provider = createProvider({