gloomberb 0.2.0 → 0.3.1
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 +20 -19
- package/bin/gloomberb +3 -0
- package/package.json +5 -4
- package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
- package/src/app.tsx +1330 -0
- package/src/brokers/sync-broker-instance.test.ts +100 -0
- package/src/brokers/sync-broker-instance.ts +341 -0
- package/src/cli/commands/plugins.ts +173 -0
- package/src/cli/commands/portfolio.ts +210 -0
- package/src/cli/commands/search.ts +120 -0
- package/src/cli/commands/ticker.ts +375 -0
- package/src/cli/commands/watchlist.ts +210 -0
- package/src/cli/context.ts +62 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/helpers.ts +85 -0
- package/src/cli/index.ts +143 -0
- package/src/cli/ticker-resolution.ts +39 -0
- package/src/cli/types.ts +15 -0
- package/src/cli.test.ts +361 -0
- package/src/components/chart/chart-data.ts +239 -0
- package/src/components/chart/chart-renderer.ts +1199 -0
- package/src/components/chart/chart-types.ts +82 -0
- package/src/components/chart/chart-viewport.ts +21 -0
- package/src/components/chart/chart.test.ts +439 -0
- package/src/components/chart/comparison-chart-data.test.ts +109 -0
- package/src/components/chart/comparison-chart-data.ts +316 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
- package/src/components/chart/comparison-chart-renderer.ts +354 -0
- package/src/components/chart/comparison-stock-chart.tsx +1486 -0
- package/src/components/chart/cursor-motion.ts +48 -0
- package/src/components/chart/native/chart-rasterizer.ts +732 -0
- package/src/components/chart/native/kitty-adapter.ts +12 -0
- package/src/components/chart/native/kitty-manager.ts +142 -0
- package/src/components/chart/native/kitty-protocol.ts +110 -0
- package/src/components/chart/native/kitty-support.ts +71 -0
- package/src/components/chart/native/kitty.test.ts +620 -0
- package/src/components/chart/native/renderer-selection.ts +24 -0
- package/src/components/chart/native/surface-manager.ts +225 -0
- package/src/components/chart/native/surface-sync.ts +34 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
- package/src/components/chart/stock-chart.test.ts +107 -0
- package/src/components/chart/stock-chart.tsx +1556 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
- package/src/components/command-bar/command-bar.test.tsx +1524 -0
- package/src/components/command-bar/command-bar.tsx +4823 -0
- package/src/components/command-bar/command-registry.ts +261 -0
- package/src/components/command-bar/helpers.test.ts +224 -0
- package/src/components/command-bar/helpers.ts +371 -0
- package/src/components/command-bar/root-shortcuts.ts +158 -0
- package/src/components/command-bar/view-model.test.ts +152 -0
- package/src/components/command-bar/view-model.ts +185 -0
- package/src/components/command-bar/workflow-ops.test.ts +70 -0
- package/src/components/command-bar/workflow-ops.ts +433 -0
- package/src/components/command-bar/workflow-types.ts +127 -0
- package/src/components/detail-feed-view.test.tsx +105 -0
- package/src/components/detail-feed-view.tsx +295 -0
- package/src/components/index.ts +32 -0
- package/src/components/layout/floating-pane.tsx +49 -0
- package/src/components/layout/header.tsx +129 -0
- package/src/components/layout/pane-header.tsx +44 -0
- package/src/components/layout/pane.tsx +49 -0
- package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
- package/src/components/layout/pixel-mouse.test.ts +40 -0
- package/src/components/layout/shell.test.tsx +625 -0
- package/src/components/layout/shell.tsx +1200 -0
- package/src/components/layout/status-bar.test.tsx +173 -0
- package/src/components/layout/status-bar.tsx +138 -0
- package/src/components/markdown-editor.tsx +134 -0
- package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
- package/src/components/pane-settings-dialog.tsx +468 -0
- package/src/components/pane-template-wizard.test.tsx +52 -0
- package/src/components/pane-template-wizard.tsx +199 -0
- package/src/components/price-selector-dialog.tsx +195 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/tab-bar.tsx +17 -0
- package/src/components/ticker-badge-text.test.tsx +160 -0
- package/src/components/ticker-badge-text.tsx +58 -0
- package/src/components/ticker-badge.tsx +88 -0
- package/src/components/ticker-list-table.tsx +188 -0
- package/src/components/toggle-list.tsx +74 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/data-table.tsx +210 -0
- package/src/components/ui/external-link.tsx +22 -0
- package/src/components/ui/fields.tsx +187 -0
- package/src/components/ui/frame.tsx +73 -0
- package/src/components/ui/index.ts +53 -0
- package/src/components/ui/list-view.tsx +166 -0
- package/src/components/ui/loading.tsx +70 -0
- package/src/components/ui/status.tsx +83 -0
- package/src/components/ui/tabs.tsx +47 -0
- package/src/components/ui/toggle.tsx +164 -0
- package/src/components/ui/ui.test.tsx +134 -0
- package/src/data/app-persistence.ts +25 -0
- package/src/data/config-store.test.ts +310 -0
- package/src/data/config-store.ts +469 -0
- package/src/data/plugin-state-store.ts +102 -0
- package/src/data/resource-store.ts +323 -0
- package/src/data/session-store.ts +52 -0
- package/src/data/sqlite-cache.test.ts +103 -0
- package/src/data/sqlite-database.ts +62 -0
- package/src/data/sqlite-json.ts +19 -0
- package/src/data/ticker-repository.ts +45 -0
- package/src/data/ticker-store.ts +46 -0
- package/src/index.tsx +67 -0
- package/src/market-data/coordinator.test.ts +369 -0
- package/src/market-data/coordinator.ts +580 -0
- package/src/market-data/hooks.tsx +234 -0
- package/src/market-data/query-store.ts +28 -0
- package/src/market-data/request-types.test.ts +64 -0
- package/src/market-data/request-types.ts +70 -0
- package/src/market-data/result-types.ts +56 -0
- package/src/market-data/selectors.ts +166 -0
- package/src/market-data/trace.ts +7 -0
- package/src/pane-settings.test.ts +71 -0
- package/src/pane-settings.ts +73 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
- package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
- package/src/plugins/builtin/ai/index.test.tsx +139 -0
- package/src/plugins/builtin/ai/index.tsx +86 -0
- package/src/plugins/builtin/ai/providers.ts +99 -0
- package/src/plugins/builtin/ai/runner.ts +82 -0
- package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
- package/src/plugins/builtin/ai/screener-contract.ts +174 -0
- package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
- package/src/plugins/builtin/ai/settings.ts +79 -0
- package/src/plugins/builtin/ai/ticker-context.ts +64 -0
- package/src/plugins/builtin/ai/utils.ts +71 -0
- package/src/plugins/builtin/ask-ai.test.tsx +258 -0
- package/src/plugins/builtin/ask-ai.tsx +9 -0
- package/src/plugins/builtin/chat-controller.test.ts +217 -0
- package/src/plugins/builtin/chat-controller.ts +340 -0
- package/src/plugins/builtin/chat.test.tsx +451 -0
- package/src/plugins/builtin/chat.tsx +620 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
- package/src/plugins/builtin/comparison-chart.tsx +169 -0
- package/src/plugins/builtin/data-providers.ts +16 -0
- package/src/plugins/builtin/debug.tsx +351 -0
- package/src/plugins/builtin/help.test.tsx +110 -0
- package/src/plugins/builtin/help.tsx +281 -0
- package/src/plugins/builtin/layout-manager.tsx +264 -0
- package/src/plugins/builtin/manual-entry.tsx +25 -0
- package/src/plugins/builtin/news.tsx +143 -0
- package/src/plugins/builtin/notes-files.ts +58 -0
- package/src/plugins/builtin/notes.tsx +396 -0
- package/src/plugins/builtin/options-availability.test.tsx +179 -0
- package/src/plugins/builtin/options-availability.ts +158 -0
- package/src/plugins/builtin/options.tsx +364 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
- package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
- package/src/plugins/builtin/portfolio-list.test.ts +74 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
- package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
- package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
- package/src/plugins/builtin/sec.tsx +250 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
- package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
- package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
- package/src/plugins/builtin/yahoo.ts +18 -0
- package/src/plugins/event-bus.ts +37 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
- package/src/plugins/ibkr/account-cache.test.ts +101 -0
- package/src/plugins/ibkr/account-cache.ts +137 -0
- package/src/plugins/ibkr/broker-adapter.ts +168 -0
- package/src/plugins/ibkr/config.test.ts +68 -0
- package/src/plugins/ibkr/config.ts +262 -0
- package/src/plugins/ibkr/dialogs.tsx +83 -0
- package/src/plugins/ibkr/flex.test.ts +107 -0
- package/src/plugins/ibkr/flex.ts +267 -0
- package/src/plugins/ibkr/fundamental-parser.ts +149 -0
- package/src/plugins/ibkr/gateway-helpers.ts +28 -0
- package/src/plugins/ibkr/gateway-selection.ts +51 -0
- package/src/plugins/ibkr/gateway-service.test.ts +450 -0
- package/src/plugins/ibkr/gateway-service.ts +2228 -0
- package/src/plugins/ibkr/index.test.tsx +183 -0
- package/src/plugins/ibkr/index.tsx +153 -0
- package/src/plugins/ibkr/instance-selection.test.ts +121 -0
- package/src/plugins/ibkr/instance-selection.ts +36 -0
- package/src/plugins/ibkr/price-normalization.test.ts +38 -0
- package/src/plugins/ibkr/price-normalization.ts +59 -0
- package/src/plugins/ibkr/trade-badge.tsx +27 -0
- package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
- package/src/plugins/ibkr/trade-tab.tsx +1050 -0
- package/src/plugins/ibkr/trade-utils.test.ts +175 -0
- package/src/plugins/ibkr/trade-utils.ts +149 -0
- package/src/plugins/ibkr/trading-pane.tsx +438 -0
- package/src/plugins/ibkr/trading-state.test.ts +109 -0
- package/src/plugins/ibkr/trading-state.ts +339 -0
- package/src/plugins/loader.ts +71 -0
- package/src/plugins/pane-manager.test.ts +103 -0
- package/src/plugins/pane-manager.ts +974 -0
- package/src/plugins/plugin-persistence.ts +85 -0
- package/src/plugins/plugin-runtime.test.tsx +185 -0
- package/src/plugins/plugin-runtime.tsx +175 -0
- package/src/plugins/prediction-markets/cache.ts +114 -0
- package/src/plugins/prediction-markets/categories.ts +223 -0
- package/src/plugins/prediction-markets/chart.tsx +512 -0
- package/src/plugins/prediction-markets/columns.ts +133 -0
- package/src/plugins/prediction-markets/controller-data.ts +614 -0
- package/src/plugins/prediction-markets/controller.ts +725 -0
- package/src/plugins/prediction-markets/detail/book.tsx +118 -0
- package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
- package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
- package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
- package/src/plugins/prediction-markets/detail.test.tsx +47 -0
- package/src/plugins/prediction-markets/index.tsx +102 -0
- package/src/plugins/prediction-markets/keyboard.ts +64 -0
- package/src/plugins/prediction-markets/launch.test.ts +126 -0
- package/src/plugins/prediction-markets/launch.ts +240 -0
- package/src/plugins/prediction-markets/metrics.ts +311 -0
- package/src/plugins/prediction-markets/navigation.ts +90 -0
- package/src/plugins/prediction-markets/outcome-order.ts +19 -0
- package/src/plugins/prediction-markets/pane.test.tsx +425 -0
- package/src/plugins/prediction-markets/pane.tsx +294 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
- package/src/plugins/prediction-markets/rows.ts +229 -0
- package/src/plugins/prediction-markets/services/fetch.ts +97 -0
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
- package/src/plugins/prediction-markets/settings.ts +164 -0
- package/src/plugins/prediction-markets/status.ts +24 -0
- package/src/plugins/prediction-markets/table.tsx +84 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
- package/src/plugins/prediction-markets/types.ts +186 -0
- package/src/plugins/registry.ts +633 -0
- package/src/sources/exchange-rates.ts +18 -0
- package/src/sources/gloomberb-cloud.test.ts +199 -0
- package/src/sources/gloomberb-cloud.ts +326 -0
- package/src/sources/provider-errors.ts +19 -0
- package/src/sources/provider-router.test.ts +1476 -0
- package/src/sources/provider-router.ts +1405 -0
- package/src/sources/sec-edgar.test.ts +286 -0
- package/src/sources/sec-edgar.ts +526 -0
- package/src/sources/yahoo-finance.test.ts +14 -0
- package/src/sources/yahoo-finance.ts +1017 -0
- package/src/state/app-activity.test.ts +41 -0
- package/src/state/app-activity.ts +106 -0
- package/src/state/app-bootstrap.test.ts +471 -0
- package/src/state/app-bootstrap.ts +308 -0
- package/src/state/app-context.test.ts +406 -0
- package/src/state/app-context.test.tsx +132 -0
- package/src/state/app-context.tsx +969 -0
- package/src/state/selectors.ts +67 -0
- package/src/state/session-persistence.test.ts +128 -0
- package/src/state/session-persistence.ts +161 -0
- package/src/state/ticker-refresh-queue.test.ts +46 -0
- package/src/state/ticker-refresh-queue.ts +40 -0
- package/src/state/use-inline-tickers.ts +174 -0
- package/src/state/use-quote-streaming.test.ts +37 -0
- package/src/state/use-quote-streaming.test.tsx +70 -0
- package/src/state/use-quote-streaming.ts +80 -0
- package/src/state/use-session-snapshot.ts +84 -0
- package/src/test-support/data-provider.ts +22 -0
- package/src/theme/color-utils.ts +67 -0
- package/src/theme/colors.ts +128 -0
- package/src/theme/themes.test.ts +45 -0
- package/src/theme/themes.ts +491 -0
- package/src/types/broker.ts +105 -0
- package/src/types/config.ts +434 -0
- package/src/types/data-provider.ts +81 -0
- package/src/types/financials.ts +142 -0
- package/src/types/instrument.ts +29 -0
- package/src/types/persistence.ts +21 -0
- package/src/types/plugin.ts +354 -0
- package/src/types/ticker.ts +58 -0
- package/src/types/trading.ts +85 -0
- package/src/updater.test.ts +162 -0
- package/src/updater.ts +247 -0
- package/src/utils/api-client.ts +666 -0
- package/src/utils/ascii-chart.ts +172 -0
- package/src/utils/broker-instances.ts +47 -0
- package/src/utils/cli-output.ts +83 -0
- package/src/utils/currency-units.test.ts +49 -0
- package/src/utils/currency-units.ts +82 -0
- package/src/utils/debug-log.ts +133 -0
- package/src/utils/exchange-rate-map.ts +8 -0
- package/src/utils/format.ts +158 -0
- package/src/utils/fuzzy-search.ts +34 -0
- package/src/utils/market-status.ts +95 -0
- package/src/utils/options.ts +86 -0
- package/src/utils/price-history.ts +39 -0
- package/src/utils/quote-time.test.ts +22 -0
- package/src/utils/quote-time.ts +50 -0
- package/src/utils/sec.test.ts +55 -0
- package/src/utils/sec.ts +45 -0
- package/src/utils/ticker-list.test.ts +22 -0
- package/src/utils/ticker-list.ts +35 -0
- package/src/utils/ticker-metadata.ts +51 -0
- package/src/utils/ticker-search.test.ts +227 -0
- package/src/utils/ticker-search.ts +764 -0
- package/src/utils/ticker-tokenizer.test.ts +48 -0
- package/src/utils/ticker-tokenizer.ts +72 -0
- package/src/version.ts +1 -0
- package/install.mjs +0 -68
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import type {
|
|
4
|
+
AppConfig,
|
|
5
|
+
BrokerInstanceConfig,
|
|
6
|
+
ChartPreferences,
|
|
7
|
+
FloatingPlacementMemory,
|
|
8
|
+
LayoutConfig,
|
|
9
|
+
PanePlacementMemory,
|
|
10
|
+
PaneBinding,
|
|
11
|
+
PaneInstanceConfig,
|
|
12
|
+
SavedLayout,
|
|
13
|
+
} from "../types/config";
|
|
14
|
+
import {
|
|
15
|
+
cloneLayout,
|
|
16
|
+
createDefaultConfig,
|
|
17
|
+
createPaneInstanceId,
|
|
18
|
+
CURRENT_CONFIG_VERSION,
|
|
19
|
+
clonePaneSettings,
|
|
20
|
+
normalizePaneLayout,
|
|
21
|
+
} from "../types/config";
|
|
22
|
+
import type { Portfolio, Watchlist } from "../types/ticker";
|
|
23
|
+
import { debugLog } from "../utils/debug-log";
|
|
24
|
+
|
|
25
|
+
const configLog = debugLog.createLogger("config");
|
|
26
|
+
|
|
27
|
+
function getGlobalConfigDir(): string {
|
|
28
|
+
return join(process.env.HOME || "~", ".gloomberb");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getGlobalConfigFile(): string {
|
|
32
|
+
return join(getGlobalConfigDir(), "config.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getDataDir(): Promise<string | null> {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(getGlobalConfigFile(), "utf-8");
|
|
38
|
+
const config = JSON.parse(raw) as { dataDir?: string };
|
|
39
|
+
return config.dataDir || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export async function loadConfig(dataDir: string): Promise<AppConfig> {
|
|
47
|
+
const { config } = await loadConfigState(dataDir);
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; needsSave: boolean }> {
|
|
52
|
+
const configPath = join(dataDir, "config.json");
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(configPath, "utf-8");
|
|
56
|
+
const saved = JSON.parse(raw) as Record<string, unknown>;
|
|
57
|
+
return normalizeConfig(saved, dataDir);
|
|
58
|
+
} catch {
|
|
59
|
+
return { config: createDefaultConfig(dataDir), needsSave: true };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeConfig(saved: Record<string, unknown>, dataDir: string): { config: AppConfig; needsSave: boolean } {
|
|
64
|
+
const defaults = createDefaultConfig(dataDir);
|
|
65
|
+
const directLayout = sanitizeLayout(saved.layout, defaults.layout);
|
|
66
|
+
const layouts = sanitizeSavedLayouts(saved.layouts, directLayout);
|
|
67
|
+
const activeLayoutIndex = sanitizeActiveLayoutIndex(saved.activeLayoutIndex, layouts.length);
|
|
68
|
+
const layout = cloneLayout(layouts[activeLayoutIndex]?.layout ?? directLayout);
|
|
69
|
+
const syncedLayouts = layouts.map((entry, index) => (
|
|
70
|
+
index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
|
|
71
|
+
));
|
|
72
|
+
|
|
73
|
+
const config: AppConfig = {
|
|
74
|
+
dataDir,
|
|
75
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
76
|
+
baseCurrency: typeof saved.baseCurrency === "string" ? saved.baseCurrency : defaults.baseCurrency,
|
|
77
|
+
refreshIntervalMinutes: typeof saved.refreshIntervalMinutes === "number" ? saved.refreshIntervalMinutes : defaults.refreshIntervalMinutes,
|
|
78
|
+
portfolios: sanitizePortfolios(saved.portfolios, defaults.portfolios),
|
|
79
|
+
watchlists: sanitizeWatchlists(saved.watchlists, defaults.watchlists),
|
|
80
|
+
layout,
|
|
81
|
+
layouts: syncedLayouts,
|
|
82
|
+
activeLayoutIndex,
|
|
83
|
+
brokerInstances: sanitizeBrokerInstances(saved.brokerInstances),
|
|
84
|
+
plugins: sanitizeStringArray(saved.plugins, defaults.plugins),
|
|
85
|
+
disabledPlugins: sanitizeDisabledPlugins(saved, defaults.disabledPlugins),
|
|
86
|
+
pluginConfig: sanitizePluginConfig(saved.pluginConfig),
|
|
87
|
+
theme: typeof saved.theme === "string" ? saved.theme : defaults.theme,
|
|
88
|
+
chartPreferences: sanitizeChartPreferences(saved.chartPreferences, defaults.chartPreferences),
|
|
89
|
+
recentTickers: sanitizeStringArray(saved.recentTickers, defaults.recentTickers),
|
|
90
|
+
onboardingComplete: typeof saved.onboardingComplete === "boolean" ? saved.onboardingComplete : defaults.onboardingComplete,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const needsSave =
|
|
94
|
+
saved.configVersion !== CURRENT_CONFIG_VERSION
|
|
95
|
+
|| !isLayoutConfig(saved.layout)
|
|
96
|
+
|| !Array.isArray((saved.layout as { instances?: unknown })?.instances)
|
|
97
|
+
|| !Array.isArray(saved.layouts)
|
|
98
|
+
|| !Array.isArray(saved.brokerInstances)
|
|
99
|
+
|| !isPluginConfigMap(saved.pluginConfig)
|
|
100
|
+
|| !isChartPreferences(saved.chartPreferences)
|
|
101
|
+
|| typeof saved.activeLayoutIndex !== "number";
|
|
102
|
+
|
|
103
|
+
return { config, needsSave };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function saveConfig(config: AppConfig): Promise<void> {
|
|
107
|
+
const configPath = join(config.dataDir, "config.json");
|
|
108
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
109
|
+
|
|
110
|
+
const activeLayoutIndex = sanitizeActiveLayoutIndex(config.activeLayoutIndex, config.layouts.length || 1);
|
|
111
|
+
const layout = sanitizeLayout(config.layout, createDefaultConfig(config.dataDir).layout);
|
|
112
|
+
const layouts = sanitizeSavedLayouts(config.layouts, layout).map((entry, index) => (
|
|
113
|
+
index === activeLayoutIndex ? { ...entry, layout: cloneLayout(layout) } : entry
|
|
114
|
+
));
|
|
115
|
+
|
|
116
|
+
const persisted: AppConfig = {
|
|
117
|
+
...config,
|
|
118
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
119
|
+
portfolios: sanitizePortfolios(config.portfolios, []),
|
|
120
|
+
watchlists: sanitizeWatchlists(config.watchlists, []),
|
|
121
|
+
layout,
|
|
122
|
+
layouts,
|
|
123
|
+
activeLayoutIndex,
|
|
124
|
+
brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
|
|
125
|
+
plugins: sanitizeStringArray(config.plugins, []),
|
|
126
|
+
disabledPlugins: sanitizeDisabledPluginList(config.disabledPlugins),
|
|
127
|
+
pluginConfig: sanitizePluginConfig(config.pluginConfig),
|
|
128
|
+
chartPreferences: sanitizeChartPreferences(config.chartPreferences, createDefaultConfig(config.dataDir).chartPreferences),
|
|
129
|
+
recentTickers: sanitizeStringArray(config.recentTickers, []),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await writeFile(configPath, JSON.stringify(persisted, null, 2), "utf-8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function initDataDir(dataDir: string): Promise<AppConfig> {
|
|
136
|
+
configLog.info(`Initializing data directory: ${dataDir}`);
|
|
137
|
+
await mkdir(dataDir, { recursive: true });
|
|
138
|
+
const { config, needsSave } = await loadConfigState(dataDir);
|
|
139
|
+
if (needsSave) {
|
|
140
|
+
await saveConfig(config);
|
|
141
|
+
}
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function resetAllData(dataDir: string): Promise<void> {
|
|
146
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function exportConfig(config: AppConfig, destPath: string): Promise<void> {
|
|
150
|
+
const { dataDir, ...rest } = config;
|
|
151
|
+
await writeFile(destPath, JSON.stringify(rest, null, 2), "utf-8");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function importConfig(dataDir: string, srcPath: string): Promise<AppConfig> {
|
|
155
|
+
const raw = await readFile(srcPath, "utf-8");
|
|
156
|
+
const saved = JSON.parse(raw) as Record<string, unknown>;
|
|
157
|
+
const { config } = normalizeConfig(saved, dataDir);
|
|
158
|
+
await saveConfig(config);
|
|
159
|
+
return config;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sanitizeStringArray(value: unknown, fallback: string[]): string[] {
|
|
163
|
+
return Array.isArray(value)
|
|
164
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
165
|
+
: fallback;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sanitizeDisabledPluginList(value: unknown): string[] {
|
|
169
|
+
return [...new Set(sanitizeStringArray(value, []))];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sanitizeDisabledPlugins(saved: Record<string, unknown>, fallback: string[]): string[] {
|
|
173
|
+
return sanitizeDisabledPluginList(saved.disabledPlugins ?? fallback);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
177
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isPluginConfigMap(value: unknown): value is Record<string, Record<string, unknown>> {
|
|
181
|
+
if (!isPlainRecord(value)) return false;
|
|
182
|
+
return Object.values(value).every((entry) => isPlainRecord(entry));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sanitizePluginConfig(value: unknown): Record<string, Record<string, unknown>> {
|
|
186
|
+
if (!isPluginConfigMap(value)) return {};
|
|
187
|
+
return Object.fromEntries(
|
|
188
|
+
Object.entries(value).map(([pluginId, state]) => [pluginId, { ...state }]),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isChartPreferences(value: unknown): value is ChartPreferences {
|
|
193
|
+
if (!value || typeof value !== "object") return false;
|
|
194
|
+
const defaultRenderMode = (value as ChartPreferences).defaultRenderMode;
|
|
195
|
+
const renderer = (value as ChartPreferences).renderer;
|
|
196
|
+
return (defaultRenderMode === "area"
|
|
197
|
+
|| defaultRenderMode === "line"
|
|
198
|
+
|| defaultRenderMode === "candles"
|
|
199
|
+
|| defaultRenderMode === "ohlc")
|
|
200
|
+
&& (renderer === "auto" || renderer === "kitty" || renderer === "braille");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function sanitizeChartPreferences(value: unknown, fallback: ChartPreferences): ChartPreferences {
|
|
204
|
+
if (!value || typeof value !== "object") return { ...fallback };
|
|
205
|
+
|
|
206
|
+
const candidate = value as Partial<ChartPreferences>;
|
|
207
|
+
const defaultRenderMode = candidate.defaultRenderMode === "area"
|
|
208
|
+
|| candidate.defaultRenderMode === "line"
|
|
209
|
+
|| candidate.defaultRenderMode === "candles"
|
|
210
|
+
|| candidate.defaultRenderMode === "ohlc"
|
|
211
|
+
? candidate.defaultRenderMode
|
|
212
|
+
: fallback.defaultRenderMode;
|
|
213
|
+
const renderer = candidate.renderer === "auto"
|
|
214
|
+
|| candidate.renderer === "kitty"
|
|
215
|
+
|| candidate.renderer === "braille"
|
|
216
|
+
? candidate.renderer
|
|
217
|
+
: fallback.renderer;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
defaultRenderMode,
|
|
221
|
+
renderer,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sanitizePortfolios(value: unknown, fallback: Portfolio[]): Portfolio[] {
|
|
226
|
+
if (!Array.isArray(value)) return fallback.map((portfolio) => ({ ...portfolio }));
|
|
227
|
+
return value
|
|
228
|
+
.filter((entry): entry is Portfolio =>
|
|
229
|
+
!!entry
|
|
230
|
+
&& typeof entry === "object"
|
|
231
|
+
&& typeof (entry as Portfolio).id === "string"
|
|
232
|
+
&& typeof (entry as Portfolio).name === "string"
|
|
233
|
+
&& typeof (entry as Portfolio).currency === "string",
|
|
234
|
+
)
|
|
235
|
+
.map((entry) => ({ ...entry }));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sanitizeWatchlists(value: unknown, fallback: Watchlist[]): Watchlist[] {
|
|
239
|
+
if (!Array.isArray(value)) return fallback.map((watchlist) => ({ ...watchlist }));
|
|
240
|
+
return value
|
|
241
|
+
.filter((entry): entry is Watchlist =>
|
|
242
|
+
!!entry
|
|
243
|
+
&& typeof entry === "object"
|
|
244
|
+
&& typeof (entry as Watchlist).id === "string"
|
|
245
|
+
&& typeof (entry as Watchlist).name === "string",
|
|
246
|
+
)
|
|
247
|
+
.map((entry) => ({ ...entry }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sanitizeBrokerInstances(value: unknown): BrokerInstanceConfig[] {
|
|
251
|
+
if (!Array.isArray(value)) return [];
|
|
252
|
+
return value
|
|
253
|
+
.filter((entry): entry is BrokerInstanceConfig =>
|
|
254
|
+
!!entry
|
|
255
|
+
&& typeof entry === "object"
|
|
256
|
+
&& typeof (entry as BrokerInstanceConfig).id === "string"
|
|
257
|
+
&& typeof (entry as BrokerInstanceConfig).brokerType === "string"
|
|
258
|
+
&& typeof (entry as BrokerInstanceConfig).label === "string"
|
|
259
|
+
&& typeof (entry as BrokerInstanceConfig).config === "object"
|
|
260
|
+
&& (entry as BrokerInstanceConfig).config !== null,
|
|
261
|
+
)
|
|
262
|
+
.map((entry) => ({
|
|
263
|
+
...entry,
|
|
264
|
+
enabled: entry.enabled ?? true,
|
|
265
|
+
config: { ...entry.config },
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isLayoutConfig(value: unknown): value is LayoutConfig {
|
|
270
|
+
return !!value
|
|
271
|
+
&& typeof value === "object"
|
|
272
|
+
&& Array.isArray((value as LayoutConfig).instances)
|
|
273
|
+
&& Array.isArray((value as LayoutConfig).floating)
|
|
274
|
+
&& "dockRoot" in (value as Record<string, unknown>);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function sanitizePaneBinding(value: unknown, fallback: PaneBinding = { kind: "none" }): PaneBinding {
|
|
278
|
+
if (!value || typeof value !== "object") return fallback;
|
|
279
|
+
if ((value as PaneBinding).kind === "fixed" && typeof (value as Extract<PaneBinding, { kind: "fixed" }>).symbol === "string") {
|
|
280
|
+
return { kind: "fixed", symbol: (value as Extract<PaneBinding, { kind: "fixed" }>).symbol };
|
|
281
|
+
}
|
|
282
|
+
if ((value as PaneBinding).kind === "follow" && typeof (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId === "string") {
|
|
283
|
+
return { kind: "follow", sourceInstanceId: (value as Extract<PaneBinding, { kind: "follow" }>).sourceInstanceId };
|
|
284
|
+
}
|
|
285
|
+
if ((value as PaneBinding).kind === "none") return { kind: "none" };
|
|
286
|
+
return fallback;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sanitizeFloatingPlacementMemory(value: unknown): FloatingPlacementMemory | undefined {
|
|
290
|
+
if (!value || typeof value !== "object") return undefined;
|
|
291
|
+
const x = typeof (value as FloatingPlacementMemory).x === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).x)) : null;
|
|
292
|
+
const y = typeof (value as FloatingPlacementMemory).y === "number" ? Math.max(0, Math.round((value as FloatingPlacementMemory).y)) : null;
|
|
293
|
+
const width = typeof (value as FloatingPlacementMemory).width === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).width)) : null;
|
|
294
|
+
const height = typeof (value as FloatingPlacementMemory).height === "number" ? Math.max(1, Math.round((value as FloatingPlacementMemory).height)) : null;
|
|
295
|
+
if (x === null || y === null || width === null || height === null) return undefined;
|
|
296
|
+
return { x, y, width, height };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefined {
|
|
300
|
+
if (!value || typeof value !== "object") return undefined;
|
|
301
|
+
|
|
302
|
+
const docked = (() => {
|
|
303
|
+
const raw = (value as PanePlacementMemory).docked;
|
|
304
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
305
|
+
const path = Array.isArray((raw as { path?: unknown }).path)
|
|
306
|
+
? (raw as { path?: unknown }).path
|
|
307
|
+
?.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
|
|
308
|
+
: undefined;
|
|
309
|
+
const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
|
|
310
|
+
? (raw as { anchorInstanceId: string }).anchorInstanceId
|
|
311
|
+
: undefined;
|
|
312
|
+
const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
|
|
313
|
+
? (raw as { position: "left" | "right" | "above" | "below" }).position
|
|
314
|
+
: undefined;
|
|
315
|
+
if (!path && !anchorInstanceId && !position) return undefined;
|
|
316
|
+
return {
|
|
317
|
+
path,
|
|
318
|
+
anchorInstanceId,
|
|
319
|
+
position,
|
|
320
|
+
};
|
|
321
|
+
})();
|
|
322
|
+
|
|
323
|
+
const floating = sanitizeFloatingPlacementMemory((value as PanePlacementMemory).floating);
|
|
324
|
+
if (!docked && !floating) return undefined;
|
|
325
|
+
return { docked, floating };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function sanitizePaneInstances(value: unknown, fallback: LayoutConfig): PaneInstanceConfig[] {
|
|
329
|
+
if (!Array.isArray(value)) return cloneLayout(fallback).instances;
|
|
330
|
+
const seen = new Set<string>();
|
|
331
|
+
const instances = value
|
|
332
|
+
.filter((entry): entry is PaneInstanceConfig =>
|
|
333
|
+
!!entry
|
|
334
|
+
&& typeof entry === "object"
|
|
335
|
+
&& typeof (entry as PaneInstanceConfig).instanceId === "string"
|
|
336
|
+
&& typeof (entry as PaneInstanceConfig).paneId === "string",
|
|
337
|
+
)
|
|
338
|
+
.map((entry) => {
|
|
339
|
+
const instanceId = seen.has(entry.instanceId) ? createPaneInstanceId(entry.paneId) : entry.instanceId;
|
|
340
|
+
seen.add(instanceId);
|
|
341
|
+
return {
|
|
342
|
+
instanceId,
|
|
343
|
+
paneId: entry.paneId,
|
|
344
|
+
title: typeof entry.title === "string" ? entry.title : undefined,
|
|
345
|
+
binding: sanitizePaneBinding(entry.binding),
|
|
346
|
+
params: entry.params && typeof entry.params === "object"
|
|
347
|
+
? Object.fromEntries(
|
|
348
|
+
Object.entries(entry.params).filter((param): param is [string, string] => typeof param[1] === "string"),
|
|
349
|
+
)
|
|
350
|
+
: undefined,
|
|
351
|
+
settings: sanitizePaneSettings(entry.settings),
|
|
352
|
+
placementMemory: sanitizePlacementMemory(entry.placementMemory),
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
return instances.length > 0 ? instances : cloneLayout(fallback).instances;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sanitizePaneSettings(value: unknown): Record<string, unknown> | undefined {
|
|
359
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
360
|
+
|
|
361
|
+
const sanitizeValue = (entry: unknown): unknown => {
|
|
362
|
+
if (entry == null) return entry;
|
|
363
|
+
if (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") {
|
|
364
|
+
return entry;
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(entry)) {
|
|
367
|
+
return entry
|
|
368
|
+
.map((child) => sanitizeValue(child))
|
|
369
|
+
.filter((child) => child !== undefined);
|
|
370
|
+
}
|
|
371
|
+
if (typeof entry === "object") {
|
|
372
|
+
return Object.fromEntries(
|
|
373
|
+
Object.entries(entry as Record<string, unknown>)
|
|
374
|
+
.map(([key, child]) => [key, sanitizeValue(child)])
|
|
375
|
+
.filter(([, child]) => child !== undefined),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const settings = Object.fromEntries(
|
|
382
|
+
Object.entries(value as Record<string, unknown>)
|
|
383
|
+
.map(([key, entry]) => [key, sanitizeValue(entry)])
|
|
384
|
+
.filter(([, entry]) => entry !== undefined),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return Object.keys(settings).length > 0 ? clonePaneSettings(settings) : undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getDefaultFollowSourceInstanceId(instances: PaneInstanceConfig[]): string | null {
|
|
391
|
+
return instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId ?? null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sanitizeFloatingEntries(value: unknown, validInstanceIds: Set<string>): LayoutConfig["floating"] {
|
|
395
|
+
if (!Array.isArray(value)) return [];
|
|
396
|
+
return value
|
|
397
|
+
.filter((entry): entry is LayoutConfig["floating"][number] =>
|
|
398
|
+
!!entry
|
|
399
|
+
&& typeof entry === "object"
|
|
400
|
+
&& typeof (entry as LayoutConfig["floating"][number]).instanceId === "string"
|
|
401
|
+
&& typeof (entry as LayoutConfig["floating"][number]).x === "number"
|
|
402
|
+
&& typeof (entry as LayoutConfig["floating"][number]).y === "number"
|
|
403
|
+
&& typeof (entry as LayoutConfig["floating"][number]).width === "number"
|
|
404
|
+
&& typeof (entry as LayoutConfig["floating"][number]).height === "number",
|
|
405
|
+
)
|
|
406
|
+
.filter((entry) => validInstanceIds.has(entry.instanceId))
|
|
407
|
+
.map((entry) => ({
|
|
408
|
+
...entry,
|
|
409
|
+
x: Math.max(0, Math.round(entry.x)),
|
|
410
|
+
y: Math.max(0, Math.round(entry.y)),
|
|
411
|
+
width: Math.max(1, Math.round(entry.width)),
|
|
412
|
+
height: Math.max(1, Math.round(entry.height)),
|
|
413
|
+
zIndex: typeof entry.zIndex === "number" ? Math.round(entry.zIndex) : entry.zIndex,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function sanitizeLayout(value: unknown, fallback: LayoutConfig): LayoutConfig {
|
|
418
|
+
if (!isLayoutConfig(value)) {
|
|
419
|
+
return cloneLayout(fallback);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!Array.isArray((value as LayoutConfig & { instances?: unknown }).instances)) {
|
|
423
|
+
const layout = cloneLayout(fallback);
|
|
424
|
+
return normalizePaneLayout(layout, {
|
|
425
|
+
defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const instances = sanitizePaneInstances((value as LayoutConfig & { instances?: unknown }).instances, fallback);
|
|
430
|
+
const validInstanceIds = new Set(instances.map((entry) => entry.instanceId));
|
|
431
|
+
const dockRoot = (value as { dockRoot?: LayoutConfig["dockRoot"] }).dockRoot ?? null;
|
|
432
|
+
const floating = sanitizeFloatingEntries((value as { floating?: unknown }).floating, validInstanceIds);
|
|
433
|
+
|
|
434
|
+
const layout: LayoutConfig = {
|
|
435
|
+
dockRoot,
|
|
436
|
+
instances,
|
|
437
|
+
floating,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return normalizePaneLayout(layout, {
|
|
441
|
+
defaultFollowSourceInstanceId: getDefaultFollowSourceInstanceId(layout.instances),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function sanitizeSavedLayouts(value: unknown, fallbackLayout: LayoutConfig): SavedLayout[] {
|
|
446
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
447
|
+
return [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const layouts = value
|
|
451
|
+
.filter((entry): entry is SavedLayout =>
|
|
452
|
+
!!entry
|
|
453
|
+
&& typeof entry === "object"
|
|
454
|
+
&& typeof (entry as SavedLayout).name === "string",
|
|
455
|
+
)
|
|
456
|
+
.map((entry) => ({
|
|
457
|
+
name: entry.name,
|
|
458
|
+
layout: sanitizeLayout(entry.layout, fallbackLayout),
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
return layouts.length > 0 ? layouts : [{ name: "Default", layout: cloneLayout(fallbackLayout) }];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function sanitizeActiveLayoutIndex(value: unknown, layoutCount: number): number {
|
|
465
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value >= layoutCount) {
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { safeParseJson, serializeJson } from "./sqlite-json";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PLUGIN_STATE_SCHEMA_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export interface PluginStateRecord<T = unknown> {
|
|
7
|
+
value: T;
|
|
8
|
+
schemaVersion: number;
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CachedPluginStateRecord {
|
|
13
|
+
value: unknown;
|
|
14
|
+
schemaVersion: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
rawValue: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PluginStateStore {
|
|
20
|
+
private readonly cache = new Map<string, CachedPluginStateRecord>();
|
|
21
|
+
|
|
22
|
+
constructor(private readonly db: Database) {}
|
|
23
|
+
|
|
24
|
+
get<T>(pluginId: string, key: string, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): PluginStateRecord<T> | null {
|
|
25
|
+
const cacheKey = `${pluginId}:${key}`;
|
|
26
|
+
const row = this.db
|
|
27
|
+
.query<{ schema_version: number; value: string; updated_at: number }, [string, string]>(
|
|
28
|
+
"SELECT schema_version, value, updated_at FROM plugin_state WHERE plugin_id = ? AND key = ?",
|
|
29
|
+
)
|
|
30
|
+
.get(pluginId, key);
|
|
31
|
+
if (!row) {
|
|
32
|
+
this.cache.delete(cacheKey);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (row.schema_version !== schemaVersion) {
|
|
36
|
+
this.cache.delete(cacheKey);
|
|
37
|
+
this.delete(pluginId, key);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cached = this.cache.get(cacheKey);
|
|
42
|
+
if (cached && cached.schemaVersion === row.schema_version && cached.updatedAt === row.updated_at && cached.rawValue === row.value) {
|
|
43
|
+
return cached as PluginStateRecord<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const value = safeParseJson<T>(row.value);
|
|
47
|
+
if (value == null) {
|
|
48
|
+
this.cache.delete(cacheKey);
|
|
49
|
+
this.delete(pluginId, key);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const record: CachedPluginStateRecord = {
|
|
54
|
+
value,
|
|
55
|
+
schemaVersion: row.schema_version,
|
|
56
|
+
updatedAt: row.updated_at,
|
|
57
|
+
rawValue: row.value,
|
|
58
|
+
};
|
|
59
|
+
this.cache.set(cacheKey, record);
|
|
60
|
+
return record as PluginStateRecord<T>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set(pluginId: string, key: string, value: unknown, schemaVersion = DEFAULT_PLUGIN_STATE_SCHEMA_VERSION): void {
|
|
64
|
+
const rawValue = serializeJson(value);
|
|
65
|
+
const updatedAt = Date.now();
|
|
66
|
+
this.db
|
|
67
|
+
.query(
|
|
68
|
+
`INSERT OR REPLACE INTO plugin_state (plugin_id, key, schema_version, value, updated_at)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
70
|
+
)
|
|
71
|
+
.run(pluginId, key, schemaVersion, rawValue, updatedAt);
|
|
72
|
+
this.cache.set(`${pluginId}:${key}`, {
|
|
73
|
+
value,
|
|
74
|
+
schemaVersion,
|
|
75
|
+
updatedAt,
|
|
76
|
+
rawValue,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
delete(pluginId: string, key: string): void {
|
|
81
|
+
this.db.query("DELETE FROM plugin_state WHERE plugin_id = ? AND key = ?").run(pluginId, key);
|
|
82
|
+
this.cache.delete(`${pluginId}:${key}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
keys(pluginId: string): string[] {
|
|
86
|
+
return this.db
|
|
87
|
+
.query<{ key: string }, [string]>(
|
|
88
|
+
"SELECT key FROM plugin_state WHERE plugin_id = ? ORDER BY key",
|
|
89
|
+
)
|
|
90
|
+
.all(pluginId)
|
|
91
|
+
.map((row) => row.key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear(pluginId: string): void {
|
|
95
|
+
this.db.query("DELETE FROM plugin_state WHERE plugin_id = ?").run(pluginId);
|
|
96
|
+
for (const cacheKey of this.cache.keys()) {
|
|
97
|
+
if (cacheKey.startsWith(`${pluginId}:`)) {
|
|
98
|
+
this.cache.delete(cacheKey);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|