@gloom-sh/terminal 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -1,8 +1,9 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import type { CachePolicy, PersistedResourceValue } from "../types/persistence";
3
3
  import type { JsonValue } from "./sqlite/json";
4
+ import { createSqliteAccess, type SqliteAccess } from "./sqlite/access";
4
5
  import { safeParseJson, serializeJson } from "./sqlite/json";
5
- import { trySqliteBusyOperation, withSqliteBusyRetry } from "./sqlite/retry";
6
+ import { logFailure } from "../utils/log-failure";
6
7
 
7
8
  const RESOURCE_CACHE_SOFT_ROW_LIMIT = 25_000;
8
9
  const RESOURCE_CACHE_SOFT_SIZE_LIMIT = 100 * 1024 * 1024;
@@ -13,6 +14,13 @@ const RESOURCE_CACHE_MAINTENANCE_BYTE_INTERVAL = Math.floor(
13
14
  const RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS = 5 * 60_000;
14
15
  /** Rows read per eviction pass, so a full cache is evicted in slices. */
15
16
  const RESOURCE_CACHE_PRUNE_BATCH_SIZE = 512;
17
+ /**
18
+ * A read moves its row up the eviction order in memory, and the rows read
19
+ * since are written in one transaction this often, before an eviction and
20
+ * at close: a read writes nothing. A crash loses at most this much of the
21
+ * order, which only decides what is evicted first.
22
+ */
23
+ const RESOURCE_CACHE_TOUCH_FLUSH_MS = 30_000;
16
24
  const DEFAULT_RESOURCE_SCHEMA_VERSION = 1;
17
25
  const EMPTY_VARIANT_KEY = "";
18
26
  const EMPTY_SOURCE_KEY = "";
@@ -93,6 +101,10 @@ function isStale(staleAt: number): boolean {
93
101
  return staleAt < Date.now();
94
102
  }
95
103
 
104
+ function rowMapKey(key: ResourceCacheKey): string {
105
+ return [key.namespace, key.kind, key.entityKey, normalizeVariantKey(key.variantKey), normalizeSourceKey(key.sourceKey)].join("\u0000");
106
+ }
107
+
96
108
  function sortRows(rows: ResourceCacheRow[]): ResourceCacheRow[] {
97
109
  return [...rows].sort((a, b) => {
98
110
  if (a.expires_at !== b.expires_at) return b.expires_at - a.expires_at;
@@ -106,8 +118,18 @@ export class ResourceStore {
106
118
  private writesSinceMaintenance = 0;
107
119
  private bytesWrittenSinceMaintenance = 0;
108
120
  private lastMaintenanceAt = 0;
121
+ /** Reads not yet written to the eviction order, by row. */
122
+ private readonly pendingTouches = new Map<string, { key: ResourceCacheKey; touchedAt: number }>();
123
+ private touchTimer: ReturnType<typeof setTimeout> | null = null;
109
124
 
110
- constructor(private readonly db: Database) {}
125
+ /**
126
+ * Everything here can be fetched again, so no statement waits for another
127
+ * program's lock: a locked read is a miss, and a locked write is skipped.
128
+ */
129
+ constructor(
130
+ private readonly db: Database,
131
+ private readonly access: SqliteAccess = createSqliteAccess(db),
132
+ ) {}
111
133
 
112
134
  private toCachedResource<T>(row: ResourceCacheRow): CachedResourceRecord<T> | null {
113
135
  const value = safeParseJson<T>(row.payload);
@@ -132,7 +154,8 @@ export class ResourceStore {
132
154
  }
133
155
 
134
156
  private deleteExact(key: ResourceCacheKey): void {
135
- withSqliteBusyRetry("delete cached resource", () => {
157
+ this.pendingTouches.delete(rowMapKey(key));
158
+ this.access.writeCache("delete cached resource", () => {
136
159
  this.db
137
160
  .query(
138
161
  `DELETE FROM resource_cache
@@ -148,74 +171,124 @@ export class ResourceStore {
148
171
  });
149
172
  }
150
173
 
151
- private touchDue(record: { lastAccessedAt: number }, options: GetResourceOptions): boolean {
152
- return options.touchIntervalMs == null || Date.now() - record.lastAccessedAt >= options.touchIntervalMs;
174
+ /** Moves a read row up the eviction order, unless it moved within `touchIntervalMs`. */
175
+ private touchRead(record: CachedResourceRecord, options: GetResourceOptions): void {
176
+ const mapKey = rowMapKey(record);
177
+ record.lastAccessedAt = Math.max(record.lastAccessedAt, this.pendingTouches.get(mapKey)?.touchedAt ?? 0);
178
+ if (options.touch === false) return;
179
+ const now = Date.now();
180
+ if (options.touchIntervalMs != null && now - record.lastAccessedAt < options.touchIntervalMs) return;
181
+ const { namespace, kind, entityKey, variantKey, sourceKey } = record;
182
+ this.pendingTouches.set(mapKey, { key: { namespace, kind, entityKey, variantKey, sourceKey }, touchedAt: now });
183
+ record.lastAccessedAt = now;
184
+ if (this.touchTimer) return;
185
+ this.touchTimer = setTimeout(() => {
186
+ this.touchTimer = null;
187
+ try {
188
+ this.flushTouches();
189
+ } catch (error) {
190
+ logFailure("data.resource-store", "Saving the cache's eviction order failed")(error);
191
+ }
192
+ }, RESOURCE_CACHE_TOUCH_FLUSH_MS);
193
+ (this.touchTimer as { unref?: () => void }).unref?.();
153
194
  }
154
195
 
155
- private touch(key: ResourceCacheKey): number | null {
156
- const touchedAt = Date.now();
157
- const result = trySqliteBusyOperation("touch cached resource", () => {
158
- this.db
159
- .query(
160
- `UPDATE resource_cache
161
- SET last_accessed_at = ?
162
- WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
163
- )
164
- .run(
165
- touchedAt,
166
- key.namespace,
167
- key.kind,
168
- key.entityKey,
169
- normalizeVariantKey(key.variantKey),
170
- normalizeSourceKey(key.sourceKey),
171
- );
172
- return touchedAt;
196
+ /**
197
+ * Writes the reads since the last flush into the eviction order, in one
198
+ * transaction. False when another program had the cache locked; they are
199
+ * kept for the next one.
200
+ */
201
+ flushTouches(): boolean {
202
+ if (this.touchTimer) {
203
+ clearTimeout(this.touchTimer);
204
+ this.touchTimer = null;
205
+ }
206
+ if (this.pendingTouches.size === 0) return true;
207
+ const touches = [...this.pendingTouches.values()];
208
+ const written = this.access.writeCache("touch cached resources", () => {
209
+ const update = this.db.query(
210
+ `UPDATE resource_cache
211
+ SET last_accessed_at = MAX(last_accessed_at, ?)
212
+ WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
213
+ );
214
+ this.db.transaction(() => {
215
+ for (const { key, touchedAt } of touches) {
216
+ update.run(
217
+ touchedAt,
218
+ key.namespace,
219
+ key.kind,
220
+ key.entityKey,
221
+ normalizeVariantKey(key.variantKey),
222
+ normalizeSourceKey(key.sourceKey),
223
+ );
224
+ }
225
+ })();
173
226
  });
174
- return result;
227
+ if (written) this.pendingTouches.clear();
228
+ return written;
229
+ }
230
+
231
+ /**
232
+ * Writes the reads still held in memory; the connection closes after. A
233
+ * failure loses only that order: the queued user writes and the
234
+ * connection still have to close.
235
+ */
236
+ close(): void {
237
+ try {
238
+ this.flushTouches();
239
+ } catch (error) {
240
+ logFailure("data.resource-store", "Saving the cache's eviction order at close failed")(error);
241
+ }
242
+ if (this.touchTimer) clearTimeout(this.touchTimer);
243
+ this.touchTimer = null;
175
244
  }
176
245
 
177
- private readCacheFootprint(operationName: string): { count: number; totalSize: number } {
178
- const stats = withSqliteBusyRetry(operationName, () => (
246
+ /** Null when another program has the cache locked; maintenance waits for the next write. */
247
+ private readCacheFootprint(operationName: string): { count: number; totalSize: number } | null {
248
+ const stats = this.access.readCache(operationName, () => (
179
249
  this.db
180
250
  .query<{ count: number; total_size: number }, []>(
181
251
  "SELECT COUNT(*) as count, COALESCE(SUM(size_bytes), 0) as total_size FROM resource_cache",
182
252
  )
183
253
  .get()
184
254
  ));
185
- return { count: stats?.count ?? 0, totalSize: stats?.total_size ?? 0 };
255
+ return stats ? { count: stats.count ?? 0, totalSize: stats.total_size ?? 0 } : null;
186
256
  }
187
257
 
188
258
  private withinSoftLimits(count: number, totalSize: number): boolean {
189
259
  return count <= RESOURCE_CACHE_SOFT_ROW_LIMIT && totalSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT;
190
260
  }
191
261
 
192
- private pruneIfNeeded(): void {
193
- const { count, totalSize } = this.readCacheFootprint("read resource cache size");
194
- if (this.withinSoftLimits(count, totalSize)) return;
262
+ /** False when another program had the cache locked and pruning has to try again later. */
263
+ private pruneIfNeeded(): boolean {
264
+ const footprint = this.readCacheFootprint("read resource cache size");
265
+ if (!footprint) return false;
266
+ if (this.withinSoftLimits(footprint.count, footprint.totalSize)) return true;
195
267
 
196
- withSqliteBusyRetry("prune expired cached resources", () => {
268
+ const prunedExpired = this.access.writeCache("prune expired cached resources", () => {
197
269
  this.db.query("DELETE FROM resource_cache WHERE expires_at < ?").run(Date.now());
198
270
  });
199
-
200
- let { count: remainingCount, totalSize: remainingSize } = this.readCacheFootprint("read pruned resource cache size");
201
- if (this.withinSoftLimits(remainingCount, remainingSize)) return;
202
-
203
- const candidates = this.db.query<{
204
- namespace: string;
205
- kind: string;
206
- entity_key: string;
207
- variant_key: string;
208
- source_key: string;
209
- size_bytes: number;
210
- }, [number]>(
211
- `SELECT namespace, kind, entity_key, variant_key, source_key, size_bytes
271
+ if (!prunedExpired) return false;
272
+
273
+ const remaining = this.readCacheFootprint("read pruned resource cache size");
274
+ if (!remaining) return false;
275
+ let { count: remainingCount, totalSize: remainingSize } = remaining;
276
+ if (this.withinSoftLimits(remainingCount, remainingSize)) return true;
277
+ // Eviction goes by the order on disk, which has to hold the latest reads.
278
+ if (!this.flushTouches()) return false;
279
+
280
+ // Only the row ids, which the recency index holds: a row's size sits
281
+ // after its payload, and reading it walks the whole payload from disk.
282
+ // Asking it of the oldest 512 rows read 10 MB to pick what to evict.
283
+ const candidates = this.db.query<{ rowid: number }, [number]>(
284
+ `SELECT rowid
212
285
  FROM resource_cache
213
286
  ORDER BY last_accessed_at ASC, fetched_at ASC
214
287
  LIMIT ?`,
215
288
  );
216
- const remove = this.db.query(
217
- `DELETE FROM resource_cache
218
- WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
289
+ // Deleting a row reads its payload anyway, to free it: the size comes along.
290
+ const remove = this.db.query<{ size_bytes: number }, [number]>(
291
+ "DELETE FROM resource_cache WHERE rowid = ? RETURNING size_bytes",
219
292
  );
220
293
 
221
294
  // A cache at its limit holds tens of thousands of rows, so eviction reads
@@ -223,29 +296,31 @@ export class ResourceStore {
223
296
  // for more only if it is still over. Reading every row first turned the
224
297
  // moment the cache filled up into a multi-second pause.
225
298
  while (!this.withinSoftLimits(remainingCount, remainingSize)) {
226
- const batch = withSqliteBusyRetry("select cached resources to prune", () => (
299
+ const batch = this.access.readCache("select cached resources to prune", () => (
227
300
  candidates.all(RESOURCE_CACHE_PRUNE_BATCH_SIZE)
228
301
  ));
229
- if (batch.length === 0) return;
230
-
231
- const rowsToDelete = [] as typeof batch;
232
- for (const row of batch) {
233
- if (this.withinSoftLimits(remainingCount, remainingSize)) break;
234
- rowsToDelete.push(row);
235
- remainingCount -= 1;
236
- remainingSize -= row.size_bytes;
237
- }
238
- if (rowsToDelete.length === 0) return;
302
+ if (!batch) return false;
303
+ if (batch.length === 0) return true;
239
304
 
240
- withSqliteBusyRetry("prune least recently used cached resources", () => {
305
+ let left = { count: remainingCount, size: remainingSize };
306
+ const pruned = this.access.writeCache("prune least recently used cached resources", () => {
307
+ left = { count: remainingCount, size: remainingSize };
241
308
  const tx = this.db.transaction(() => {
242
- for (const row of rowsToDelete) {
243
- remove.run(row.namespace, row.kind, row.entity_key, row.variant_key, row.source_key);
309
+ for (const { rowid } of batch) {
310
+ if (this.withinSoftLimits(left.count, left.size)) break;
311
+ const removed = remove.get(rowid);
312
+ if (!removed) continue;
313
+ left.count -= 1;
314
+ left.size -= removed.size_bytes;
244
315
  }
245
316
  });
246
317
  tx();
247
318
  });
319
+ if (!pruned) return false;
320
+ remainingCount = left.count;
321
+ remainingSize = left.size;
248
322
  }
323
+ return true;
249
324
  }
250
325
 
251
326
  private maintainCacheAfterWrite(now: number, sizeBytes: number): void {
@@ -257,14 +332,14 @@ export class ResourceStore {
257
332
  || now - this.lastMaintenanceAt >= RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS;
258
333
  if (!maintenanceDue) return;
259
334
 
260
- this.pruneIfNeeded();
335
+ if (!this.pruneIfNeeded()) return;
261
336
  this.writesSinceMaintenance = 0;
262
337
  this.bytesWrittenSinceMaintenance = 0;
263
338
  this.lastMaintenanceAt = now;
264
339
  }
265
340
 
266
341
  get<T>(key: ResourceCacheKey, options: GetResourceOptions = {}): CachedResourceRecord<T> | null {
267
- const row = withSqliteBusyRetry("load cached resource", () => (
342
+ const row = this.access.readCache("load cached resource", () => (
268
343
  this.db
269
344
  .query<ResourceCacheRow, [string, string, string, string, string]>(
270
345
  `SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
@@ -294,12 +369,7 @@ export class ResourceStore {
294
369
  return null;
295
370
  }
296
371
 
297
- if (options.touch !== false && this.touchDue(record, options)) {
298
- const touchedAt = this.touch(key);
299
- if (touchedAt != null) {
300
- record.lastAccessedAt = touchedAt;
301
- }
302
- }
372
+ this.touchRead(record, options);
303
373
  return record;
304
374
  }
305
375
 
@@ -307,25 +377,33 @@ export class ResourceStore {
307
377
  key: Pick<ResourceCacheKey, "namespace" | "kind" | "entityKey">,
308
378
  options: ListResourceOptions = {},
309
379
  ): CachedResourceRecord<T>[] {
310
- const rows = withSqliteBusyRetry("list cached resources", () => (
380
+ const variantKeys = options.variantKeys ? [...new Set(options.variantKeys.map(normalizeVariantKey))] : null;
381
+ const sourceKeys = options.sourceKeys ? [...new Set(options.sourceKeys.map(normalizeSourceKey))] : null;
382
+ if (variantKeys?.length === 0 || sourceKeys?.length === 0) return [];
383
+ // The rows asked for only: an entity can hold dozens of variants (every
384
+ // range of a price history, from every source), megabytes of payload
385
+ // that a filter after the read threw away. The keys come from the
386
+ // primary key's index. Expiry stays below: the column sits after the
387
+ // payload, and checking it here walked a 10 MB payload twice.
388
+ const filters = [
389
+ ...(variantKeys ? [`variant_key IN (${variantKeys.map(() => "?").join(", ")})`] : []),
390
+ ...(sourceKeys ? [`source_key IN (${sourceKeys.map(() => "?").join(", ")})`] : []),
391
+ ];
392
+ const params: string[] = [key.namespace, key.kind, key.entityKey, ...(variantKeys ?? []), ...(sourceKeys ?? [])];
393
+ const rows = this.access.readCache("list cached resources", () => (
311
394
  this.db
312
- .query<ResourceCacheRow, [string, string, string]>(
395
+ .query<ResourceCacheRow, string[]>(
313
396
  `SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
314
397
  fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
315
398
  FROM resource_cache
316
- WHERE namespace = ? AND kind = ? AND entity_key = ?`,
399
+ WHERE ${["namespace = ?", "kind = ?", "entity_key = ?", ...filters].join(" AND ")}`,
317
400
  )
318
- .all(key.namespace, key.kind, key.entityKey)
319
- ));
320
-
321
- const allowedVariantKeys = options.variantKeys ? new Set(options.variantKeys.map(normalizeVariantKey)) : null;
322
- const allowedSourceKeys = options.sourceKeys ? new Set(options.sourceKeys.map(normalizeSourceKey)) : null;
401
+ .all(...params)
402
+ )) ?? [];
323
403
 
324
404
  const records: CachedResourceRecord<T>[] = [];
325
405
  for (const row of sortRows(rows)) {
326
406
  if (!options.allowExpired && isExpired(row.expires_at)) continue;
327
- if (allowedVariantKeys && !allowedVariantKeys.has(row.variant_key)) continue;
328
- if (allowedSourceKeys && !allowedSourceKeys.has(row.source_key)) continue;
329
407
  if (options.schemaVersion != null && row.schema_version !== options.schemaVersion) {
330
408
  this.deleteExact({
331
409
  namespace: row.namespace,
@@ -349,13 +427,7 @@ export class ResourceStore {
349
427
  }
350
428
  records.push(record);
351
429
  }
352
- if (options.touch !== false) {
353
- for (const record of records) {
354
- if (!this.touchDue(record, options)) continue;
355
- const touchedAt = this.touch(record);
356
- if (touchedAt != null) record.lastAccessedAt = touchedAt;
357
- }
358
- }
430
+ for (const record of records) this.touchRead(record, options);
359
431
  return records;
360
432
  }
361
433
 
@@ -375,7 +447,8 @@ export class ResourceStore {
375
447
  sourceKey: normalizeSourceKey(key.sourceKey),
376
448
  };
377
449
 
378
- withSqliteBusyRetry("save cached resource", () => {
450
+ this.pendingTouches.delete(rowMapKey(rowKey));
451
+ const saved = this.access.writeCache("save cached resource", () => {
379
452
  this.db
380
453
  .query(
381
454
  `INSERT OR REPLACE INTO resource_cache (
@@ -400,7 +473,7 @@ export class ResourceStore {
400
473
  );
401
474
  });
402
475
 
403
- this.maintainCacheAfterWrite(Date.now(), sizeBytes);
476
+ if (saved) this.maintainCacheAfterWrite(Date.now(), sizeBytes);
404
477
  return {
405
478
  ...rowKey,
406
479
  value: JSON.parse(payload) as T,
@@ -420,14 +493,16 @@ export class ResourceStore {
420
493
  this.deleteExact(key);
421
494
  }
422
495
 
496
+ /** Asked for by the user, so it waits for another program's lock, and throws when it cannot get it. */
423
497
  clear(namespace?: string): void {
424
498
  if (!namespace) {
425
- withSqliteBusyRetry("clear cached resources", () => {
499
+ this.pendingTouches.clear();
500
+ this.access.run("clear cached resources", () => {
426
501
  this.db.query("DELETE FROM resource_cache").run();
427
502
  });
428
503
  return;
429
504
  }
430
- withSqliteBusyRetry("clear namespaced cached resources", () => {
505
+ this.access.run("clear namespaced cached resources", () => {
431
506
  this.db.query("DELETE FROM resource_cache WHERE namespace = ?").run(namespace);
432
507
  });
433
508
  }
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
+ import { createSqliteAccess, type SqliteAccess } from "./sqlite/access";
2
3
  import { safeParseJson, serializeJson } from "./sqlite/json";
3
- import { withSqliteBusyRetry } from "./sqlite/retry";
4
4
 
5
5
  const DEFAULT_SESSION_SCHEMA_VERSION = 1;
6
6
 
@@ -11,11 +11,23 @@ export interface SessionSnapshotRecord<T = unknown> {
11
11
  updatedAt: number;
12
12
  }
13
13
 
14
+ /** A queued write's row (null for a delete), one object per write. */
15
+ interface PendingSession {
16
+ row: { schema_version: number; value: string; updated_at: number } | null;
17
+ }
18
+
14
19
  export class SessionStore {
15
- constructor(private readonly db: Database) {}
20
+ /** Writes queued behind another program's lock, read back until they land. */
21
+ private readonly pending = new Map<string, PendingSession>();
22
+
23
+ constructor(
24
+ private readonly db: Database,
25
+ private readonly access: SqliteAccess = createSqliteAccess(db),
26
+ ) {}
16
27
 
17
28
  get<T>(sessionId = "app", schemaVersion = DEFAULT_SESSION_SCHEMA_VERSION): SessionSnapshotRecord<T> | null {
18
- const row = withSqliteBusyRetry("load session snapshot", () => (
29
+ const pending = this.pending.get(sessionId);
30
+ const row = pending ? pending.row : this.access.run("load session snapshot", () => (
19
31
  this.db
20
32
  .query<{ schema_version: number; value: string; updated_at: number }, [string]>(
21
33
  "SELECT schema_version, value, updated_at FROM session_snapshots WHERE session_id = ?",
@@ -24,12 +36,12 @@ export class SessionStore {
24
36
  ));
25
37
  if (!row) return null;
26
38
  if (row.schema_version !== schemaVersion) {
27
- this.delete(sessionId);
39
+ if (!pending) this.delete(sessionId);
28
40
  return null;
29
41
  }
30
42
  const value = safeParseJson<T>(row.value);
31
43
  if (value == null) {
32
- this.delete(sessionId);
44
+ if (!pending) this.delete(sessionId);
33
45
  return null;
34
46
  }
35
47
  return {
@@ -41,19 +53,30 @@ export class SessionStore {
41
53
  }
42
54
 
43
55
  set(sessionId: string, value: unknown, schemaVersion = DEFAULT_SESSION_SCHEMA_VERSION): void {
44
- withSqliteBusyRetry("save session snapshot", () => {
56
+ const row = { schema_version: schemaVersion, value: serializeJson(value), updated_at: Date.now() };
57
+ this.write("save session snapshot", sessionId, { row }, () => {
45
58
  this.db
46
59
  .query(
47
60
  `INSERT OR REPLACE INTO session_snapshots (session_id, schema_version, value, updated_at)
48
61
  VALUES (?, ?, ?, ?)`,
49
62
  )
50
- .run(sessionId, schemaVersion, serializeJson(value), Date.now());
63
+ .run(sessionId, row.schema_version, row.value, row.updated_at);
51
64
  });
52
65
  }
53
66
 
54
67
  delete(sessionId: string): void {
55
- withSqliteBusyRetry("delete session snapshot", () => {
68
+ this.write("delete session snapshot", sessionId, { row: null }, () => {
56
69
  this.db.query("DELETE FROM session_snapshots WHERE session_id = ?").run(sessionId);
57
70
  });
58
71
  }
72
+
73
+ private write(operationName: string, sessionId: string, entry: PendingSession, statement: () => void): void {
74
+ this.pending.set(sessionId, entry);
75
+ this.access.write(operationName, statement, {
76
+ key: `session_snapshots\u0000${sessionId}`,
77
+ settled: () => {
78
+ if (this.pending.get(sessionId) === entry) this.pending.delete(sessionId);
79
+ },
80
+ });
81
+ }
59
82
  }