gloomberb 0.19.2 → 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.
- package/README.md +7 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +20 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +5 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +20 -0
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/index.tsx +1 -2
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +2 -0
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +15 -2
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +15 -4
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +2 -0
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
|
@@ -2,7 +2,7 @@ import { t } from "../../i18n";
|
|
|
2
2
|
import type { ThemeColors } from "../../theme/colors";
|
|
3
3
|
import { useThemeColors } from "../../theme/theme-context";
|
|
4
4
|
import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
5
|
-
import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
|
|
5
|
+
import { clipToDisplayWidth, displayWidth, padTo, truncateToDisplayWidth } from "../../utils/format";
|
|
6
6
|
import { SplitBar, type SplitBarPart } from "./split-bar";
|
|
7
7
|
|
|
8
8
|
export type StatTone = "neutral" | "positive" | "negative" | "warning" | "accent" | "muted";
|
|
@@ -49,6 +49,13 @@ export interface StatGridProps {
|
|
|
49
49
|
width: number;
|
|
50
50
|
/** Fixed column count; by default as many as the widest item allows, up to 4, balanced over the rows. */
|
|
51
51
|
columns?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The most lines the grid takes. A whole-row cell too narrow for its value
|
|
54
|
+
* and detail wraps onto a second line, the detail under the value; with
|
|
55
|
+
* `maxLines` only the leading such cells the lines leave room for do, and
|
|
56
|
+
* the rest keep one line, cut (`statGridRows` counts the same).
|
|
57
|
+
*/
|
|
58
|
+
maxLines?: number;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
const MAX_COLUMNS = 4;
|
|
@@ -102,9 +109,207 @@ function layoutRows(items: StatItem[], columns: number): StatItem[][] {
|
|
|
102
109
|
return rows;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
/** What a grid's cells share: the label column, the inner width and one cell's width, in terminal cells. */
|
|
113
|
+
function gridGeometry(items: StatItem[], width: number, columns: number, nativePaneChrome: boolean) {
|
|
114
|
+
const labels = labelChars(items);
|
|
115
|
+
const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
116
|
+
const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
|
|
117
|
+
const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
|
|
118
|
+
return { labels, innerWidth, cellWidth, widestValue };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The label's column in a cell `width` wide: it grows into what the grid's
|
|
123
|
+
* widest value leaves, and never gets less than half the cell, so one long
|
|
124
|
+
* value cannot cut every label in a grid with a fixed column count.
|
|
125
|
+
*/
|
|
126
|
+
function cellLabelWidth(width: number, labels: number, widestValue: number): number {
|
|
127
|
+
return Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Cells a split bar takes after a value that leaves `room` in its column; none when too few to read. */
|
|
131
|
+
function splitBarCells(room: number): number {
|
|
132
|
+
return room >= 4 ? Math.min(SPLIT_BAR_CELLS, room) : 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\\.?";
|
|
136
|
+
/** A date, time or day count in a cell: `2026-10-29`, `18:00`, `Dec 9`, `30 Sep`, `Dec '26`, `2026`, `in 40 days`, `3d ago`. */
|
|
137
|
+
const DATED = new RegExp([
|
|
138
|
+
"\\d{4}-\\d{2}",
|
|
139
|
+
"\\b\\d{1,2}:\\d{2}\\b",
|
|
140
|
+
`\\b${MONTH} (?:'\\d{2}|\\d{1,4})\\b`,
|
|
141
|
+
`\\b\\d{1,2} ${MONTH}(?![a-z])`,
|
|
142
|
+
"\\b(?:19|20)\\d{2}\\b",
|
|
143
|
+
"\\bin \\d+ ?(?:d|days?|wks?|weeks?|mo|months?)\\b",
|
|
144
|
+
"\\b\\d+ ?(?:d|h|m|min|days?|hours?) ago\\b",
|
|
145
|
+
].join("|"));
|
|
146
|
+
|
|
147
|
+
/** `text` cut to `room` after its last whole word that fits, behind a `…`; mid-word only when its first word does not fit. */
|
|
148
|
+
function clipWords(text: string, room: number): string {
|
|
149
|
+
if (displayWidth(text) <= room) return text;
|
|
150
|
+
const words = text.split(" ");
|
|
151
|
+
for (let count = words.length - 1; count > 0; count--) {
|
|
152
|
+
const kept = `${words.slice(0, count).join(" ")}…`;
|
|
153
|
+
if (displayWidth(kept) <= room) return kept;
|
|
154
|
+
}
|
|
155
|
+
return clipToDisplayWidth(text, room);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* `text` cut to `width` without cutting a date: its ` · ` parts that have none
|
|
160
|
+
* give way first, from the last, cut short at a word and then to a `…` that
|
|
161
|
+
* says something is left out; then the words before a part's date
|
|
162
|
+
* (`1,561,837 shares… 16:02:15.123`). Only dates too wide for the width are
|
|
163
|
+
* cut at their end.
|
|
164
|
+
*/
|
|
165
|
+
function cutKeepingDates(text: string, width: number): string {
|
|
166
|
+
const parts = text.split(" · ");
|
|
167
|
+
const joined = () => parts.join(" · ");
|
|
168
|
+
const roomFor = (index: number) => width - (displayWidth(joined()) - displayWidth(parts[index]!));
|
|
169
|
+
for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
|
|
170
|
+
const part = parts[index]!;
|
|
171
|
+
if (DATED.test(part)) continue;
|
|
172
|
+
const room = roomFor(index);
|
|
173
|
+
if (room >= 4) parts[index] = clipWords(part, room);
|
|
174
|
+
else if (parts[index + 1] !== "…" && part !== "…") parts[index] = "…";
|
|
175
|
+
else parts.splice(index, 1);
|
|
176
|
+
}
|
|
177
|
+
for (let index = parts.length - 1; index >= 0 && displayWidth(joined()) > width; index--) {
|
|
178
|
+
const part = parts[index]!;
|
|
179
|
+
const at = part.search(DATED);
|
|
180
|
+
if (at <= 0) continue;
|
|
181
|
+
const date = part.slice(at);
|
|
182
|
+
const room = roomFor(index) - displayWidth(date) - 1;
|
|
183
|
+
parts[index] = room >= 4 ? `${clipWords(part.slice(0, at).trimEnd(), room)} ${date}` : date;
|
|
184
|
+
}
|
|
185
|
+
return truncateToDisplayWidth(joined(), width);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `text` on at most two lines no wider than `width`: broken after the ` · `
|
|
190
|
+
* parts that fit the first, else at its last space that does; the second line
|
|
191
|
+
* is cut where it has no date (cutKeepingDates).
|
|
192
|
+
*/
|
|
193
|
+
function twoLines(text: string, width: number): string[] {
|
|
194
|
+
const parts = text.split(" · ");
|
|
195
|
+
let first = "";
|
|
196
|
+
let taken = 0;
|
|
197
|
+
for (; taken < parts.length; taken++) {
|
|
198
|
+
const next = first ? `${first} · ${parts[taken]}` : parts[taken]!;
|
|
199
|
+
if (displayWidth(next) > width) break;
|
|
200
|
+
first = next;
|
|
201
|
+
}
|
|
202
|
+
let rest = parts.slice(taken).join(" · ");
|
|
203
|
+
if (!first) {
|
|
204
|
+
// The first part alone is wider than a line: it breaks at a space, or is cut.
|
|
205
|
+
const words = parts[0]!.split(" ");
|
|
206
|
+
let count = 0;
|
|
207
|
+
while (count < words.length && displayWidth(words.slice(0, count + 1).join(" ")) <= width) count++;
|
|
208
|
+
first = count ? words.slice(0, count).join(" ") : truncateToDisplayWidth(words[0]!, width);
|
|
209
|
+
rest = [words.slice(Math.max(1, count)).join(" "), ...parts.slice(1)].filter(Boolean).join(" · ");
|
|
210
|
+
}
|
|
211
|
+
return rest ? [first, cutKeepingDates(rest, width)] : [first];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A whole-row cell on its two lines (wrapStatCell). */
|
|
215
|
+
interface WrappedStatCell {
|
|
216
|
+
/** The value: one line, or both when it is wider than its column and the detail gives way. */
|
|
217
|
+
value: string[];
|
|
218
|
+
/** Cells of the split bar after the value on the first line; 0 for none. */
|
|
219
|
+
bar: number;
|
|
220
|
+
/** The detail on the second line, or null. */
|
|
221
|
+
detail: string | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Lines a wrapped cell takes: its value, then its detail. */
|
|
225
|
+
const WRAPPED_CELL_LINES = 2;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A whole-row cell (a `wide` item, or any cell of a one-column grid) that does
|
|
229
|
+
* not fit one line, on two: the value and its split bar on the first, and the
|
|
230
|
+
* detail on the second, in the longest form that fits, else cut where it has
|
|
231
|
+
* no date. A value wider than its column takes both lines, its bar after the
|
|
232
|
+
* first where it has room, when the detail can go (there is none, the pane
|
|
233
|
+
* offers an empty form, or it has no date); with a dated detail the value is
|
|
234
|
+
* cut instead, as a date is never what gives way.
|
|
235
|
+
* Null when the cell fits one line as it is, with the detail form that fits
|
|
236
|
+
* beside the value (an empty form counts: the pane chose to show none). Worked
|
|
237
|
+
* out in terminal cells for both renderers, so the grid's rows are the same
|
|
238
|
+
* count on each.
|
|
239
|
+
*/
|
|
240
|
+
function wrapStatCell(item: StatItem, width: number, labelWidth: number): WrappedStatCell | null {
|
|
241
|
+
const valueWidth = Math.max(1, width - labelWidth);
|
|
242
|
+
const valueCells = displayWidth(item.value);
|
|
243
|
+
const forms = item.detail ? [item.detail, ...(item.detailVariants ?? [])] : [];
|
|
244
|
+
const shown = forms.filter(Boolean);
|
|
245
|
+
const detailLine = () => shown.find((form) => displayWidth(form) <= valueWidth) ?? cutKeepingDates(shown.at(-1)!, valueWidth);
|
|
246
|
+
if (valueCells <= valueWidth) {
|
|
247
|
+
const bar = item.split ? splitBarCells(valueWidth - valueCells - 1) : 0;
|
|
248
|
+
const detailRoom = valueWidth - valueCells - (bar ? bar + 1 : 0) - 2;
|
|
249
|
+
const shortest = sizingDetail(item);
|
|
250
|
+
if (!shortest || (detailRoom > 1 && displayWidth(shortest) <= detailRoom)) return null;
|
|
251
|
+
return { value: [item.value], bar, detail: detailLine() };
|
|
252
|
+
}
|
|
253
|
+
const detailCanGo = shown.length === 0 || shown.length < forms.length || !shown.some((form) => DATED.test(form));
|
|
254
|
+
if (detailCanGo) {
|
|
255
|
+
const value = twoLines(item.value, valueWidth);
|
|
256
|
+
return { value, bar: item.split ? splitBarCells(valueWidth - displayWidth(value[0]!) - 1) : 0, detail: null };
|
|
257
|
+
}
|
|
258
|
+
return { value: [cutKeepingDates(item.value, valueWidth)], bar: 0, detail: detailLine() };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A laid-out row's one cell, wrapped, when it spans the row and does not fit one line (wrapStatCell). */
|
|
262
|
+
function wrappedRow(row: StatItem[], columns: number, geometry: ReturnType<typeof gridGeometry>): WrappedStatCell | null {
|
|
263
|
+
const item = row[0];
|
|
264
|
+
if (!item || row.length > 1 || (!item.wide && columns > 1)) return null;
|
|
265
|
+
const width = item.wide ? geometry.innerWidth : geometry.cellWidth;
|
|
266
|
+
return wrapStatCell(item, width, cellLabelWidth(width, geometry.labels, geometry.widestValue));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The grid's rows and which of them wrap: every whole-row cell too narrow for
|
|
271
|
+
* one line, in order, while the grid stays within `maxLines`; past that a cell
|
|
272
|
+
* keeps its one line, cut. Decided in terminal cells on both renderers, so a
|
|
273
|
+
* pane sizes the same rows on each.
|
|
274
|
+
*/
|
|
275
|
+
function planStatGrid(items: StatItem[], width: number, columns: number, maxLines = Number.POSITIVE_INFINITY) {
|
|
276
|
+
const rows = layoutRows(items, columns);
|
|
277
|
+
const geometry = gridGeometry(items, width, columns, false);
|
|
278
|
+
let spare = maxLines - rows.length;
|
|
279
|
+
const wrapped = rows.map((row) => {
|
|
280
|
+
if (spare < WRAPPED_CELL_LINES - 1) return null;
|
|
281
|
+
const cell = wrappedRow(row, columns, geometry);
|
|
282
|
+
if (cell) spare -= WRAPPED_CELL_LINES - 1;
|
|
283
|
+
return cell;
|
|
284
|
+
});
|
|
285
|
+
const lines = rows.length + wrapped.filter(Boolean).length * (WRAPPED_CELL_LINES - 1);
|
|
286
|
+
return { rows, wrapped, lines };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Rows a StatGrid takes, so panes can size what sits below it: a whole-row
|
|
291
|
+
* cell that wraps counts both its lines. With `maxLines`, the grid drawn with
|
|
292
|
+
* the same `maxLines`.
|
|
293
|
+
*/
|
|
294
|
+
export function statGridRows(items: StatItem[], width: number, columns?: number, { maxLines }: { maxLines?: number } = {}): number {
|
|
295
|
+
return planStatGrid(items, width, Math.max(1, columns ?? statGridColumns(items, width)), maxLines).lines;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The leading items that fit a band of `maxLines`, in whole rows: as many rows
|
|
300
|
+
* as there are lines, and the rows they leave for a whole-row cell to wrap
|
|
301
|
+
* into (`maxLines` on the grid, which `lines` counts).
|
|
302
|
+
*/
|
|
303
|
+
export function statGridWithin(items: StatItem[], width: number, maxLines: number, columns?: number): { items: StatItem[]; lines: number } {
|
|
304
|
+
if (maxLines < 1 || items.length === 0) return { items: [], lines: 0 };
|
|
305
|
+
const resolved = Math.max(1, columns ?? statGridColumns(items, width));
|
|
306
|
+
const shown = layoutRows(items, resolved).slice(0, maxLines).flat();
|
|
307
|
+
return { items: shown, lines: statGridRows(shown, width, resolved, { maxLines }) };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** A label in its column, cut and padded by terminal cells, so a wide character keeps the values aligned. */
|
|
311
|
+
function labelText(label: string, labelWidth: number): string {
|
|
312
|
+
return padTo(truncateToDisplayWidth(label, labelWidth - 1), labelWidth);
|
|
108
313
|
}
|
|
109
314
|
|
|
110
315
|
/**
|
|
@@ -114,16 +319,13 @@ export function statGridRows(items: StatItem[], width: number, columns?: number)
|
|
|
114
319
|
* hairline-split cells, so the top of the pane reads as one piece of chrome;
|
|
115
320
|
* the terminal draws aligned text columns.
|
|
116
321
|
*/
|
|
117
|
-
export function StatGrid({ items, width, columns: columnsProp }: StatGridProps) {
|
|
322
|
+
export function StatGrid({ items, width, columns: columnsProp, maxLines }: StatGridProps) {
|
|
118
323
|
const colors = useThemeColors();
|
|
119
324
|
const { nativePaneChrome } = useUiCapabilities();
|
|
120
325
|
if (items.length === 0) return null;
|
|
121
326
|
const columns = Math.max(1, columnsProp ?? statGridColumns(items, width));
|
|
122
|
-
const rows =
|
|
123
|
-
const labels =
|
|
124
|
-
const innerWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
125
|
-
const cellWidth = Math.max(8, Math.floor((innerWidth - CELL_GAP * (columns - 1)) / columns));
|
|
126
|
-
const widestValue = Math.max(0, ...items.filter((item) => !item.wide).map((item) => displayWidth(item.value)));
|
|
327
|
+
const { rows, wrapped: wrapping, lines: lineCount } = planStatGrid(items, width, columns, maxLines);
|
|
328
|
+
const { labels, innerWidth, cellWidth, widestValue } = gridGeometry(items, width, columns, !!nativePaneChrome);
|
|
127
329
|
|
|
128
330
|
const toneColor = (item: StatItem): string =>
|
|
129
331
|
item.color || (statToneColor(item.tone, colors) ?? (nativePaneChrome ? colors.textBright : colors.text));
|
|
@@ -132,25 +334,66 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
|
|
|
132
334
|
<Box
|
|
133
335
|
flexDirection="column"
|
|
134
336
|
paddingX={nativePaneChrome ? 0 : 1}
|
|
135
|
-
height={
|
|
337
|
+
height={lineCount}
|
|
136
338
|
flexShrink={0}
|
|
137
339
|
data-gloom-role="stat-grid"
|
|
138
340
|
style={nativePaneChrome ? { "--stat-label-w": `${labels}ch`, "--stat-columns": String(columns) } : undefined}
|
|
139
341
|
>
|
|
140
|
-
{rows.map((row, rowIndex) =>
|
|
342
|
+
{rows.map((row, rowIndex) => {
|
|
343
|
+
const wrapped = wrapping[rowIndex];
|
|
344
|
+
if (wrapped) {
|
|
345
|
+
const item = row[0]!;
|
|
346
|
+
const label = t(item.label);
|
|
347
|
+
const width = item.wide ? innerWidth : cellWidth;
|
|
348
|
+
const labelWidth = cellLabelWidth(width, labels, widestValue);
|
|
349
|
+
const [firstValue, ...moreValue] = wrapped.value;
|
|
350
|
+
return (
|
|
351
|
+
<Box key={rowIndex} height={WRAPPED_CELL_LINES} flexDirection="row" data-gloom-role="stat-grid-row">
|
|
352
|
+
<Box
|
|
353
|
+
key={item.id ?? `${rowIndex}:0`}
|
|
354
|
+
width={nativePaneChrome ? undefined : width}
|
|
355
|
+
height={WRAPPED_CELL_LINES}
|
|
356
|
+
flexDirection="row"
|
|
357
|
+
overflow="hidden"
|
|
358
|
+
data-gloom-role="stat-grid-cell"
|
|
359
|
+
data-wide={item.wide ? "true" : undefined}
|
|
360
|
+
data-wrapped="true"
|
|
361
|
+
data-gloom-stat-id={item.id}
|
|
362
|
+
>
|
|
363
|
+
<Text fg={colors.textDim} data-gloom-role="stat-grid-label">
|
|
364
|
+
{nativePaneChrome ? label : labelText(label, labelWidth)}
|
|
365
|
+
</Text>
|
|
366
|
+
<Box flexDirection="column" flexGrow={1} minWidth={0}>
|
|
367
|
+
<Box height={1} flexDirection="row">
|
|
368
|
+
<Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{firstValue}</Text>
|
|
369
|
+
{item.split && wrapped.bar ? (
|
|
370
|
+
<Box marginLeft={nativePaneChrome ? 0 : 1} flexShrink={nativePaneChrome ? 1 : 0} style={nativePaneChrome ? { minWidth: 0 } : undefined}>
|
|
371
|
+
<SplitBar parts={item.split} width={nativePaneChrome ? SPLIT_BAR_CELLS : wrapped.bar} />
|
|
372
|
+
</Box>
|
|
373
|
+
) : null}
|
|
374
|
+
</Box>
|
|
375
|
+
{moreValue.map((line, index) => (
|
|
376
|
+
<Text key={`value:${index}`} fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{line}</Text>
|
|
377
|
+
))}
|
|
378
|
+
{wrapped.detail != null ? (
|
|
379
|
+
<Text fg={colors.textDim} data-gloom-role="stat-grid-detail">{wrapped.detail}</Text>
|
|
380
|
+
) : null}
|
|
381
|
+
</Box>
|
|
382
|
+
</Box>
|
|
383
|
+
</Box>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return (
|
|
141
387
|
<Box key={rowIndex} height={1} flexDirection="row" gap={nativePaneChrome ? 0 : CELL_GAP} data-gloom-role="stat-grid-row">
|
|
142
388
|
{row.map((item, index) => {
|
|
143
389
|
const label = t(item.label);
|
|
144
390
|
const width = item.wide ? innerWidth : cellWidth;
|
|
145
|
-
|
|
146
|
-
// never gets less than half the cell, so one long value cannot cut
|
|
147
|
-
// every label in a grid with a fixed column count.
|
|
148
|
-
const labelWidth = Math.min(labels + 1, Math.max(4, Math.floor(width * 0.5), width - widestValue - 1));
|
|
391
|
+
const labelWidth = cellLabelWidth(width, labels, widestValue);
|
|
149
392
|
const valueWidth = Math.max(1, width - labelWidth);
|
|
150
393
|
const value = nativePaneChrome ? item.value : truncateToDisplayWidth(item.value, valueWidth);
|
|
151
394
|
// The bar shows only whole enough to read; the value never gives way to it.
|
|
152
395
|
const barRoom = valueWidth - displayWidth(value) - 1;
|
|
153
|
-
const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS :
|
|
396
|
+
const barCells = !item.split ? 0 : nativePaneChrome ? SPLIT_BAR_CELLS : splitBarCells(barRoom);
|
|
154
397
|
const detailWidth = valueWidth - displayWidth(value) - (barCells ? barCells + 1 : 0) - 2;
|
|
155
398
|
// The desktop cell has no gap between cells: its label column, inset and gaps take the room
|
|
156
399
|
// (measured short, as a cell is wider than a character, so a form that fits is never clipped).
|
|
@@ -177,7 +420,7 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
|
|
|
177
420
|
: undefined}
|
|
178
421
|
>
|
|
179
422
|
<Text fg={colors.textDim} data-gloom-role="stat-grid-label">
|
|
180
|
-
{nativePaneChrome ? label :
|
|
423
|
+
{nativePaneChrome ? label : labelText(label, labelWidth)}
|
|
181
424
|
</Text>
|
|
182
425
|
<Text fg={toneColor(item)} attributes={TextAttributes.BOLD} data-gloom-role="stat-grid-value">{value}</Text>
|
|
183
426
|
{item.split && barCells ? (
|
|
@@ -192,7 +435,8 @@ export function StatGrid({ items, width, columns: columnsProp }: StatGridProps)
|
|
|
192
435
|
);
|
|
193
436
|
})}
|
|
194
437
|
</Box>
|
|
195
|
-
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
196
440
|
</Box>
|
|
197
441
|
);
|
|
198
442
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logFailure } from "../../../utils/log-failure";
|
|
2
2
|
import { existsSync, readdirSync, type Dirent } from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, rename, rm } from "fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "path";
|
|
5
5
|
import { expandUserHome, getGloomberbDirs } from "../home";
|
|
6
6
|
import type { AppConfig } from "../../../types/config";
|
|
@@ -8,6 +8,8 @@ import { createDefaultConfig } from "../../../types/config";
|
|
|
8
8
|
import { debugLog } from "../../../utils/debug-log";
|
|
9
9
|
import { findAbsorbedCheckout, isDirectoryOrLink, isPluginDirectory } from "../../../plugins/loader";
|
|
10
10
|
import { EXTRACTED_PLUGINS } from "../../../plugins/extracted-plugins";
|
|
11
|
+
import { makePrivateDir, writePrivateFile } from "../../../utils/private-files";
|
|
12
|
+
import { tightenProfilePermissions } from "../../profile-permissions";
|
|
11
13
|
import type { ConfigMigrationHost } from "./migrations";
|
|
12
14
|
import {
|
|
13
15
|
normalizeConfigForSave,
|
|
@@ -113,11 +115,12 @@ const saveQueues = new Map<string, Promise<void>>();
|
|
|
113
115
|
let tempFileCounter = 0;
|
|
114
116
|
|
|
115
117
|
async function writeConfigFile(configPath: string, contents: string): Promise<void> {
|
|
116
|
-
await
|
|
118
|
+
await makePrivateDir(dirname(configPath));
|
|
117
119
|
// Unique per call: a clock tick is not, and two saves can land in one.
|
|
118
120
|
const tempPath = `${configPath}.${process.pid}.${Date.now()}.${tempFileCounter++}.tmp`;
|
|
119
121
|
try {
|
|
120
|
-
|
|
122
|
+
// Readable by the user alone: config.json holds broker credentials.
|
|
123
|
+
await writePrivateFile(tempPath, contents);
|
|
121
124
|
await rename(tempPath, configPath);
|
|
122
125
|
} catch (error) {
|
|
123
126
|
await rm(tempPath, { force: true }).catch(() => { /* Intentional ignore: Remove a partial temporary file while preserving the write error rethrown below. */ });
|
|
@@ -149,11 +152,12 @@ export async function saveConfig(config: AppConfig): Promise<void> {
|
|
|
149
152
|
|
|
150
153
|
export async function initDataDir(dataDir: string): Promise<AppConfig> {
|
|
151
154
|
configLog.info(`Initializing data directory: ${dataDir}`);
|
|
152
|
-
await
|
|
155
|
+
await makePrivateDir(dataDir);
|
|
153
156
|
const { config, needsSave } = await loadConfigState(dataDir);
|
|
154
157
|
if (needsSave) {
|
|
155
158
|
await saveConfig(config);
|
|
156
159
|
}
|
|
160
|
+
await tightenProfilePermissions(dataDir, [configFileFor(dataDir), getGloomberbDirs().configFile]);
|
|
157
161
|
return config;
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -166,7 +170,9 @@ export async function resetAllData(dataDir: string): Promise<void> {
|
|
|
166
170
|
|
|
167
171
|
export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
|
|
168
172
|
const { dataDir, ...rest } = config;
|
|
169
|
-
|
|
173
|
+
// The export carries broker credentials as config.json does: readable by the
|
|
174
|
+
// user alone, an existing file included, and never written through a link.
|
|
175
|
+
await writePrivateFile(expandHomePath(destPath), JSON.stringify(rest, null, 2));
|
|
170
176
|
}
|
|
171
177
|
|
|
172
178
|
export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
|
|
@@ -30,6 +30,7 @@ import { DEFAULT_THEME, themes } from "../../../theme/themes";
|
|
|
30
30
|
import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
|
|
31
31
|
import { sanitizeCommandBarOpens } from "../../../app/command-bar-intro";
|
|
32
32
|
import { sanitizeUpdatesConfig } from "../../../updater/channel";
|
|
33
|
+
import { ATTENTION_NOTICE_ACCOUNTS } from "../../../telemetry/attention-setting";
|
|
33
34
|
|
|
34
35
|
const configLog = debugLog.createLogger("config");
|
|
35
36
|
const reportedUnknownThemes = new Set<string>();
|
|
@@ -201,17 +202,28 @@ function withUpdates(updates: AppConfig["updates"]): Pick<AppConfig, "updates">
|
|
|
201
202
|
return updates ? { updates } : {};
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Only the switches that are set survive; an empty object is the same as none.
|
|
207
|
+
* Attention Counts is on by default, so a value that cannot be read turns it
|
|
208
|
+
* off instead: a hand-edited "false" or a broken block never counts as on.
|
|
209
|
+
*/
|
|
205
210
|
function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
|
|
206
|
-
if (
|
|
211
|
+
if (value === undefined) return undefined;
|
|
212
|
+
if (!isRecord(value)) return { attention: false };
|
|
207
213
|
const telemetry: TelemetryConfig = {
|
|
208
214
|
...(typeof value.crashReports === "boolean" ? { crashReports: value.crashReports } : {}),
|
|
209
215
|
...(typeof value.usage === "boolean" ? { usage: value.usage } : {}),
|
|
210
|
-
...(
|
|
216
|
+
...(value.attention !== undefined ? { attention: value.attention === true } : {}),
|
|
217
|
+
...sanitizeAttentionNotice(value.attentionNoticeShownTo),
|
|
211
218
|
};
|
|
212
219
|
return Object.keys(telemetry).length > 0 ? telemetry : undefined;
|
|
213
220
|
}
|
|
214
221
|
|
|
222
|
+
function sanitizeAttentionNotice(value: unknown): Pick<TelemetryConfig, "attentionNoticeShownTo"> {
|
|
223
|
+
const accounts = sanitizeUniqueStringList(value).slice(-ATTENTION_NOTICE_ACCOUNTS);
|
|
224
|
+
return accounts.length > 0 ? { attentionNoticeShownTo: accounts } : {};
|
|
225
|
+
}
|
|
226
|
+
|
|
215
227
|
/**
|
|
216
228
|
* Keeps the shape honest without judging the contents: a chord that does not
|
|
217
229
|
* parse stays in the file and is reported when the table resolves, so a typo
|
|
@@ -286,6 +298,7 @@ function sanitizeOnboardingProgress(value: unknown): OnboardingProgress | undefi
|
|
|
286
298
|
accountStatus,
|
|
287
299
|
checkoutOpenedAt: typeof value.checkoutOpenedAt === "string" ? value.checkoutOpenedAt : undefined,
|
|
288
300
|
desks: Array.isArray(value.desks) ? sanitizeUniqueStringList(value.desks) : undefined,
|
|
301
|
+
rerun: value.rerun === true ? true : undefined,
|
|
289
302
|
};
|
|
290
303
|
}
|
|
291
304
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readdir } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { logFailure } from "../utils/log-failure";
|
|
4
|
+
import { restrictDir, restrictFile } from "../utils/private-files";
|
|
5
|
+
|
|
6
|
+
/** The data folder's files that hold the Cloud session and cache, or logs of what the user did. */
|
|
7
|
+
const PRIVATE_DATA_FILES = [
|
|
8
|
+
".gloomberb-cache.db",
|
|
9
|
+
".gloomberb-cache.db-wal",
|
|
10
|
+
".gloomberb-cache.db-shm",
|
|
11
|
+
"stall-log.txt",
|
|
12
|
+
"stall-reports.json",
|
|
13
|
+
"crash-reports.json",
|
|
14
|
+
"desktop-view-ids.json",
|
|
15
|
+
];
|
|
16
|
+
/** `remote-control.json` and one per app (`remote-control.tui.json`): each holds the remote-control token. */
|
|
17
|
+
const REMOTE_ENDPOINT_FILE = /^remote-control(?:\.[\w-]+)?\.json$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tightens a profile made before its folders and files were created private,
|
|
21
|
+
* or under a umask that let others read them: the data folder and each
|
|
22
|
+
* config.json's folder open to the user alone, config.json (broker
|
|
23
|
+
* credentials) and the files above readable by the user alone. A step that
|
|
24
|
+
* fails (a folder another account owns, a disk without POSIX modes) is logged
|
|
25
|
+
* and the others still run; startup goes on either way. Nothing to do on
|
|
26
|
+
* Windows (see private-files.ts).
|
|
27
|
+
*/
|
|
28
|
+
export async function tightenProfilePermissions(dataDir: string, configFiles: readonly string[]): Promise<void> {
|
|
29
|
+
if (process.platform === "win32") return;
|
|
30
|
+
const steps: Array<() => Promise<void>> = [
|
|
31
|
+
...[...new Set([dataDir, ...configFiles.map((file) => dirname(file))])].map((dir) => () => restrictDir(dir)),
|
|
32
|
+
...[...new Set(configFiles)].map((file) => () => restrictFile(file)),
|
|
33
|
+
...PRIVATE_DATA_FILES.map((name) => () => restrictFile(join(dataDir, name))),
|
|
34
|
+
async () => {
|
|
35
|
+
for (const name of await readdir(dataDir)) {
|
|
36
|
+
if (REMOTE_ENDPOINT_FILE.test(name)) await restrictFile(join(dataDir, name));
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
for (const step of steps) {
|
|
41
|
+
try {
|
|
42
|
+
await step();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logFailure("data.profile-permissions", "Restricting a profile file to its user failed")(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -277,22 +277,18 @@ export class ResourceStore {
|
|
|
277
277
|
// Eviction goes by the order on disk, which has to hold the latest reads.
|
|
278
278
|
if (!this.flushTouches()) return false;
|
|
279
279
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
source_key: string;
|
|
286
|
-
size_bytes: number;
|
|
287
|
-
}, [number]>(
|
|
288
|
-
`SELECT namespace, kind, entity_key, variant_key, source_key, size_bytes
|
|
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
|
|
289
285
|
FROM resource_cache
|
|
290
286
|
ORDER BY last_accessed_at ASC, fetched_at ASC
|
|
291
287
|
LIMIT ?`,
|
|
292
288
|
);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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",
|
|
296
292
|
);
|
|
297
293
|
|
|
298
294
|
// A cache at its limit holds tens of thousands of rows, so eviction reads
|
|
@@ -306,24 +302,23 @@ export class ResourceStore {
|
|
|
306
302
|
if (!batch) return false;
|
|
307
303
|
if (batch.length === 0) return true;
|
|
308
304
|
|
|
309
|
-
|
|
310
|
-
for (const row of batch) {
|
|
311
|
-
if (this.withinSoftLimits(remainingCount, remainingSize)) break;
|
|
312
|
-
rowsToDelete.push(row);
|
|
313
|
-
remainingCount -= 1;
|
|
314
|
-
remainingSize -= row.size_bytes;
|
|
315
|
-
}
|
|
316
|
-
if (rowsToDelete.length === 0) return true;
|
|
317
|
-
|
|
305
|
+
let left = { count: remainingCount, size: remainingSize };
|
|
318
306
|
const pruned = this.access.writeCache("prune least recently used cached resources", () => {
|
|
307
|
+
left = { count: remainingCount, size: remainingSize };
|
|
319
308
|
const tx = this.db.transaction(() => {
|
|
320
|
-
for (const
|
|
321
|
-
|
|
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;
|
|
322
315
|
}
|
|
323
316
|
});
|
|
324
317
|
tx();
|
|
325
318
|
});
|
|
326
319
|
if (!pruned) return false;
|
|
320
|
+
remainingCount = left.count;
|
|
321
|
+
remainingSize = left.size;
|
|
327
322
|
}
|
|
328
323
|
return true;
|
|
329
324
|
}
|
|
@@ -382,25 +377,33 @@ export class ResourceStore {
|
|
|
382
377
|
key: Pick<ResourceCacheKey, "namespace" | "kind" | "entityKey">,
|
|
383
378
|
options: ListResourceOptions = {},
|
|
384
379
|
): CachedResourceRecord<T>[] {
|
|
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 ?? [])];
|
|
385
393
|
const rows = this.access.readCache("list cached resources", () => (
|
|
386
394
|
this.db
|
|
387
|
-
.query<ResourceCacheRow, [
|
|
395
|
+
.query<ResourceCacheRow, string[]>(
|
|
388
396
|
`SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
|
|
389
397
|
fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
|
|
390
398
|
FROM resource_cache
|
|
391
|
-
WHERE namespace = ?
|
|
399
|
+
WHERE ${["namespace = ?", "kind = ?", "entity_key = ?", ...filters].join(" AND ")}`,
|
|
392
400
|
)
|
|
393
|
-
.all(
|
|
401
|
+
.all(...params)
|
|
394
402
|
)) ?? [];
|
|
395
403
|
|
|
396
|
-
const allowedVariantKeys = options.variantKeys ? new Set(options.variantKeys.map(normalizeVariantKey)) : null;
|
|
397
|
-
const allowedSourceKeys = options.sourceKeys ? new Set(options.sourceKeys.map(normalizeSourceKey)) : null;
|
|
398
|
-
|
|
399
404
|
const records: CachedResourceRecord<T>[] = [];
|
|
400
405
|
for (const row of sortRows(rows)) {
|
|
401
406
|
if (!options.allowExpired && isExpired(row.expires_at)) continue;
|
|
402
|
-
if (allowedVariantKeys && !allowedVariantKeys.has(row.variant_key)) continue;
|
|
403
|
-
if (allowedSourceKeys && !allowedSourceKeys.has(row.source_key)) continue;
|
|
404
407
|
if (options.schemaVersion != null && row.schema_version !== options.schemaVersion) {
|
|
405
408
|
this.deleteExact({
|
|
406
409
|
namespace: row.namespace,
|
|
@@ -43,9 +43,13 @@ const sqliteLog = debugLog.createLogger("sqlite");
|
|
|
43
43
|
*
|
|
44
44
|
* In WAL mode the connection commits with `synchronous=NORMAL`: a commit no
|
|
45
45
|
* longer flushes the disk, and a power cut can cost the last few, never the
|
|
46
|
-
* database. A write of user data
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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.
|
|
49
53
|
*
|
|
50
54
|
* On the app's connection the cache writes of one turn of the event loop
|
|
51
55
|
* share a transaction and commit together once the turn is over: a batch of
|
|
@@ -99,6 +103,12 @@ export interface SqliteAccessOptions {
|
|
|
99
103
|
* connection's own level is left alone.
|
|
100
104
|
*/
|
|
101
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;
|
|
102
112
|
/** After each commit of the app's connection (see checkpointer.ts). */
|
|
103
113
|
onCommit?: () => void;
|
|
104
114
|
}
|
|
@@ -152,16 +162,17 @@ export function createSqliteAccess(db: Database, options: SqliteAccessOptions =
|
|
|
152
162
|
};
|
|
153
163
|
|
|
154
164
|
/**
|
|
155
|
-
* Commits `write` with a full flush when the connection's own
|
|
156
|
-
* NORMAL. The cache writes of the turn
|
|
157
|
-
* from (a queued write's `settled`, the
|
|
158
|
-
* never lands inside their batch
|
|
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.
|
|
159
170
|
*/
|
|
160
171
|
const durable = <T>(write: () => T): T => {
|
|
161
172
|
commitCacheBatch();
|
|
162
173
|
durableWrites += 1;
|
|
163
174
|
try {
|
|
164
|
-
if (!options.relaxedCommits || db.inTransaction) return write();
|
|
175
|
+
if (!options.relaxedCommits || db.inTransaction || options.flushedElsewhere?.()) return write();
|
|
165
176
|
db.exec("PRAGMA synchronous=FULL");
|
|
166
177
|
try {
|
|
167
178
|
return write();
|