@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
@@ -0,0 +1,324 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { debugLog } from "../../utils/debug-log";
3
+ import { logFailure } from "../../utils/log-failure";
4
+ import { isSqliteBusyError, trySqliteBusyOperation, withSqliteBusyRetry, withSqliteLockWait } from "./retry";
5
+
6
+ /**
7
+ * How long a write of the app waits for a lock another program holds (a
8
+ * second copy of the app, the `gloom` command, a restart's old process). The
9
+ * app runs SQLite on its only thread, so every millisecond of it is a frozen
10
+ * window: past this, the write waits in a queue instead.
11
+ */
12
+ const SQLITE_APP_WRITE_WAIT_MS = 200;
13
+ /**
14
+ * Reads are not held up by another program's writes (WAL); only its opening
15
+ * or recovering the database locks them out, briefly. A read that gives up
16
+ * throws, and user data that does not load reads as gone, so it waits longer.
17
+ */
18
+ const SQLITE_APP_READ_WAIT_MS = 1_000;
19
+ /**
20
+ * How long one timer tick of the queue may hold the thread: queued writes
21
+ * run in order until this is spent, and the rest wait for the next tick.
22
+ */
23
+ const QUEUED_WRITE_TICK_BUDGET_MS = 40;
24
+ /** How long a queued write waits for the lock within a tick. */
25
+ const QUEUED_WRITE_LOCK_WAIT_MS = 25;
26
+ const QUEUED_WRITE_FIRST_RETRY_MS = 100;
27
+ const QUEUED_WRITE_MAX_RETRY_MS = 2_000;
28
+ /** At shutdown nothing waits on the thread any more, and what is queued has nowhere else to go. */
29
+ const CLOSE_LOCK_WAIT_MS = 5_000;
30
+
31
+ const sqliteLog = debugLog.createLogger("sqlite");
32
+
33
+ /**
34
+ * How a store reaches its tables, by what the data is.
35
+ *
36
+ * The cache can always be fetched again, so it never waits: a read another
37
+ * program has locked is a miss, and a write is skipped. User data (tickers
38
+ * with their positions, plugin state, the session) waits briefly; a write of
39
+ * it that still finds the lock taken is queued and retried in the background,
40
+ * a little at a time, and each store reads its own queued writes back until
41
+ * they land. At shutdown the app waits up to 5 s for the lock to write what
42
+ * is left; a write still locked out then is lost, and the log says how many.
43
+ *
44
+ * In WAL mode the connection commits with `synchronous=NORMAL`: a commit no
45
+ * longer flushes the disk, and a power cut can cost the last few, never the
46
+ * database. A write of user data is flushed within a second or two by the
47
+ * checkpointer's thread, which flushes the WAL before copying it (see
48
+ * checkpointer.ts): on Windows the flush took the app's thread for seconds
49
+ * at a time. Where nothing else flushes (a command, a checkpointer that
50
+ * stopped), a write of user data flushes itself (`FULL`), and with it
51
+ * everything committed ahead of it. Where WAL does not take (a network
52
+ * share), every commit flushes.
53
+ *
54
+ * On the app's connection the cache writes of one turn of the event loop
55
+ * share a transaction and commit together once the turn is over: a batch of
56
+ * quotes, a list's touched rows. A write of user data, the queue and close
57
+ * commit them first, so a user write still commits on its own and flushes.
58
+ */
59
+ export interface SqliteAccess {
60
+ /** A read of cached data; null when another program has it locked. */
61
+ readCache<T>(operationName: string, read: () => T): T | null;
62
+ /** A write of cached data; false when another program has it locked, and nothing was written. */
63
+ writeCache(operationName: string, write: () => void): boolean;
64
+ /** A read of user data, or a statement asked for by the user: waits for a lock up to a second, then throws. */
65
+ run<T>(operationName: string, statement: () => T): T;
66
+ /**
67
+ * A write of user data. Returns whether it landed now; when it did not, it
68
+ * is queued behind the lock, in order with every write after it, and
69
+ * `settled` runs once it is written, superseded or given up on.
70
+ */
71
+ write(operationName: string, write: () => void, options?: SqliteWriteOptions): boolean;
72
+ /**
73
+ * One timer tick's worth of the queue: queued writes in order, for up to
74
+ * the tick's budget. True when the queue is empty.
75
+ */
76
+ flush(): boolean;
77
+ /** Writes what is still queued, waiting up to 5 s for the lock. */
78
+ close(): void;
79
+ }
80
+
81
+ interface SqliteWriteOptions {
82
+ /**
83
+ * The one row this write replaces or deletes in full. A queued write with
84
+ * the same key is superseded and dropped from the queue: the later write
85
+ * is all that row will hold, and it runs after every write queued before.
86
+ * A write that touches several rows has no key.
87
+ */
88
+ key?: string;
89
+ settled?: () => void;
90
+ }
91
+
92
+ export interface SqliteAccessOptions {
93
+ /**
94
+ * The app's own connection, on the thread that answers the user: no
95
+ * statement waits on another program for more than a moment. Without it
96
+ * (commands, tests) statements wait out the busy timeout and retry, as a
97
+ * command has nothing better to do.
98
+ */
99
+ interactive?: boolean;
100
+ /**
101
+ * The connection runs `synchronous=NORMAL`, as it does once WAL took, and
102
+ * a write of user data switches to `FULL` for its commit. Without it the
103
+ * connection's own level is left alone.
104
+ */
105
+ relaxedCommits?: boolean;
106
+ /**
107
+ * Whether another thread flushes the WAL soon after each commit (the
108
+ * checkpointer, while it runs). A write of user data then commits at
109
+ * `NORMAL` like the cache's.
110
+ */
111
+ flushedElsewhere?: () => boolean;
112
+ /** After each commit of the app's connection (see checkpointer.ts). */
113
+ onCommit?: () => void;
114
+ }
115
+
116
+ interface QueuedWrite {
117
+ operationName: string;
118
+ write: () => void;
119
+ key?: string;
120
+ settled?: () => void;
121
+ }
122
+
123
+ function settle(write: { settled?: () => void }): void {
124
+ try {
125
+ write.settled?.();
126
+ } catch (error) {
127
+ logFailure("data.sqlite", "Settling a queued write failed")(error);
128
+ }
129
+ }
130
+
131
+ function now(): number {
132
+ return typeof performance !== "undefined" && typeof performance.now === "function"
133
+ ? performance.now()
134
+ : Date.now();
135
+ }
136
+
137
+ export function createSqliteAccess(db: Database, options: SqliteAccessOptions = {}): SqliteAccess {
138
+ /** The app's cache writes of this turn, committed together once it is over (interactive only). */
139
+ let cacheBatch: ReturnType<typeof setTimeout> | null = null;
140
+ /** User writes running now: a cache write inside one commits on its own, outside any batch. */
141
+ let durableWrites = 0;
142
+
143
+ const commitCacheBatch = (): void => {
144
+ if (!cacheBatch) return;
145
+ clearTimeout(cacheBatch);
146
+ cacheBatch = null;
147
+ // A failed statement can roll the whole transaction back; its writes are
148
+ // gone then, which a cache can afford.
149
+ if (!db.inTransaction) return;
150
+ try {
151
+ db.exec("COMMIT");
152
+ options.onCommit?.();
153
+ } catch (error) {
154
+ logFailure("data.sqlite", "Committing cached writes failed")(error);
155
+ if (!db.inTransaction) return;
156
+ try {
157
+ db.exec("ROLLBACK");
158
+ } catch (rollbackError) {
159
+ logFailure("data.sqlite", "Rolling back cached writes failed")(rollbackError);
160
+ }
161
+ }
162
+ };
163
+
164
+ /**
165
+ * Commits `write` on its own, with a full flush when the connection's own
166
+ * level is NORMAL and nothing else flushes it. The cache writes of the turn
167
+ * commit first, wherever they came from (a queued write's `settled`, the
168
+ * write before), so the user write never lands inside their batch, which
169
+ * a failed commit rolls back whole.
170
+ */
171
+ const durable = <T>(write: () => T): T => {
172
+ commitCacheBatch();
173
+ durableWrites += 1;
174
+ try {
175
+ if (!options.relaxedCommits || db.inTransaction || options.flushedElsewhere?.()) return write();
176
+ db.exec("PRAGMA synchronous=FULL");
177
+ try {
178
+ return write();
179
+ } finally {
180
+ db.exec("PRAGMA synchronous=NORMAL");
181
+ }
182
+ } finally {
183
+ durableWrites -= 1;
184
+ }
185
+ };
186
+
187
+ if (!options.interactive) {
188
+ return {
189
+ readCache: (operationName, read) => trySqliteBusyOperation(operationName, read),
190
+ writeCache: (operationName, write) => trySqliteBusyOperation(operationName, () => {
191
+ write();
192
+ return true;
193
+ }) ?? false,
194
+ run: (operationName, statement) => withSqliteBusyRetry(operationName, statement),
195
+ write: (operationName, write, writeOptions) => {
196
+ try {
197
+ durable(() => withSqliteBusyRetry(operationName, write));
198
+ } finally {
199
+ settle({ settled: writeOptions?.settled });
200
+ }
201
+ return true;
202
+ },
203
+ flush: () => true,
204
+ close: () => {},
205
+ };
206
+ }
207
+
208
+ const queue: QueuedWrite[] = [];
209
+ let timer: ReturnType<typeof setTimeout> | null = null;
210
+ let retryMs = QUEUED_WRITE_FIRST_RETRY_MS;
211
+ let closed = false;
212
+
213
+ /**
214
+ * Writes the queue in order for up to `budgetMs`, each write waiting for
215
+ * the lock no longer than `lockWaitMs` or what is left of the budget.
216
+ * Says why it stopped: the queue is empty, the lock is still taken, or the
217
+ * budget is spent. A write already running finishes past the budget.
218
+ */
219
+ const drain = (budgetMs: number, lockWaitMs: number): "empty" | "locked" | "budget" => {
220
+ const startedAt = now();
221
+ while (queue.length > 0) {
222
+ const leftMs = budgetMs - (now() - startedAt);
223
+ if (leftMs <= 0) return "budget";
224
+ const next = queue[0]!;
225
+ try {
226
+ durable(() => withSqliteLockWait(next.operationName, next.write, Math.min(lockWaitMs, leftMs)));
227
+ options.onCommit?.();
228
+ } catch (error) {
229
+ if (isSqliteBusyError(error)) return "locked";
230
+ logFailure("data.sqlite", "A queued write failed")(error);
231
+ }
232
+ queue.shift();
233
+ settle(next);
234
+ }
235
+ return "empty";
236
+ };
237
+
238
+ const flush = (): boolean => {
239
+ const result = drain(QUEUED_WRITE_TICK_BUDGET_MS, QUEUED_WRITE_LOCK_WAIT_MS);
240
+ if (result === "empty") {
241
+ retryMs = QUEUED_WRITE_FIRST_RETRY_MS;
242
+ return true;
243
+ }
244
+ // A spent budget comes back at once, after whatever else waits on the
245
+ // thread; a lock still taken backs off.
246
+ if (result === "locked") retryMs = Math.min(QUEUED_WRITE_MAX_RETRY_MS, retryMs * 2);
247
+ schedule(result === "budget" ? 0 : retryMs);
248
+ return false;
249
+ };
250
+
251
+ function schedule(delayMs: number): void {
252
+ if (timer || closed) return;
253
+ timer = setTimeout(() => {
254
+ timer = null;
255
+ flush();
256
+ }, delayMs);
257
+ (timer as { unref?: () => void }).unref?.();
258
+ }
259
+
260
+ const tryCache = <T>(operationName: string, statement: () => T): { value: T } | null => {
261
+ try {
262
+ return { value: withSqliteLockWait(operationName, statement, 0) };
263
+ } catch (error) {
264
+ if (!isSqliteBusyError(error)) throw error;
265
+ logFailure("data.sqlite", "Skipped a cache statement: another program holds the database")(error);
266
+ return null;
267
+ }
268
+ };
269
+
270
+ /** False when another program holds the lock, and the write is skipped. */
271
+ const joinCacheBatch = (): boolean => {
272
+ // Inside a user write, a batch would hold that write's own statements until the turn is over.
273
+ if (durableWrites > 0) return true;
274
+ if (cacheBatch && db.inTransaction) return true;
275
+ if (cacheBatch) commitCacheBatch();
276
+ // Inside a transaction a statement of this connection opened, the write is part of it.
277
+ if (db.inTransaction) return true;
278
+ if (!tryCache("begin cached writes", () => db.exec("BEGIN IMMEDIATE"))) return false;
279
+ cacheBatch = setTimeout(commitCacheBatch, 0);
280
+ (cacheBatch as { unref?: () => void }).unref?.();
281
+ return true;
282
+ };
283
+
284
+ return {
285
+ readCache: (operationName, read) => tryCache(operationName, read)?.value ?? null,
286
+ writeCache: (operationName, write) => joinCacheBatch() && tryCache(operationName, write) !== null,
287
+ run: (operationName, statement) => withSqliteLockWait(operationName, statement, SQLITE_APP_READ_WAIT_MS),
288
+ write: (operationName, write, writeOptions = {}) => {
289
+ const { key, settled } = writeOptions;
290
+ if (queue.length === 0) {
291
+ try {
292
+ durable(() => withSqliteLockWait(operationName, write, SQLITE_APP_WRITE_WAIT_MS));
293
+ options.onCommit?.();
294
+ settle({ settled });
295
+ return true;
296
+ } catch (error) {
297
+ if (!isSqliteBusyError(error)) {
298
+ settle({ settled });
299
+ throw error;
300
+ }
301
+ }
302
+ }
303
+ if (key !== undefined) {
304
+ const superseded = queue.findIndex((queued) => queued.key === key);
305
+ if (superseded >= 0) settle(queue.splice(superseded, 1)[0]!);
306
+ }
307
+ queue.push({ operationName, write, key, settled });
308
+ if (queue.length === 1) sqliteLog.warn("A write is waiting for another program to release the database", { operation: operationName });
309
+ schedule(retryMs);
310
+ return false;
311
+ },
312
+ flush,
313
+ close: () => {
314
+ if (closed) return;
315
+ closed = true;
316
+ commitCacheBatch();
317
+ if (timer) clearTimeout(timer);
318
+ timer = null;
319
+ if (drain(CLOSE_LOCK_WAIT_MS, CLOSE_LOCK_WAIT_MS) === "empty") return;
320
+ sqliteLog.error("Writes could not be saved: another program kept the database locked", { count: queue.length });
321
+ for (const write of queue.splice(0)) settle(write);
322
+ },
323
+ };
324
+ }
@@ -0,0 +1,285 @@
1
+ import { resolve } from "path";
2
+ import { debugLog } from "../../utils/debug-log";
3
+ import { logFailure } from "../../utils/log-failure";
4
+ import { recordPerfSample } from "../../utils/perf-marks";
5
+
6
+ /**
7
+ * Pages SQLite lets the WAL grow to before the connection that commits
8
+ * copies them into the database itself. The app's connection keeps this as
9
+ * a backstop only: the checkpointer below does the copying long before.
10
+ */
11
+ export const SQLITE_DEFAULT_AUTOCHECKPOINT_PAGES = 1_000;
12
+ export const SQLITE_BACKSTOP_AUTOCHECKPOINT_PAGES = 10_000;
13
+ /** How soon after a commit the WAL is copied into the database, at most once per this. */
14
+ const CHECKPOINT_DELAY_MS = 1_000;
15
+ /** How long closing waits for the checkpointer to let go of the file (Windows cannot delete an open one). */
16
+ const CLOSE_WAIT_MS = 1_000;
17
+ /**
18
+ * How long the thread may go without answering while it owes an answer
19
+ * (opening its connection, or a checkpoint) before it counts as stuck. A
20
+ * stuck thread flushes nothing, and the app's commits of user data skip
21
+ * their flush while it runs, so past this they flush again. The slowest
22
+ * checkpoint measured on Windows took under a second, and the slowest
23
+ * flush in stall reports 8 s.
24
+ */
25
+ const STUCK_AFTER_MS = 30_000;
26
+
27
+ const checkpointLog = debugLog.createLogger("sqlite");
28
+
29
+ /**
30
+ * Runs on its own thread with its own connection. A passive checkpoint
31
+ * copies what it can without waiting on a lock and never stops the app's
32
+ * connection from reading or writing meanwhile. It flushes the WAL to disk
33
+ * before copying it, and the database after: that flush is what makes the
34
+ * app's writes of user data durable (access.ts), so it never runs with
35
+ * `synchronous=OFF`.
36
+ */
37
+ const WORKER_SOURCE = `
38
+ import { Database } from "bun:sqlite";
39
+ let db = null;
40
+ let closedFlag = null;
41
+ self.onmessage = (event) => {
42
+ const message = event.data;
43
+ if (message.type === "open") {
44
+ closedFlag = new Int32Array(message.closed);
45
+ try {
46
+ db = new Database(message.path);
47
+ db.exec("PRAGMA busy_timeout=1000");
48
+ db.exec("PRAGMA synchronous=FULL");
49
+ postMessage({ type: "ready" });
50
+ } catch (error) {
51
+ postMessage({ type: "failed", message: String(error && error.message || error) });
52
+ }
53
+ return;
54
+ }
55
+ if (message.type === "checkpoint") {
56
+ if (!db) return;
57
+ const startedAt = performance.now();
58
+ try {
59
+ const result = db.query("PRAGMA wal_checkpoint(PASSIVE)").get();
60
+ const endedAt = performance.now();
61
+ // Wall-clock times, so a pause on the app's thread can be lined up with it.
62
+ postMessage({
63
+ type: "checkpointed",
64
+ ms: endedAt - startedAt,
65
+ startedAt: performance.timeOrigin + startedAt,
66
+ endedAt: performance.timeOrigin + endedAt,
67
+ result,
68
+ });
69
+ } catch (error) {
70
+ postMessage({ type: "failed", message: String(error && error.message || error) });
71
+ }
72
+ return;
73
+ }
74
+ if (message.type === "close") {
75
+ try {
76
+ db?.close();
77
+ } catch (error) {
78
+ // Intentional ignore: the thread ends either way, and a failed close only leaves the handle to the OS.
79
+ }
80
+ db = null;
81
+ if (closedFlag) {
82
+ Atomics.store(closedFlag, 0, 1);
83
+ Atomics.notify(closedFlag, 0);
84
+ }
85
+ self.close();
86
+ }
87
+ };
88
+ `;
89
+
90
+ export interface WalCheckpointerOptions {
91
+ /** Overrides how long the thread may owe an answer before it counts as stuck. */
92
+ stuckAfterMs?: number;
93
+ /** Overrides the thread's code, so a test can run one that never answers. */
94
+ workerSource?: string;
95
+ }
96
+
97
+ export interface WalCheckpointer {
98
+ /** Writes were committed: the WAL is copied into the database shortly, off this thread. */
99
+ nudge(): void;
100
+ /** Stops the thread, waiting briefly for it to close its connection. */
101
+ close(): void;
102
+ }
103
+
104
+ interface WorkerMessage {
105
+ type: "ready" | "failed" | "checkpointed";
106
+ message?: string;
107
+ ms?: number;
108
+ startedAt?: number;
109
+ endedAt?: number;
110
+ result?: { busy?: number; log?: number; checkpointed?: number };
111
+ }
112
+
113
+ /**
114
+ * Copies the WAL into the database file on a thread of its own.
115
+ *
116
+ * SQLite does this inside a commit once the WAL passes 1,000 pages: one
117
+ * write in a few hundred copies about 4 MB into the database and flushes the
118
+ * disk, on the thread that asked for the write. That is the app's only
119
+ * thread, and the copy takes 150 ms to a second on a laptop, more under an
120
+ * antivirus. The app's connection leaves it to this instead (see
121
+ * database.ts), along with flushing what it commits, and `onFailure` puts
122
+ * SQLite's own schedule and the app's flushes back when the thread cannot
123
+ * run.
124
+ *
125
+ * Null where there is no Worker to run it on.
126
+ */
127
+ export function startWalCheckpointer(
128
+ dbPath: string,
129
+ onFailure: () => void,
130
+ options: WalCheckpointerOptions = {},
131
+ ): WalCheckpointer | null {
132
+ if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL.createObjectURL !== "function") return null;
133
+ let worker: Worker;
134
+ const source = URL.createObjectURL(new Blob([options.workerSource ?? WORKER_SOURCE], { type: "application/javascript" }));
135
+ const stuckAfterMs = options.stuckAfterMs ?? STUCK_AFTER_MS;
136
+ try {
137
+ worker = new Worker(source);
138
+ } catch (error) {
139
+ checkpointLog.warn("Could not start the WAL checkpointer; commits checkpoint themselves", { error: String(error) });
140
+ URL.revokeObjectURL(source);
141
+ return null;
142
+ }
143
+ (worker as { unref?: () => void }).unref?.();
144
+ const closed = new SharedArrayBuffer(4);
145
+ let failed = false;
146
+ let stopped = false;
147
+ let running = false;
148
+ let pending = false;
149
+ let timer: ReturnType<typeof setTimeout> | null = null;
150
+ /** Messages the thread has yet to answer, and the clock on its next answer. */
151
+ let owed = 0;
152
+ let watchdog: ReturnType<typeof setTimeout> | null = null;
153
+
154
+ const stopWatching = () => {
155
+ if (watchdog) clearTimeout(watchdog);
156
+ watchdog = null;
157
+ };
158
+
159
+ const fail = (reason: string) => {
160
+ if (failed || stopped) return;
161
+ failed = true;
162
+ stopWatching();
163
+ if (timer) clearTimeout(timer);
164
+ timer = null;
165
+ checkpointLog.warn("The WAL checkpointer stopped; commits checkpoint themselves", { reason });
166
+ try {
167
+ onFailure();
168
+ } catch (error) {
169
+ logFailure("data.sqlite", "Could not give commits their checkpoints back")(error);
170
+ }
171
+ };
172
+
173
+ /** False when the worker is gone (a crashed thread throws on a message), which ends the checkpointer. */
174
+ const post = (message: { type: string; path?: string; closed?: SharedArrayBuffer }): boolean => {
175
+ try {
176
+ worker.postMessage(message);
177
+ return true;
178
+ } catch (error) {
179
+ fail(error instanceof Error ? error.message : String(error));
180
+ return false;
181
+ }
182
+ };
183
+
184
+ const terminate = () => {
185
+ try {
186
+ worker.terminate();
187
+ } catch (error) {
188
+ logFailure("data.sqlite", "Stopping the WAL checkpointer failed")(error);
189
+ }
190
+ };
191
+
192
+ /**
193
+ * Restarts the clock whenever the thread answers and still owes one. A
194
+ * thread stuck without an error event would otherwise leave user writes
195
+ * unflushed until the backstop checkpoint or close.
196
+ */
197
+ const watch = () => {
198
+ stopWatching();
199
+ if (owed === 0 || failed || stopped) return;
200
+ watchdog = setTimeout(() => {
201
+ watchdog = null;
202
+ fail(`no answer in ${stuckAfterMs} ms`);
203
+ terminate();
204
+ URL.revokeObjectURL(source);
205
+ }, stuckAfterMs);
206
+ (watchdog as { unref?: () => void }).unref?.();
207
+ };
208
+
209
+ const ask = (message: { type: string; path?: string; closed?: SharedArrayBuffer }): boolean => {
210
+ if (!post(message)) return false;
211
+ owed += 1;
212
+ if (owed === 1) watch();
213
+ return true;
214
+ };
215
+
216
+ const answered = () => {
217
+ owed = Math.max(0, owed - 1);
218
+ watch();
219
+ };
220
+
221
+ const schedule = () => {
222
+ if (timer || running || failed || stopped) return;
223
+ timer = setTimeout(() => {
224
+ timer = null;
225
+ if (failed || stopped) return;
226
+ pending = false;
227
+ running = ask({ type: "checkpoint" });
228
+ }, CHECKPOINT_DELAY_MS);
229
+ (timer as { unref?: () => void }).unref?.();
230
+ };
231
+
232
+ worker.onmessage = (event: MessageEvent<WorkerMessage>) => {
233
+ const message = event.data;
234
+ if (message.type === "ready") {
235
+ answered();
236
+ URL.revokeObjectURL(source);
237
+ return;
238
+ }
239
+ if (message.type === "failed") {
240
+ fail(message.message ?? "unknown error");
241
+ return;
242
+ }
243
+ if (message.type === "checkpointed") {
244
+ answered();
245
+ running = false;
246
+ // On its own thread, so never the step a stall of the app's names.
247
+ recordPerfSample("sqlite.checkpoint", message.ms ?? 0, {
248
+ pages: message.result?.log,
249
+ copied: message.result?.checkpointed,
250
+ startedAt: Math.round(message.startedAt ?? 0),
251
+ endedAt: Math.round(message.endedAt ?? 0),
252
+ }, { lastStep: false });
253
+ if (pending) schedule();
254
+ }
255
+ };
256
+ worker.onerror = (event) => {
257
+ fail(event.message);
258
+ };
259
+ if (!ask({ type: "open", path: resolve(dbPath), closed })) {
260
+ terminate();
261
+ URL.revokeObjectURL(source);
262
+ return null;
263
+ }
264
+
265
+ return {
266
+ nudge() {
267
+ if (failed || stopped) return;
268
+ pending = true;
269
+ schedule();
270
+ },
271
+ close() {
272
+ if (stopped) return;
273
+ if (timer) clearTimeout(timer);
274
+ timer = null;
275
+ stopWatching();
276
+ const asked = !failed && post({ type: "close" });
277
+ stopped = true;
278
+ if (!asked) {
279
+ terminate();
280
+ return;
281
+ }
282
+ Atomics.wait(new Int32Array(closed), 0, 0, CLOSE_WAIT_MS);
283
+ },
284
+ };
285
+ }